Go

Das Paket honk ist ein Client mit Context-Unterstützung, gebaut nur auf der Standardbibliothek und sicher bei paralleler Nutzung. Dasselbe Modul liefert die CLI honk-me für Skripte, Cron und CI.

Paket
github.com/honk-me/honk-go Veröffentlicht
Quellcode
github.com/honk-me/honk-go · MIT-Lizenz
Voraussetzungen
Go 1.22 oder neuer.

Installation

Die CLI gibt es außerdem als vorkompilierte Binärdateien bei jedem GitHub-Release.

Terminal
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

Mit einem Ingest-Schlüssel (honk_…) kann jeder an sein Projekt senden. Er gehört auf Server, in Jobs und in CI-Secrets, nie in einen Browser, eine Mobil- oder Desktop-App.

Ein Ereignis senden

Oder explizit: honk.New(honk.Options{URL: "https://honk-me.app", Key: key}). Ist der Fehler nil, hat Honk die Nachricht gespeichert. Ob ein Push zugestellt wurde, sagt er nicht.

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")

Rezept: eine Kundenanfrage auf deinem Handy

Speichere zuerst die Anfrage und benachrichtige dann außerhalb des Request-Pfads. Eine Gruppe und ein Idempotency-Key pro Anfrage.

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
}

Die Honk-Skala und Vorfälle

Die Helfer nehmen Optionen wie WithGroupKey, WithChannel und 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

Fehler

Fehler sind vom Typ *honk.Error und lassen sich mit errors.Is gegen Sentinels prüfen. Retryable() sagt dir, ob du es mit demselben Schlüssel erneut versuchen solltest.

_, 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 ohne Doppelversand

  • Jedes Senden hat einen Idempotency-Key: deinen oder eine neue UUIDv7. Jeder Retry nutzt denselben Schlüssel, und innerhalb von 24 Stunden beantwortet Honk eine erneut gesendete Anfrage mit der ursprünglichen ID und duplicate: true. Geht eine Antwort verloren, entsteht also nie eine zweite Nachricht.
  • Wiederholt werden nur Netzwerkfehler, Timeouts, 429 und 5xx, mit exponentiellem Backoff und vollem Jitter, nie früher als das Retry-After des Servers. Andere 4xx-Antworten werden nie wiederholt: Korrigiere stattdessen die Anfrage.
  • Jeder Versuch bricht nach 5 Sekunden ab, und nach insgesamt 30 Sekunden ist Schluss. Wäre die nötige Wartezeit länger, etwa bis ein Tageskontingent um Mitternacht zurückgesetzt wird, schlägt der Aufruf sofort fehl und sagt dir, wann du es erneut versuchen kannst.
  • Felder werden vor dem Senden geprüft, und alle ungültigen Felder werden auf einmal gemeldet.