Go
The honk package is a context-aware client built on the standard library only and safe for concurrent use. The same module ships the honk-me CLI for scripts, cron and CI.
- Package
-
github.com/honk-me/honk-goPublished - Source
- github.com/honk-me/honk-go · MIT License
- Requirements
- Go 1.22 or later.
Install
The CLI is also attached as prebuilt binaries to every GitHub release.
go get github.com/honk-me/honk-go # library (Go 1.22+)
go install github.com/honk-me/honk-go/cmd/honk-me@latest # CLI Anyone with an ingestion key (honk_…) can post to its project. Keep it on servers, in jobs and in CI secrets, never in a browser, mobile or desktop app.
Send an event
Or explicitly: honk.New(honk.Options{URL: "https://honk-me.app", Key: key}). A nil error means Honk stored the message, not that a push was delivered.
import honk "github.com/honk-me/honk-go" // package honk
c, err := honk.FromEnv() // HONK_URL, HONK_KEY (+ optional HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
if err != nil {
log.Fatal(err)
}
_, err = c.Beep(ctx, "Backup finished", "nightly pg_dump took 42 s") Recipe: a customer request on your phone
Save the request first, then notify off the request path. One group and one idempotency key per request.
var notifier, _ = honk.FromEnv() // create once, reuse (keep-alive)
func onCustomerRequest(r CustomerRequest) {
// ...save the request first, then notify off the request path:
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_, err := notifier.Send(ctx, honk.Message{
Title: truncate("New request: "+r.Subject, 150),
Message: truncate(fmt.Sprintf("%s (%s) asked: %s", r.Name, r.Company, r.Body), 2000),
Priority: honk.PriorityHigh, // push right away
Category: honk.CategoryCustomers,
Channel: "requests",
GroupKey: fmt.Sprintf("requests/%d", r.ID), // one group per request
URL: fmt.Sprintf("https://shop.example.com/admin/requests/%d", r.ID),
Metadata: map[string]any{"request_id": strconv.Itoa(r.ID)},
}, honk.WithIdempotencyKey(fmt.Sprintf("request-%d", r.ID)))
if err != nil {
log.Printf("honk: %v", err) // never fail the customer's request
}
}()
}
func truncate(s string, n int) string { // by runes, never splits UTF-8
if r := []rune(s); len(r) > n {
return string(r[:n-1]) + "…"
}
return s
} The Honk scale and incidents
The helpers take options such as WithGroupKey, WithChannel and WithIdempotencyKey.
c.Loud(ctx, "Disk 91%", "/var on app-01", honk.WithGroupKey("disk/app-01/var"))
c.Light(ctx, "Deploy started", "v4.2.0", honk.WithChannel("deploys"))
c.Problem(ctx, "db/backup", "Backup failed", "pg_dump exited with 1") // a long honk by default
c.Recovery(ctx, "db/backup", "Backup OK", "pg_dump finished in 41 s") // a beep by default Errors
Errors are *honk.Error and match sentinels with errors.Is. Retryable() says whether to try again with the same key.
_, err := c.Long(ctx, "Payment failed", "Stripe declined order 1042", honk.WithGroupKey("payments/stripe"))
var he *honk.Error
switch {
case err == nil:
case errors.Is(err, honk.ErrValidation):
log.Printf("bug: %v", err) // he.Fields says what to fix
case errors.As(err, &he) && he.Retryable():
requeue(he.IdempotencyKey, he.RetryAfter)
default:
log.Print(err)
} Retries that never send twice
- Every send carries an
Idempotency-Key: yours, or a fresh UUIDv7. The same key is reused on every retry, and within 24 hours Honk answers a replay with the original id andduplicate: true, so a lost response never turns into a second message. - Only network errors, timeouts,
429and5xxare retried, with exponential backoff and full jitter, never sooner than the server’sRetry-After. Other4xxresponses are never retried; fix the request instead. - Each attempt times out after 5 seconds and everything stops after 30. A wait that would cross that deadline, like a daily quota that resets at midnight, fails right away and tells you when to retry.
- Fields are validated before sending, and all invalid fields are reported together.