honk

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 21 Imported by: 0

README

honk-go

CI Go Reference

Official Go client and CLI for Honk, the inbox that turns events from your apps, scripts, cron jobs and CI into calm, grouped push notifications on your phone.

  • Package honk: context-aware, standard library only, safe for concurrent use.
  • CLI honk-me for shell scripts, cron and CI (replaces the cURL snippet).
  • Retries with backoff, Retry-After, a total deadline and an idempotency key on every send, so a retry never creates a duplicate.

The ingestion key (honk_…) is a secret: keep it in the environment or a secret store, never in source code or client apps.

Install

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

The CLI is also attached as prebuilt binaries (Linux, macOS, Windows; amd64 and arm64) to every GitHub release.

Create a project and an ingestion key at honk-me.app. Its Integrations page generates ready-to-paste code for the library and the CLI.

Quick start

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

Or explicitly: honk.New(honk.Options{URL: "https://honk.example.com", Key: key}).

The Honk scale

Every severity has a horn name. Use either; the SDK always sends the canonical value.

Horn Severity Method Constant CLI
light honk light (info) c.Light(ctx, title, message, opts...) honk.Light honk-me light …
beep-beep beep (success) c.Beep(…) honk.Beep honk-me beep …
loud honk loud (warning) c.Loud(…) honk.Loud honk-me loud …
long honk long (error) c.Long(…) honk.Long honk-me long …
blast blast (critical) c.Blast(…) honk.Blast honk-me blast …

The horn constants are aliases (honk.Loud == honk.SeverityWarning), and Severity: "LOUD" is normalized too, so alias and canonical spellings are the same event, also for idempotency. Long and Blast push at least as high priority. Info, Success, Warning, Error and Critical remain as synonyms; honk.ParseSeverity("Blast") returns SeverityCritical.

Recipe: notify me when a customer asks for something

One group per request (requests/<id>) and a stable idempotency key: two different customers never fold into one notification, and a retried handler or job never buzzes twice.

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
}

From a queue worker, return the error when he.Retryable() and let the queue retry with the same key.

Grouping in three lines

Messages with the same GroupKey (per project, environment, source and channel) form one group: the first one pushes, repeats update it calmly instead of buzzing again. Use one key per customer request (requests/<id>), and a shared key only for repeats of the same problem (queue/failed-jobs). Problem/Recovery pairs need a GroupKey.

Sending

func (c *Client) Send(ctx context.Context, m honk.Message, opts ...honk.Option) (*honk.Accepted, error)
// Accepted{ID, Duplicate, ReceivedAt}
Field Notes
Message required, 1–8192 bytes UTF-8, line breaks allowed
Title ≤ 160 chars, one line; default: first line of Message
Severity honk.Light Beep Loud Long Blast (or SeverityInfo … SeverityCritical, or any-case strings); Long/Blast push at least as high
Priority PriorityLow PriorityNormal PriorityHigh PriorityUrgent (urgent needs a key with allow urgent)
Category CategoryInfrastructure, CategorySecurity, CategoryBackups, CategoryDeployments, CategoryPayments, CategoryCustomers, CategorySales, CategoryAutomation, CategoryPersonal, CategoryOther
Source / Environment / Channel ≤ 64 / 32 / 64 chars; default api / default / general or Options.Defaults
GroupKey ≤ 128 chars
EventType EventTypeEvent EventTypeProblem EventTypeRecovery (recovery needs GroupKey)
OccurredAt time.Time, sent as UTC RFC 3339 with milliseconds
URL / ImageURL https:// only, no credentials (ImageURL: no #fragment; fetched by the server afterwards)
Actions []honk.Action{{Title, URL}}, up to 3 buttons, the first is the primary; see below
Metadata map[string]any, ≤ 16 keys [A-Za-z0-9_.-]{1,64}; string (≤ 512 chars), number or bool values
TTLSeconds push lifetime 60–86400 (0 = default 3600)
SourceSequence *int64 (honk.Ptr[int64](n)), 0 … 2^53-1, needs GroupKey

