Documentation
¶
Overview ¶
Package relay receives bide's signed webhooks and forwards each verified notification to Slack, PagerDuty, or any other Sink. It is the library behind bide notify relay: embed Handler in your own server, or run the command.
A Handler is the receiver side of the notifier's at-least-once contract. It verifies every request with notify.Verify, so a bad, stale or replayed-with-a-changed-ID delivery is refused with 401; it answers 2xx only after every sink took the notification, and 502 when one did not, which makes bide retry. Because bide may deliver an event more than once (a lost reply, a restart, a retry after a 502), it remembers the delivery IDs it has forwarded, in a bounded cache, and acknowledges a repeat without forwarding it again. With several sinks it remembers which of them already took the event, so a retry after a partial failure reaches only the sink that failed.
The relay never forwards error text or tool arguments, even if bide was told to send them: a notification that reaches a chat room or a pager has left the access control the viewer applies.
Index ¶
- Constants
- func ClearsID(p notify.Payload) string
- func DedupKey(p notify.Payload) string
- func DefaultResolveTarget(p notify.Payload) (string, bool)
- func DefaultSeverity(p notify.Payload) string
- func KindTitle(kind string) string
- func RejectionClass(err error) string
- type Config
- type Handler
- type PagerDuty
- type PagerDutyConfig
- type PagerDutyEvent
- type PagerDutyLink
- type PagerDutyPayload
- type Sink
- type Slack
- type SlackConfig
- type SlackMsg
- type StatusError
Constants ¶
const ( // DefaultAttempts is how many requests one Send may make. DefaultAttempts = 3 // DefaultRequestTimeout bounds each request. DefaultRequestTimeout = 3 * time.Second // DefaultBackoff is the wait before the second request; it doubles for each further one. DefaultBackoff = 250 * time.Millisecond )
Defaults of an outbound call, shared by the Slack and PagerDuty sinks.
const ( // DefaultDedupeSize is how many delivery IDs a Handler remembers. DefaultDedupeSize = 10000 // DefaultForwardTimeout bounds one delivery's forwarding. It is below the notifier's own // per-request timeout (10 s), so a slow third party makes the relay answer 502 before bide // gives up on the request. DefaultForwardTimeout = 8 * time.Second // DefaultStateFlushDelay is how long a state file write may be deferred and coalesced. DefaultStateFlushDelay = time.Second )
Defaults of a Handler.
const DefaultPagerDutyURL = "https://events.pagerduty.com/v2/enqueue"
DefaultPagerDutyURL is the Events API v2 endpoint. An EU-region account uses https://events.eu.pagerduty.com/v2/enqueue.
const ResolveKind = "resolved"
ResolveKind is the notification kind that clears an incident: the notifier's "resolved" kind (agent.NotificationResolved). Its payload has a top-level clears object whose id is the ID of the event it ends, which is this sink's dedup key for the incident.
Variables ¶
This section is empty.
Functions ¶
func ClearsID ¶
ClearsID returns the ID of the event a clear notification ends, or "" if p is not a clear. It is the payload's clears.id, which is the "id" the ended event's own notifications carried, so it is also the dedup key of the incident that event opened.
func DedupKey ¶
DedupKey is the PagerDuty dedup_key of the incident an event raises: the event's notification ID, the SHA-256 of its key. It is stable across every delivery of the event, so PagerDuty collapses redeliveries into one incident, and it is what a clear names to resolve it. Another key scheme is possible by wrapping the sink, but a resolve must use the same one.
func DefaultResolveTarget ¶
DefaultResolveTarget treats a notification of kind ResolveKind as a resolve of the incident whose dedup key is the ID of the event it clears (ClearsID), and everything else as a trigger. A resolved notification with no clears detail names no incident: the sink sends nothing for it.
func DefaultSeverity ¶
DefaultSeverity is the severity of the incident a notification opens: a halt on an unknown outcome, an overdue notification and an escalation are critical (a missed alert is expensive there), an abort and a failing recovery are errors, and an approval waiting is a warning.
func KindTitle ¶
KindTitle is the short heading for a notification kind; a kind it does not know is shown as it is with spaces for underscores.
func RejectionClass ¶
RejectionClass names which check a failed notify.Verify failed, for a log line. The sender is told only "unauthorized".
Types ¶
type Config ¶
type Config struct {
// Secrets are the signing secrets to verify with: the current one, and the previous one during a
// rotation. Required.
Secrets []string
// Tolerance is how far a request's timestamp may be from the relay's clock; zero means
// notify.DefaultTolerance.
Tolerance time.Duration
// Sinks are where a verified notification goes. At least one is required.
Sinks []Sink
// DedupeSize bounds the delivery IDs remembered; zero means DefaultDedupeSize. When the cache is
// full the least recently seen ID is forgotten, and an old event redelivered after that is
// forwarded again: the bound trades memory for a rare repeat, never for a lost notification.
DedupeSize int
// ClearsID says whether p is a clear and the ID of the event it ends ("" when it is not); nil
// means ClearsID. The relay remembers the IDs it saw cleared (as many as DedupeSize) and drops a
// later notification of such an event instead of forwarding it: a trigger that arrives after its
// clear (the event was cleared while its request failed, or its reply was lost) would reopen an
// incident that is over.
ClearsID func(notify.Payload) string
// StatePath, if set, is a file the Handler keeps its memory in, so the delivery IDs it forwarded
// and the event IDs it saw cleared survive a restart: without it a trigger that arrives after a
// restart, for an event cleared before it, is forwarded and reopens the incident, and a
// redelivery is forwarded again. The file is written atomically after each change (bounded by
// DedupeSize, like the in-memory sets), holds IDs and sink names only, and is read at New: a
// missing file is a first start, an unreadable or invalid one is an error. If a later write
// fails the Handler logs it and carries on, with its memory in the process as without a file.
// StateFlushDelay is how long a change to the forwarded-delivery set waits before the file is
// rewritten, so a burst of events costs one write, not one each (the whole file is rewritten and
// synced, which grows with DedupeSize). Zero means DefaultStateFlushDelay; negative writes through
// on every change. A clear is never delayed: its ID is written at once, because a late trigger
// after a crash is the failure the file exists to prevent. Close writes what is pending. A crash
// (not Close) can lose the forwarded-delivery entries of the last StateFlushDelay, so a redelivery
// of those events is forwarded once more: a repeat, never a lost notification.
StateFlushDelay time.Duration
// The Handler holds an exclusive operating-system lock on StatePath+".lock" until Close or the
// process ends (a crash releases it), and New fails if another relay holds it: two relays
// sharing one file would overwrite each other's memory.
StatePath string
// ForwardTimeout bounds the forwarding of one delivery; zero means DefaultForwardTimeout. Forwarding
// continues after the sender gives up, so a retry finds it done.
ForwardTimeout time.Duration
// Logf, if set, receives one line per rejection, duplicate and failure. Lines hold delivery IDs,
// sink names and error classes; they never hold a secret, a URL or a payload.
Logf func(format string, args ...any)
}
Config configures a Handler.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
A Handler is an http.Handler for bide's webhook requests. Build one with New. It is safe for concurrent use.
func (*Handler) Close ¶
Close writes any pending state and releases the state file's lock, so another relay may use the file. A Handler without a StatePath has nothing to do. The lock is also released when the process exits.
func (*Handler) ServeHTTP ¶
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP verifies the request and forwards it.
401 the signature, timestamp or delivery ID did not verify (one answer for all of them) 400 verified, but not a bide.notification.v1 document 405 not a POST 502 a sink did not take the notification: bide retries 503 the same delivery is being forwarded by another request: bide retries 204 every sink has the notification (including a repeat that was already forwarded)
type PagerDuty ¶
type PagerDuty struct {
// contains filtered or unexported fields
}
PagerDuty is a Sink that triggers and resolves PagerDuty incidents through the Events API v2.
func NewPagerDuty ¶
func NewPagerDuty(c PagerDutyConfig) (*PagerDuty, error)
NewPagerDuty returns a PagerDuty sink.
type PagerDutyConfig ¶
type PagerDutyConfig struct {
// RoutingKey is the integration key of an Events API v2 integration. It is a credential: it
// is never put in an error or a log line. Required.
RoutingKey string
// URL overrides DefaultPagerDutyURL (the EU endpoint, or a test server).
URL string
// Source is the event's source field; empty means "bide".
Source string
// Severity, if set, picks the severity ("critical", "error", "warning" or "info") of a trigger;
// nil means DefaultSeverity.
Severity func(notify.Payload) string
// ResolveTarget, if set, says whether p is a clear that ends an incident, and the dedup key of the
// incident it ends. nil means DefaultResolveTarget. See ResolveKind.
ResolveTarget func(notify.Payload) (dedupKey string, ok bool)
// HTTPClient, Attempts, Timeout and Backoff tune the outbound call, as for SlackConfig.
HTTPClient *http.Client
Attempts int
Timeout time.Duration
Backoff time.Duration
}
PagerDutyConfig configures a PagerDuty sink.
type PagerDutyEvent ¶
type PagerDutyEvent struct {
RoutingKey string `json:"routing_key"`
EventAction string `json:"event_action"`
DedupKey string `json:"dedup_key"`
Payload *PagerDutyPayload `json:"payload,omitempty"`
Links []PagerDutyLink `json:"links,omitempty"`
}
PagerDutyEvent is an Events API v2 request body.
func PagerDutyResolve ¶
func PagerDutyResolve(routingKey, dedupKey string) PagerDutyEvent
PagerDutyResolve builds the resolve of the incident with dedupKey.
func PagerDutyTrigger ¶
func PagerDutyTrigger(routingKey, source, severity string, p notify.Payload) PagerDutyEvent
PagerDutyTrigger builds the trigger for p: the dedup key is DedupKey(p), the summary is the generated one, and the details hold the structured fields (run, kind, policy, approvers, deadline, view command). It never carries error text or tool arguments.
type PagerDutyLink ¶
PagerDutyLink is a link on an incident.
type PagerDutyPayload ¶
type PagerDutyPayload struct {
Summary string `json:"summary"`
Source string `json:"source"`
Severity string `json:"severity"`
Timestamp string `json:"timestamp,omitempty"`
Component string `json:"component,omitempty"`
Group string `json:"group,omitempty"`
Class string `json:"class,omitempty"`
CustomDetails map[string]any `json:"custom_details,omitempty"`
}
PagerDutyPayload is the payload of a trigger.
type Sink ¶
A Sink takes a verified notification to somewhere else. Send returns nil only once the destination accepted it, and may be called again for the same notification after an error, so a destination that can dedupe (PagerDuty can) should be given a key derived from the payload. Name identifies the sink in the dedupe cache and in logs: it must be unique in a Handler.
type Slack ¶
type Slack struct {
// contains filtered or unexported fields
}
Slack is a Sink that posts a Block Kit message to a Slack incoming webhook.
func NewSlack ¶
func NewSlack(c SlackConfig) (*Slack, error)
NewSlack returns a Slack sink. The URL must be https (http is allowed for localhost, for tests).
type SlackConfig ¶
type SlackConfig struct {
// WebhookURL is the Slack incoming webhook URL. It is a credential: keep it in the relay's
// configuration, and it is never put in an error or a log line. Required.
WebhookURL string
// ViewURL, if set, is the base of the run viewer, used for the "Open in viewer" button when the
// notification carries no view.url of its own (bide's WithViewURL). The button is the base, a
// slash and the escaped run ID.
ViewURL string
// HTTPClient, Attempts, Timeout and Backoff tune the outbound call; the zero values mean
// http.DefaultClient-like defaults, DefaultAttempts, DefaultRequestTimeout and DefaultBackoff.
HTTPClient *http.Client
Attempts int
Timeout time.Duration
Backoff time.Duration
}
SlackConfig configures a Slack sink.
type SlackMsg ¶
type SlackMsg struct {
Text string `json:"text"`
Blocks []slackBlock `json:"blocks"`
}
A SlackMsg is the JSON body of an incoming-webhook post: a plain-text fallback (what a notification or a screen reader shows) and Block Kit blocks.
func SlackMessage ¶
SlackMessage formats p as a Slack message: a header naming what happened, the generated summary, the run, kind, deadline and (for an approval) who may decide, and a button to the run viewer when there is a link: the payload's view.url, else viewURL plus the run ID. Every value that came from the run (its ID, tool and approver names) is escaped, so none of it can open a mention (<!channel>) or a link.
type StatusError ¶
A StatusError is a non-2xx answer from a third party. Code is a short token the third party named the problem with ("no_service", "channel_not_found", "invalid event"), kept only if it looks like one (see errorCode); it is never free text, which can echo the request, and never a URL.
func (*StatusError) Error ¶
func (e *StatusError) Error() string
Error names the sink, the status and the code if there is one; never a URL or reply text.