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)
}
}
Output:
Index ¶
- Constants
- Variables
- func EncodeMessage(m Message, defaults Defaults) ([]byte, error)
- func NewIdempotencyKey() string
- func Ptr[T any](v T) *T
- type Accepted
- type Action
- type Backoff
- type Category
- type Client
- func (c *Client) Beep(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Blast(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Critical(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Error(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Info(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Light(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Long(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Loud(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Problem(ctx context.Context, groupKey, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Recovery(ctx context.Context, groupKey, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Send(ctx context.Context, m Message, opts ...Option) (*Accepted, error)
- func (c *Client) Success(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- func (c *Client) Warning(ctx context.Context, title, message string, opts ...Option) (*Accepted, error)
- type Defaults
- type Error
- type EventType
- type FieldError
- type Kind
- type Message
- type Option
- func WithActions(actions ...Action) Option
- func WithCategory(cat Category) Option
- func WithChannel(ch string) Option
- func WithEnvironment(e string) Option
- func WithGroupKey(k string) Option
- func WithIdempotencyKey(key string) Option
- func WithImageURL(u string) Option
- func WithMetadata(md map[string]any) Option
- func WithOccurredAt(t time.Time) Option
- func WithPriority(p Priority) Option
- func WithSeverity(s Severity) Option
- func WithSource(s string) Option
- func WithSourceSequence(n int64) Option
- func WithTTLSeconds(s int) Option
- func WithURL(u string) Option
- type Options
- type Priority
- type Severity
Examples ¶
Constants ¶
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).
const NoRetries = -1
NoRetries disables retries when used as Options.Retries.
const Version = "0.2.0"
Version of this SDK, sent in the User-Agent header.
Variables ¶
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).
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 ¶
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.
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)
}
}
Output:
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 (*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 ¶
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)
}
}()
}
Output:
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)
}
}
Output:
type EventType ¶
type EventType string
EventType: a problem opens an incident for its group, a recovery closes it. Both need a GroupKey.
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
WithActions appends buttons to the message (at most 3 in all, in display order).
func WithChannel ¶
WithChannel sets the channel (≤ 64 characters).
func WithEnvironment ¶
WithEnvironment sets the environment (≤ 32 characters).
func WithGroupKey ¶
WithGroupKey sets the group key, e.g. "requests/4812".
func WithIdempotencyKey ¶
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 ¶
WithImageURL sets an https image the server fetches after ingestion.
func WithMetadata ¶
WithMetadata merges keys into the message metadata.
func WithOccurredAt ¶
WithOccurredAt sets when the event happened at the source.
func WithPriority ¶
WithPriority sets the declared priority (urgent needs a key with allow_urgent).
func WithSeverity ¶
WithSeverity sets the severity: a horn name (honk.Loud) or canonical value, any case (helpers like Problem default it; Light…Blast fix it).
func WithSourceSequence ¶
WithSourceSequence sets the monotonic sequence for problem/recovery ordering.
func WithTTLSeconds ¶
WithTTLSeconds sets the push lifetime (60–86400 seconds).
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.
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 ¶
ParseSeverity returns the canonical severity for a canonical value or horn alias, case-insensitively: ParseSeverity("LOUD") == SeverityWarning.