Zero values are omitted. A nil error means Honk durably stored the message (202), not that a push was delivered or read. honk.EncodeMessage(m, defaults) returns the exact JSON.

Helpers take the core fields plus options (WithIdempotencyKey, WithSeverity, WithPriority, WithCategory, WithSource, WithEnvironment, WithChannel, WithGroupKey, WithOccurredAt, WithURL, WithImageURL, WithActions, WithMetadata, WithTTLSeconds, WithSourceSequence):

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.Beep(...); c.Long(...); c.Blast(...)
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
Buttons (actions)

Up to three buttons on the message, in display order: reply to the customer, call them, open the order.

_, err := c.Send(ctx, honk.Message{
	Title:    "New request: online shop quote",
	Message:  "Emily Carter (Acme) asked for a quote: online shop, 40 products",
	Category: honk.CategoryCustomers,
	GroupKey: "requests/4812",
	Actions: []honk.Action{
		{Title: "Reply", URL: "mailto:emily@example.com?subject=" + url.PathEscape("Your quote")},
		{Title: "Call Emily", URL: "tel:+15550134"},
	},
})
  • Title: 1–40 characters, one line, shown as sent.
  • URL, at most 2048 bytes without spaces: https:// (no credentials); mailto: with one address and optionally ?subject=…&body=… (percent-encoded with url.PathEscape, no other keys); tel: with a number (digits, - . ( ), + only first); sms: with a number and optionally ?body=…. Other schemes are refused.
  • Honk never opens or fetches them; the app does when you tap one. They appear on the message in the app and the web inbox, and on iPhone notifications that show the message text. Errors name the button: actions[1].url.
  • Helpers take honk.WithActions(honk.Action{…}, …); the CLI takes --action "Call Emily=tel:+15550134".
Options
honk.New(honk.Options{
	URL:      "https://honk.example.com",
	Key:      key,
	Timeout:  5 * time.Second,  // per attempt
	Retries:  4,                // after the first attempt; honk.NoRetries disables
	Deadline: 30 * time.Second, // total, waits included; a shorter ctx deadline wins
	Defaults: honk.Defaults{Source: "billing", Environment: "production"},
	SkipValidation: false,      // local checks (the server always validates)
	HTTPClient: myClient,       // proxies, tracing; redirects are never followed
})

