Help us shape the future of Go! Take the 2026 Go Developer Survey

relay

package
v0.23.0 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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

func ClearsID(p notify.Payload) string

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

func DedupKey(p notify.Payload) string

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

func DefaultResolveTarget(p notify.Payload) (string, bool)

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

func DefaultSeverity(p notify.Payload) string

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

func KindTitle(kind string) string

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

func RejectionClass(err error) string

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 New

func New(c Config) (*Handler, error)

New returns a Handler, or an error for a configuration that cannot work.

func (*Handler) Close

func (h *Handler) Close() error

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.

func (*PagerDuty) Name

func (d *PagerDuty) Name() string

Name is "pagerduty".

func (*PagerDuty) Send

func (d *PagerDuty) Send(ctx context.Context, p notify.Payload) error

Send triggers an incident for p, or resolves one if p is a clear (see ResolveTarget).

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 struct {
	Href string `json:"href"`
	Text string `json:"text"`
}

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

type Sink interface {
	Name() string
	Send(ctx context.Context, p notify.Payload) error
}

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

func (*Slack) Name

func (s *Slack) Name() string

Name is "slack".

func (*Slack) Send

func (s *Slack) Send(ctx context.Context, p notify.Payload) error

Send posts p as a message.

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

func SlackMessage(p notify.Payload, viewURL string) SlackMsg

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

type StatusError struct {
	Sink   string
	Status int
	Code   string
}

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.

Jump to

Keyboard shortcuts

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