Go

Pachetul honk este un client cu suport pentru context, construit doar pe biblioteca standard și sigur pentru utilizare concurentă. Același modul include CLI-ul honk-me pentru scripturi, cron și CI.

Pachet
github.com/honk-me/honk-go Publicat
Cod sursă
github.com/honk-me/honk-go · Licență MIT
Cerințe
Go 1.22 sau mai nou.

Instalare

CLI-ul este atașat și ca binare precompilate la fiecare versiune publicată pe GitHub.

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

Oricine are o cheie API (honk_…) poate trimite mesaje în proiectul ei. Ține-o pe servere, în joburi și în secretele din CI, niciodată într-o aplicație de browser, de mobil sau de desktop.

Trimite un eveniment

Sau explicit: honk.New(honk.Options{URL: "https://honk-me.app", Key: key}). O eroare nil înseamnă că Honk a salvat mesajul, nu că o notificare a fost livrată.

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

Rețetă: cererea unui client, direct pe telefon

Salvează întâi cererea, apoi trimite notificarea în afara fluxului cererii. Un grup și o cheie de idempotență pentru fiecare cerere.

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
}

Scala Honk și incidentele

Helperele primesc opțiuni precum WithGroupKey, WithChannel și 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

Erori

Erorile sunt de tip *honk.Error și se compară cu valorile sentinel prin errors.Is. Retryable() îți spune dacă să încerci din nou cu aceeași cheie.

_, 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)
}

Reîncercări care nu trimit nimic de două ori

  • Fiecare trimitere are un Idempotency-Key: al tău sau un UUIDv7 nou. Aceeași cheie e refolosită la fiecare reîncercare, iar timp de 24 de ore Honk răspunde la o repetare cu id-ul original și duplicate: true, așa că un răspuns pierdut nu creează niciodată un al doilea mesaj.
  • Se reîncearcă doar erorile de rețea, cererile care expiră, 429 și 5xx, cu backoff exponențial și jitter complet, niciodată mai devreme decât Retry-After de la server. Celelalte răspunsuri 4xx nu se reîncearcă niciodată: corectează cererea.
  • Fiecare încercare expiră după 5 secunde, iar totul se oprește după 30. Dacă ar trebui să aștepte mai mult, de exemplu până se resetează cota zilnică la miezul nopții, eșuează imediat și îți spune când să reîncerci.
  • Câmpurile sunt verificate înainte de trimitere, iar toate câmpurile invalide sunt raportate deodată.