Retries and idempotency, guaranteed

  • Every send carries an Idempotency-Key: yours (WithIdempotencyKey), or a fresh UUIDv7 (honk.NewIdempotencyKey()). The same key is reused on every retry. Within 24 h Honk answers a replay with the original ID and Duplicate: true, so a lost response never creates a second message.
  • Only network errors, timeouts, 429 and 5xx are retried, with exponential backoff and full jitter (rand(0, min(8 s, 0.5 s·2ⁿ))), never sooner than the server's Retry-After.
  • Everything stops at the deadline (or the context's): if the next wait would cross it (for example a daily quota that resets at midnight), the error is returned at once with RetryAfter.
  • 4xx other than 429 are never retried: fix the request instead.
  • Short per-attempt timeouts and a keep-alive transport: create one Client and share it.

Errors

Errors are *honk.Error (Kind, Status, Code, Fields, Local, RequestID, IdempotencyKey, Attempts, RetryAfter, Retryable()), and match sentinels with errors.Is:

Sentinel / Kind When What to do
ErrValidation / KindValidation rejected locally (Local) or 400/413/415/422; Fields lists every problem fix the message
ErrAuth / KindAuth 401 invalid_key, 403 priority_not_allowed, project_suspended, workspace_suspended fix the key or the priority
ErrQuota / KindQuota 429 quota_exceeded (daily, resets at UTC midnight) or rate_limited, after retries retry after RetryAfter
ErrConflict / KindConflict 409 idempotency_conflict: same key, different payload new key or original payload
ErrNetwork / KindNetwork unreachable on every attempt retry later, same key
ErrTimeout / KindTimeout attempts (or the context) timed out; maybe stored retry later, same key
ErrServer / KindServer 5xx on every attempt retry later, same key

KindCanceled wraps context.Canceled; timeouts also match context.DeadlineExceeded when the context expired.

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

CLI: honk-me

For shell scripts, cron and CI. Reads HONK_URL and HONK_KEY (never pass the key as a flag), plus optional HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL and HONK_IDEMPOTENCY_KEY.

honk-me loud "Disk 91%"                                   # shortcut: light, beep, loud, long, blast
honk-me beep "Backup finished" "nightly pg_dump took 42 s" # [TITLE] MESSAGE, flags anywhere
honk-me send --title "Disk almost full" --message "/var at 91%" --severity loud \
  --group-key "disk/$(hostname)/var" --source "$(hostname)" --meta host="$(hostname)" --meta used:=91
honk-me problem  --group-key db/backup --title "Backup failed" --message "pg_dump exited with 1"
honk-me recovery --group-key db/backup --title "Backup OK"     --message "pg_dump finished"
tail -c 8000 /var/log/backup.log | honk-me send --title "Backup log" --message -   # message from stdin
honk-me send --message "Front door" --image-url https://cam.example.com/snap.jpg --priority high
honk-me loud "Disk 91%" "/var on app-01" --action "Open Grafana=https://grafana.example.com/d/disk"

Cron, alerting only when the job fails:

0 3 * * * pg_dump app > /backup/app.sql || honk-me problem --group-key db/backup --source "$(hostname)" --title "Backup failed" --message "pg_dump exited with $?"

Send recovery only when something was actually broken (for example from a check that remembers its last state): a recovery with no open problem still opens a "recovered" episode and notifies, so a recovery after every successful run would buzz every night.

CI (GitHub Actions), with one key per run attempt so the CLI's own retries never duplicate:

- name: Notify
  if: failure()
  env:
    HONK_URL: ${{ secrets.HONK_URL }}
    HONK_KEY: ${{ secrets.HONK_KEY }}
  run: |
    honk-me problem --group-key "ci/${{ github.repository }}/${{ github.ref_name }}" \
      --title "CI failed: ${{ github.workflow }}" --message "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
      --source github-actions --category deployments \
      --idempotency-key "gh-${{ github.run_id }}-${{ github.run_attempt }}" || true

Shortcuts honk-me light|beep|loud|long|blast [TITLE] MESSAGE [flags] fix the severity and take the text as arguments. send, problem and recovery take flags only: --title, --message (- = stdin), --severity (horn or canonical name), --priority, --category, --source, --environment, --channel, --group-key, --event-type (send only), --occurred-at (RFC 3339 or now), --url, --image-url, --action TITLE=URL (repeatable, up to 3; split at the first =), --meta k=v / --meta k:=3 (repeatable), --ttl, --source-sequence, --idempotency-key, --timeout, --deadline, --retries, --json, --quiet, --dry-run. honk-me send -h lists them all.

On success it prints the message ID (or JSON with --json). Exit codes:

Code Meaning
0 accepted (or a duplicate of an accepted event)
1 unexpected answer (wrong HONK_URL, redirect, malformed reply)
2 usage error, or HONK_URL / HONK_KEY missing
3 invalid message: fix the flags
4 authentication: invalid/revoked key, urgent not allowed, suspended project
5 quota or rate limit (429); stderr shows Retry-After
6 idempotency conflict (409): same key, different payload
7 temporary failure (network, timeout, 5xx) after retries: rerun with the same --idempotency-key

Add || true where a notification failure must not fail the script.

Development

go test -race ./...                                   # unit tests (httptest) + CLI tests
HONK_URL=… HONK_KEY=… go test -run Integration ./...  # against a real server (use a test project's key)

The version lives in honk.Version (also the User-Agent and honk-me version). Releases: push a tag vX.Y.Z matching it; the release workflow attaches the CLI binaries and the Go proxy serves the module (see CHANGELOG.md).

MIT License.

Documentation

Overview

Package honk is the official Go client for Honk (https://honk-me.app), the inbox that turns events from apps, scripts, cron jobs and CI into calm, grouped push notifications.

c, err := honk.FromEnv() // HONK_URL, HONK_KEY
if err != nil { log.Fatal(err) }
_, err = c.Problem(ctx, "db/backup", "Backup failed", "pg_dump exited with 1")

Every Send carries an Idempotency-Key (a UUIDv7 unless you pass WithIdempotencyKey) that is reused on every retry, so a lost response never creates a duplicate. Only network errors, 429 and 5xx are retried, with exponential backoff, full jitter and Retry-After, within a total deadline. Errors are *Error; match them with errors.Is(err, honk.ErrQuota) and friends, or errors.As for the details.

The ingestion key belongs on the server side only. Send from a goroutine or a queue so the critical path of your service never waits for a notification.

Example
package main

import (
	"context"
	"log"

	honk "github.com/honk-me/honk-go"
)

func main() {
	c, err := honk.FromEnv() // HONK_URL, HONK_KEY
	if err != nil {
		log.Fatal(err)
	}
	ctx := context.Background()
	if _, err := c.Problem(ctx, "db/backup", "Backup failed", "pg_dump exited with 1"); err != nil {
		log.Print(err)
	}
}

Index

Examples

Constants

View Source
const (
	MaxBodyBytes      = 16 << 10
	MaxMessageBytes   = 8192
	MaxTitle          = 160
	MaxSource         = 64
	MaxEnvironment    = 32
	MaxChannel        = 64
	MaxGroupKey       = 128
	MaxURLBytes       = 2048
	MaxActions        = 3
	MaxActionTitle    = 40
	MaxMetadataKeys   = 16
	MaxMetadataString = 512
	MinTTLSeconds     = 60
	MaxTTLSeconds     = 86400
)

Limits from contracts/openapi.yaml (and the server's validator).

View Source
const NoRetries = -1

NoRetries disables retries when used as Options.Retries.

View Source
const Version = "0.2.0"

Version of this SDK, sent in the User-Agent header.

Variables

View Source
var (
	ErrValidation = errors.New("honk: invalid message")
	ErrAuth       = errors.New("honk: authentication failed")
	ErrQuota      = errors.New("honk: quota or rate limit exceeded")
	ErrConflict   = errors.New("honk: idempotency conflict")
	ErrNetwork    = errors.New("honk: network error")
	ErrTimeout    = errors.New("honk: timeout")
	ErrServer     = errors.New("honk: server error")
)

Sentinels for errors.Is: errors.Is(err, honk.ErrQuota).

View Source
var SeverityAliases = map[string]Severity{
	"light": SeverityInfo, "beep": SeveritySuccess, "loud": SeverityWarning, "long": SeverityError, "blast": SeverityCritical,
}

SeverityAliases maps the horn names to the canonical severities.

Functions

func EncodeMessage

func EncodeMessage(m Message, defaults Defaults) ([]byte, error)

EncodeMessage validates m (with defaults applied) and returns the exact JSON body Send would post. Useful for logging, dry runs and tests.

func NewIdempotencyKey

func NewIdempotencyKey() string

NewIdempotencyKey returns a new UUIDv7 (RFC 9562): 48-bit Unix milliseconds, then random bits. Store it with your job if you want to retry the same event across process restarts.

func Ptr

func Ptr[T any](v T) *T

Ptr returns a pointer to v, e.g. Message{SourceSequence: honk.Ptr[int64](42)}.

Types

type Accepted

type Accepted struct {
	// ID of the message (msg_…). The original ID when Duplicate is true.
	ID string `json:"id"`
	// Duplicate is true when this idempotency key was already accepted with the same payload
	// in the last 24 hours.
	Duplicate bool `json:"duplicate"`
	// ReceivedAt is when the server accepted it (the first time, for a duplicate).
	ReceivedAt time.Time `json:"received_at"`
}

Accepted is the 202 answer: the message is durably stored (not necessarily pushed yet).

type Action added in v0.2.0

type Action struct {
	// Title is 1–40 characters (trimmed), one line, shown as sent: "Reply", "Call Emily".
	Title string `json:"title"`
	// URL is https:// (no credentials), mailto: with one address (?subject=…&body=…
	// percent-encoded, no other keys), tel: or sms: with a number (sms: also ?body=…), at most
	// 2048 bytes without spaces. Other schemes are refused.
	URL string `json:"url"`
}

Action is a button on a message. Honk never opens or fetches the URL; the app opens it when the user taps the button.

Example

Buttons on a customer request: reply by email or call back, straight from the notification.

package main

import (
	"context"
	"log"
	"net/url"

	honk "github.com/honk-me/honk-go"
)

func main() {
	c, _ := honk.FromEnv()
	_, err := c.Send(context.Background(), honk.Message{
		Title:    "New request: online shop quote",
		Message:  "Emily Carter (Acme) asked for a quote: online shop, 40 products",
		Category: honk.CategoryCustomers,
		GroupKey: "requests/4812",
		Actions: []honk.Action{
			{Title: "Reply", URL: "mailto:emily@example.com?subject=" + url.PathEscape("Your quote")},
			{Title: "Call Emily", URL: "tel:+15550134"},
		},
	}, honk.WithIdempotencyKey("request-4812"))
	if err != nil {
		log.Printf("honk: %v", err)
	}
}

type Backoff

type Backoff struct {
	Base time.Duration
	Max  time.Duration
}

Backoff configures the delay between retries.

type Category

type Category string

Category from taxonomy v1.

const (
	CategoryInfrastructure Category = "infrastructure"
	CategorySecurity       Category = "security"
	CategoryBackups        Category = "backups"
	CategoryDeployments    Category = "deployments"
	CategoryPayments       Category = "payments"
	CategoryCustomers      Category = "customers"
	CategorySales          Category = "sales"
	CategoryAutomation     Category = "automation"
	CategoryPersonal       Category = "personal"
	CategoryOther          Category = "other"
)

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client sends messages to one Honk project. It is safe for concurrent use; create one and reuse it so connections are kept alive.

func FromEnv

func FromEnv() (*Client, error)

FromEnv is New(OptionsFromEnv()).

func New

func New(o Options) (*Client, error)

New validates the options and returns a Client.

func (*Client) Beep

func (c *Client) Beep(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Beep sends a beep-beep (severity success).

func (*Client) Blast

func (c *Client) Blast(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Blast sends a blast (severity critical; pushes at least as high priority).

func (*Client) Critical

func (c *Client) Critical(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Critical is a synonym of Blast.

func (*Client) Error

func (c *Client) Error(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Error is a synonym of Long.

func (*Client) Info

func (c *Client) Info(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Info is a synonym of Light.

func (*Client) Light

func (c *Client) Light(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Light sends a light honk (severity info). An empty title lets the server derive one.

func (*Client) Long

func (c *Client) Long(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Long sends a long honk (severity error; pushes at least as high priority).

func (*Client) Loud

func (c *Client) Loud(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Loud sends a loud honk (severity warning).

func (*Client) Problem

func (c *Client) Problem(ctx context.Context, groupKey, title, message string, opts ...Option) (*Accepted, error)

Problem reports a problem for groupKey (opens or continues its incident). Severity defaults to Long (error); options may override it.

func (*Client) Recovery

func (c *Client) Recovery(ctx context.Context, groupKey, title, message string, opts ...Option) (*Accepted, error)

Recovery reports that groupKey recovered (closes its open incident). Severity defaults to Beep (success); options may override it.

func (*Client) Send

func (c *Client) Send(ctx context.Context, m Message, opts ...Option) (*Accepted, error)

Send posts one event and returns once Honk has durably stored it (202), which does not mean a push was delivered. Network errors, 429 and 5xx are retried with the same Idempotency-Key until Retries or the deadline runs out. Errors are *Error.

Example (CustomerRequest)

A customer request: one group per request, pushed right away, linked to the admin page.

package main

import (
	"context"
	"log"

	honk "github.com/honk-me/honk-go"
)

func main() {
	c, _ := honk.FromEnv()
	go func() { // never block the request path on a notification
		_, err := c.Send(context.Background(), honk.Message{
			Title:    "New request: online shop quote",
			Message:  "Ana Pop (Acme) asked for a quote: 40 products, delivery in November.",
			Priority: honk.PriorityHigh,
			Category: honk.CategoryCustomers,
			Channel:  "requests",
			GroupKey: "requests/4812",
			URL:      "https://shop.example.com/admin/requests/4812",
			Metadata: map[string]any{"request_id": "4812"},
		}, honk.WithIdempotencyKey("request-4812"))
		if err != nil {
			log.Printf("honk: %v", err)
		}
	}()
}

func (*Client) Success

func (c *Client) Success(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Success is a synonym of Beep.

func (*Client) Warning

func (c *Client) Warning(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)

Warning is a synonym of Loud.

type Defaults

type Defaults struct {
	Source      string
	Environment string
	Channel     string
}

Defaults are applied to every message that leaves these fields empty.

type Error

type Error struct {
	Kind Kind
	// Status is the HTTP status (0 when no answer was received).
	Status int
	// Code is the API error code (invalid_key, quota_exceeded, …), or network_error/timeout.
	Code    string
	Message string
	// Fields lists invalid fields for KindValidation.
	Fields []FieldError
	// Local is true when the SDK rejected the message before sending anything.
	Local     bool
	RequestID string
	// IdempotencyKey that was used. Retry later with the same key to stay duplicate-free.
	IdempotencyKey string
	// Attempts is the number of HTTP attempts made.
	Attempts int
	// RetryAfter is the server's Retry-After, when present.
	RetryAfter time.Duration
	// Err is the underlying transport or context error, if any.
	Err error
}

Error is returned by Send and the helpers. Use errors.As to read the details, or errors.Is with the Err* sentinels.

Example
package main

import (
	"context"
	"errors"
	"log"
	"time"

	honk "github.com/honk-me/honk-go"
)

func main() {
	c, _ := honk.FromEnv()
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	_, 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("fix the message: %v", err) // he.Fields lists every invalid field
	case errors.Is(err, honk.ErrQuota) && errors.As(err, &he):
		log.Printf("over quota, retry in %s", he.RetryAfter)
	case errors.As(err, &he) && he.Retryable():
		log.Printf("Honk unreachable; retry later with key %s", he.IdempotencyKey)
	default:
		log.Print(err)
	}
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches the Err* sentinels by Kind (ErrNetwork also matches timeouts).

func (*Error) Retryable

func (e *Error) Retryable() bool

Retryable reports whether sending the same event again later (with the same idempotency key) may succeed.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type EventType

type EventType string

EventType: a problem opens an incident for its group, a recovery closes it. Both need a GroupKey.

const (
	EventTypeEvent    EventType = "event"
	EventTypeProblem  EventType = "problem"
	EventTypeRecovery EventType = "recovery"
)

type FieldError

type FieldError struct {
	// Field is the wire name: group_key, metadata.region, Idempotency-Key, body, …
	Field string `json:"field"`
	// Code: required, too_long, too_short, invalid_enum, invalid_format, out_of_range,
	// not_allowed, invalid_utf8, requires_group_key.
	Code    string `json:"code"`
	Message string `json:"message,omitempty"`
}

FieldError is one invalid field, from the server (error.fields[]) or local validation.

type Kind

type Kind string

Kind classifies an *Error.

const (
	// KindValidation: the message is invalid (rejected locally, or 400/413/415/422). Fix it.
	KindValidation Kind = "validation"
	// KindAuth: 401/403 — invalid or revoked key, priority_not_allowed (urgent without
	// allow_urgent), project_suspended, workspace_suspended.
	KindAuth Kind = "auth"
	// KindQuota: 429 quota_exceeded (daily messages_per_day, until UTC midnight) or
	// rate_limited, after retries. See RetryAfter.
	KindQuota Kind = "quota"
	// KindConflict: 409 idempotency_conflict — the key was already used with a different
	// payload in the last 24 hours.
	KindConflict Kind = "conflict"
	// KindNetwork: Honk could not be reached on any attempt before the deadline.
	KindNetwork Kind = "network"
	// KindTimeout: attempts timed out (or the context deadline passed). The event may or may
	// not have been stored; retrying with the same idempotency key is safe.
	KindTimeout Kind = "timeout"
	// KindCanceled: the context was canceled.
	KindCanceled Kind = "canceled"
	// KindServer: 5xx on every attempt before the deadline.
	KindServer Kind = "server"
	// KindHTTP: any other unexpected answer (404 wrong URL, a redirect, a malformed 202).
	KindHTTP Kind = "http"
)

type Message

type Message struct {
	// Title is one line, at most 160 characters. Defaults to the first line of Message.
	Title string
	// Message is plain text, 1–8192 bytes of UTF-8. Line breaks and tabs are allowed.
	Message string
	// Severity: a horn name (Light, Beep, Loud, Long, Blast) or canonical value; any case.
	// Sent canonical. Default light (info).
	Severity Severity
	Priority Priority
	Category Category
	// Source (≤ 64), Environment (≤ 32) and Channel (≤ 64) default to the client's Defaults.
	Source      string
	Environment string
	Channel     string
	// GroupKey (≤ 128) relates occurrences: messages with the same key (per environment,
	// source and channel) form one group, so the first one pushes and repeats update it calmly.
	// Use one key per customer request ("requests/<id>"), a shared key only for repeats of the
	// same problem.
	GroupKey  string
	EventType EventType
	// OccurredAt is when it happened at the source (informational). Zero means unset.
	OccurredAt time.Time
	// URL is an https link shown as "Open link" (no credentials, ≤ 2048 bytes).
	URL string
	// ImageURL is an https image the server fetches after ingestion (no credentials or
	// fragment, ≤ 2048 bytes).
	ImageURL string
	// Actions are up to 3 buttons, in display order (the first is the primary). Empty means
	// none.
	Actions []Action
	// Metadata has at most 16 keys matching [A-Za-z0-9_.-]{1,64}; values are strings
	// (≤ 512 characters), numbers or booleans.
	Metadata map[string]any
	// TTLSeconds is the push lifetime, 60–86400. Zero means the server default (3600).
	TTLSeconds int
	// SourceSequence is a monotonic counter per source stream (0 … 2^53-1) so a delayed
	// recovery can never close a newer problem. Requires GroupKey. Use honk.Ptr(n).
	SourceSequence *int64
}

Message is one event for POST /v1/messages. Only Message is required; zero values are omitted, so the server defaults apply (severity info, priority normal, source "api", environment "default", channel "general", event type event, TTL 3600 s).

type Option

type Option func(*sendCall)

Option adjusts one Send (or helper) call: the idempotency key, or message fields.

func WithActions added in v0.2.0

func WithActions(actions ...Action) Option

WithActions appends buttons to the message (at most 3 in all, in display order).

func WithCategory

func WithCategory(cat Category) Option

WithCategory sets the category.

func WithChannel

func WithChannel(ch string) Option

WithChannel sets the channel (≤ 64 characters).

func WithEnvironment

func WithEnvironment(e string) Option

WithEnvironment sets the environment (≤ 32 characters).

func WithGroupKey

func WithGroupKey(k string) Option

WithGroupKey sets the group key, e.g. "requests/4812".

func WithIdempotencyKey

func WithIdempotencyKey(key string) Option

WithIdempotencyKey sets a stable key for this event (1–128 printable ASCII characters), e.g. "request-4812". It is reused on every retry. Default: a new UUIDv7 per call.

func WithImageURL

func WithImageURL(u string) Option

WithImageURL sets an https image the server fetches after ingestion.

func WithMetadata

func WithMetadata(md map[string]any) Option

WithMetadata merges keys into the message metadata.

func WithOccurredAt

func WithOccurredAt(t time.Time) Option

WithOccurredAt sets when the event happened at the source.

func WithPriority

func WithPriority(p Priority) Option

WithPriority sets the declared priority (urgent needs a key with allow_urgent).

func WithSeverity

func WithSeverity(s Severity) Option

WithSeverity sets the severity: a horn name (honk.Loud) or canonical value, any case (helpers like Problem default it; Light…Blast fix it).

func WithSource

func WithSource(s string) Option

WithSource sets the source (≤ 64 characters).

func WithSourceSequence

func WithSourceSequence(n int64) Option

WithSourceSequence sets the monotonic sequence for problem/recovery ordering.

func WithTTLSeconds

func WithTTLSeconds(s int) Option

WithTTLSeconds sets the push lifetime (60–86400 seconds).

func WithURL

func WithURL(u string) Option

WithURL sets the https link shown as "Open link".

type Options

type Options struct {
	// URL is the base address of your Honk server, e.g. https://honk.example.com.
	URL string
	// Key is a project ingestion key (honk_…). Keep it server-side.
	Key string
	// Timeout of one HTTP attempt. Default 5s.
	Timeout time.Duration
	// Retries after the first attempt (network errors, 429 and 5xx only). 0 means the
	// default (4); use NoRetries to disable.
	Retries int
	// Deadline is the total time budget of one Send, waits included. Default 30s. A shorter
	// context deadline wins.
	Deadline time.Duration
	// Defaults fill Source/Environment/Channel when a message leaves them empty.
	Defaults Defaults
	// SkipValidation sends messages without local checks (the server always validates).
	SkipValidation bool
	// Backoff between retries: attempt n waits rand(0, min(Max, Base·2ⁿ)) (full jitter), or
	// the server's Retry-After when longer. Default 500ms / 8s.
	Backoff Backoff
	// HTTPClient to use (proxies, instrumentation). Its CheckRedirect is overridden so
	// redirects are reported instead of followed. Default: a keep-alive client.
	HTTPClient *http.Client
	// UserAgent is appended to the User-Agent header.
	UserAgent string
}

Options configure a Client. URL and Key are required.

func OptionsFromEnv

func OptionsFromEnv() Options

OptionsFromEnv reads HONK_URL, HONK_KEY and the optional HONK_SOURCE, HONK_ENVIRONMENT and HONK_CHANNEL defaults.

type Priority

type Priority string

Priority declared by the source. Urgent needs an ingestion key with allow_urgent.

const (
	PriorityLow    Priority = "low"
	PriorityNormal Priority = "normal"
	PriorityHigh   Priority = "high"
	PriorityUrgent Priority = "urgent"
)

type Severity

type Severity string

Severity of a message, lowest to highest. Error and critical raise the effective priority to at least high. Every severity has a horn name on the Honk scale (Light, Beep, Loud, Long, Blast); those constants are aliases of the canonical values.

const (
	Light Severity = SeverityInfo     // light honk
	Beep  Severity = SeveritySuccess  // beep-beep
	Loud  Severity = SeverityWarning  // loud honk
	Long  Severity = SeverityError    // long honk
	Blast Severity = SeverityCritical // blast
)

The Honk scale: horn names, aliases of the canonical severities (honk.Loud == honk.SeverityWarning).

const (
	SeverityInfo     Severity = "info"
	SeveritySuccess  Severity = "success"
	SeverityWarning  Severity = "warning"
	SeverityError    Severity = "error"
	SeverityCritical Severity = "critical"
)

Canonical severities, as stored and returned by the server.

func ParseSeverity

func ParseSeverity(s string) (Severity, bool)

ParseSeverity returns the canonical severity for a canonical value or horn alias, case-insensitively: ParseSeverity("LOUD") == SeverityWarning.

Directories

Path Synopsis
cmd
honk-me command
Command honk-me sends one event to Honk from a shell script, cron job or CI step.
Command honk-me sends one event to Honk from a shell script, cron job or CI step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL