Documentation
¶
Overview ¶
Package wardenpoint sends alerts and heartbeats from long-running services and devices to a WardenPoint generic webhook integration.
The client is built for services that must keep working no matter what WardenPoint, the network or the client itself is doing:
- Fire and Resolve never block: they validate the alert, put an event on a bounded in-memory queue and return. All network I/O happens in Run.
- An event is never dropped silently. It is either delivered, rejected by WardenPoint (logged at ERROR with the HTTP status), or evicted by queue overflow (logged at WARN). Every drop is also counted in Metrics.
- The heartbeat runs in its own goroutine and does not share the alert queue: an alert stuck in retries or rejected by the server never stops the heartbeat.
- The integration secret never appears in logs, errors, Status or metrics.
Repeated alerts ¶
Each call to Fire is a new event of the alert identified by Alert.ID. WardenPoint groups events by that ID and counts them towards the integration's accumulation threshold, so firing the same alert in a loop does not create new alerts, but it does move the threshold. The client does not deduplicate on its own: call Fire when the condition is detected, not on every check while it persists.
A request whose response is lost (a timeout after the server accepted it) is retried, and WardenPoint then counts the event twice. Until the inbound API accepts an idempotency key this is the price of never losing an alert.
Index ¶
- Constants
- type Alert
- type AlertStatus
- type Client
- func (c *Client) Fire(a Alert) error
- func (c *Client) GoString() string
- func (c *Client) LogValue() slog.Value
- func (c *Client) Resolve(id string) error
- func (c *Client) ResolveAlert(a Alert) error
- func (c *Client) Run(ctx context.Context) error
- func (c *Client) Status() Status
- func (c *Client) String() string
- type Clock
- type Config
- type ConfigError
- type Delivery
- type DropReason
- type Event
- type HeartbeatState
- type HeartbeatStatus
- type Metrics
- type SentinelError
- type Severity
- type Status
- type Store
- type Timer
Constants ¶
const ( MaxIDLength = 255 MaxTitleLength = 255 MaxDescriptionLength = 5000 MaxSourceURLLength = 1024 )
Limits of the generic webhook. Fire truncates title and description to them instead of letting WardenPoint reject the whole alert with 422.
const ( DefaultQueueSize = 50 DefaultRequestTimeout = 15 * time.Second DefaultLateThreshold = time.Minute // MinHeartbeatInterval keeps a misconfigured interval from spending the // integration's rate limit (60 requests per minute) on heartbeats alone. MinHeartbeatInterval = 5 * time.Second )
Defaults applied by New when the corresponding Config field is zero.
const ( HeartbeatAlertID = "wardenpoint-heartbeat" HeartbeatLabelKey = "kind" HeartbeatLabelValue = "heartbeat" )
The heartbeat is sent as an alert with a fixed body. WardenPoint's heartbeat health check only needs something to arrive; the label lets a routing rule suppress the alert itself (see the README).
const Version = "0.1.0"
Version is sent in the User-Agent header.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Alert ¶
type Alert struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Severity Severity `json:"severity,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
SourceURL string `json:"source_url,omitempty"`
}
Alert is one condition of the service. ID identifies it across events: Fire and Resolve with the same ID refer to the same WardenPoint alert.
type AlertStatus ¶
type AlertStatus string
AlertStatus is the state an event reports for its alert.
const ( StatusFiring AlertStatus = "firing" StatusResolved AlertStatus = "resolved" )
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client delivers alerts and heartbeats. Create it with New, start delivery with Run, raise alerts with Fire and Resolve from any goroutine.
func New ¶
New validates cfg and returns a client. It starts no goroutines and makes no requests; it may log a WARN about the configuration.
With an empty IntegrationURL or Secret it returns an error, or, when cfg.AllowDisabled is set, a disabled client whose methods do nothing.
func (*Client) Fire ¶
Fire queues a firing event of the alert and returns at once, without I/O. It only fails on an invalid alert. Each call is a new event of the alert a.ID; see the package documentation on repeated alerts.
func (*Client) Resolve ¶
Resolve queues a resolved event for the alert with the given ID. It resends the title, severity, labels and source URL of the last Fire with that ID: WardenPoint overwrites them with each event, and a bare resolve would rename the alert. The description is not kept. For an ID this client has not fired, e.g. after a restart, the title is the ID. Use ResolveAlert to set the fields.
func (*Client) ResolveAlert ¶
ResolveAlert queues a resolved event with exactly the given fields.
func (*Client) Run ¶
Run delivers queued events and sends heartbeats until ctx is cancelled. It returns nil after ctx is done, once every goroutine it started has exited, or ErrAlreadyRunning. Events left undelivered stay queued (and in the Store) for the next Run.
type Clock ¶
Clock is the client's source of time: heartbeat ticks, retry delays, event timestamps and HMAC timestamps. HTTP timeouts always use real time.
type Config ¶
type Config struct {
// IntegrationURL is the full inbound URL of a generic webhook integration:
// https://wardenpoint.com/api/v1/integrations/alerts/<integration-uuid>.
IntegrationURL string
// Secret is the integration secret. It is sent as a Bearer token, or used
// only as the HMAC key when HMAC is true.
Secret string
// HeartbeatURL is where heartbeats go. Empty means IntegrationURL. A
// dedicated integration is recommended, see the README.
HeartbeatURL string
// HeartbeatSecret is the secret of the HeartbeatURL integration. Empty
// means Secret.
HeartbeatSecret string
// HeartbeatInterval is the period between heartbeats. Zero disables the
// heartbeat. It must be shorter than the integration's heartbeat grace
// window (180 s by default), ideally a third of it.
HeartbeatInterval time.Duration
// HeartbeatCheck, when set, runs before every heartbeat. If it returns an
// error the heartbeat is not sent, so WardenPoint pages the integration's
// owner group once the grace window expires: the service is up but not
// healthy. Leave it nil to report liveness only. It gets RequestTimeout to
// finish; a panic counts as a failed check.
HeartbeatCheck func(ctx context.Context) error
// QueueSize bounds the number of undelivered events. On overflow the
// oldest event that is not being sent right now is evicted.
QueueSize int
// RequestTimeout bounds one HTTP request, from connect to the end of the
// response body.
RequestTimeout time.Duration
// LateThreshold: an event delivered later than this after it was created
// gets the original time appended to its description.
LateThreshold time.Duration
// HTTPClient is used for all requests. Nil means a client tuned for small
// devices. Redirects are never followed, even with a custom client: its
// CheckRedirect is replaced on a copy.
HTTPClient *http.Client
// Logger receives the client's logs. Nil means slog.Default().
Logger *slog.Logger
// Metrics receives counters and gauges. Nil disables metrics.
Metrics Metrics
// Store persists the queue across restarts. Nil keeps it in memory only.
Store Store
// Clock is the time source for timers and timestamps. Nil means the
// system clock; tests pass wardenpointtest.Clock.
Clock Clock
// HMAC signs every request with the integration's HMAC scheme instead of
// sending the secret. It must match the integration's "Require HMAC"
// setting and applies to the heartbeat integration as well. Signatures
// carry a timestamp, so the device clock must be synchronised (NTP).
HMAC bool
// AllowDisabled makes New return a working no-op client instead of an
// error when IntegrationURL or Secret is empty, e.g. in a development
// environment without credentials. The client logs one WARN.
AllowDisabled bool
}
Config configures a Client. Only IntegrationURL and Secret are required.
Config implements fmt.Stringer, fmt.GoStringer and slog.LogValuer with the secrets masked, so logging or printing it does not leak them.
type ConfigError ¶
type ConfigError string
ConfigError is returned by New for an unusable Config. Its message never contains a URL or a secret.
func (ConfigError) Error ¶
func (e ConfigError) Error() string
type Delivery ¶
type Delivery struct {
AlertID string `json:"-"`
Status AlertStatus `json:"-"`
HTTPStatus int `json:"-"`
At time.Time `json:"-"`
AlertUUID string `json:"alert_uuid"`
EventUUID string `json:"event_uuid"`
// Action is the accumulation action taken: suppress, message or
// escalate.
Action string `json:"action"`
NotificationQueued bool `json:"notification_queued"`
// QueueSkipReason says why nobody was notified, e.g. suppressed_by_rule
// or no_group; empty when a notification was queued.
QueueSkipReason string `json:"queue_skip_reason"`
EventCount int `json:"event_count"`
ThresholdCount int `json:"threshold_count"`
}
Delivery is WardenPoint's answer to an accepted event.
type DropReason ¶
type DropReason string
DropReason says why an event left the queue without being delivered.
const ( // DropOverflow: the queue was full and the event was the oldest one not // being sent at that moment. DropOverflow DropReason = "overflow" // DropRejected: WardenPoint answered with a status that a retry cannot // fix (4xx other than 408 and 429, or a redirect). DropRejected DropReason = "rejected" )
type Event ¶
type Event struct {
// ID is unique per event and is sent as client_event_id. It also makes
// every request body unique, which HMAC replay protection relies on.
ID string `json:"id"`
Alert Alert `json:"alert"`
Status AlertStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
// Attempts counts failed delivery attempts. It is not persisted on each
// attempt, so it restarts after a reload from the Store.
Attempts int `json:"attempts,omitempty"`
}
Event is one queued delivery of an alert. It is exported for Store implementations; it marshals to JSON as is.
type HeartbeatState ¶
type HeartbeatState string
HeartbeatState is the heartbeat's last known outcome.
const ( HeartbeatDisabled HeartbeatState = "disabled" // HeartbeatUnknown: Run has not sent the first heartbeat yet. HeartbeatUnknown HeartbeatState = "unknown" HeartbeatOK HeartbeatState = "ok" // HeartbeatFailing: the last heartbeat did not reach WardenPoint. HeartbeatFailing HeartbeatState = "failing" // HeartbeatUnhealthy: HeartbeatCheck failed, the heartbeat was withheld. HeartbeatUnhealthy HeartbeatState = "unhealthy" )
type HeartbeatStatus ¶
type HeartbeatStatus struct {
State HeartbeatState
LastAttempt time.Time
LastOK time.Time
LastError string
}
HeartbeatStatus describes the heartbeat.
type Metrics ¶
type Metrics interface {
// EventDelivered counts events accepted by WardenPoint (2xx).
EventDelivered()
// EventDropped counts events that will never be delivered.
EventDropped(reason DropReason)
// EventRetried counts failed attempts that will be retried.
EventRetried()
// QueueLength reports the number of undelivered events after each change.
QueueLength(n int)
// Heartbeat reports the outcome of each heartbeat tick. ok is false when
// the request failed or HeartbeatCheck stopped it.
Heartbeat(ok bool)
}
Metrics receives the client's counters and gauges. Implementations must be safe for concurrent use and must not block: methods are called from Fire and from the delivery path, sometimes while an internal lock is held.
A Prometheus adapter is a few lines: counters for the first three methods, gauges for the last two.
type SentinelError ¶
type SentinelError string
SentinelError is a constant error value; the sentinels below are constants rather than package variables, so nothing can reassign them.
const ( // ErrAlreadyRunning is returned by Run when another Run of the same // client has not returned yet. ErrAlreadyRunning SentinelError = "wardenpoint: Run is already running" // ErrInvalidAlert wraps every validation error from Fire and Resolve. ErrInvalidAlert SentinelError = "wardenpoint: invalid alert" )
func (SentinelError) Error ¶
func (e SentinelError) Error() string
type Severity ¶
type Severity string
Severity describes the alert. It is not the notification priority: how loudly people are woken is decided by the action of the WardenPoint routing rule that matches the alert. Severity is one of the things a rule can match on.
type Status ¶
type Status struct {
Enabled bool
Running bool
QueueLength int
Delivered uint64
Dropped uint64
Retries uint64
LastAttempt time.Time
LastSuccess time.Time
// LastError describes the last failed alert delivery, secrets masked.
LastError string
LastErrorAt time.Time
// LastDelivery is WardenPoint's answer to the last delivered event.
LastDelivery *Delivery
Heartbeat HeartbeatStatus
}
Status is a snapshot of the client's state.
type Store ¶
type Store interface {
// Load returns the stored events, oldest first. It is called once, at
// the start of the first Run.
Load(ctx context.Context) ([]Event, error)
// Add stores an event. Events are added in queue order.
Add(ctx context.Context, ev Event) error
// Delete removes the event with the given Event.ID. Deleting an unknown
// ID is not an error.
Delete(ctx context.Context, eventID string) error
}
Store persists queued events, so alerts raised before a crash or a reboot are delivered after it.
The client keeps the queue in memory and mirrors it into the Store from the delivery goroutine only: Fire never waits for the Store, and methods are never called concurrently. An event raised less than a moment before the process dies may therefore exist only in memory.
A failing Store does not stop delivery: the client logs a WARN and keeps the events in memory. A failed Delete means the event can be sent again after a restart, never that it is lost.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
boiler
command
Command boiler is a complete example service: it watches a sensor, raises and resolves an alert in WardenPoint, sends a heartbeat that stops when the sensor bus is unreadable, and exposes the client status on /healthz.
|
Command boiler is a complete example service: it watches a sensor, raises and resolves an alert in WardenPoint, sends a heartbeat that stops when the sensor bus is unreadable, and exposes the client status on /healthz. |
|
Package wardenpointtest provides fakes for testing code that uses the wardenpoint client: an HTTP server that imitates the generic webhook endpoint and a manual clock.
|
Package wardenpointtest provides fakes for testing code that uses the wardenpoint client: an HTTP server that imitates the generic webhook endpoint and a manual clock. |