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

notifications

package
v0.10.1 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package notifications sends user-facing convenience notifications (workflow completion, ...) to configured channels. It is deliberately NOT the admin monitoring path — leaked workers, DB pool saturation, quota exhaustion etc. surface through server/metrics as Prometheus gauges/counters that Zabbix / Grafana consume. Alerting logic (silence, escalate, page on-call) lives in the monitoring tool.

This package is best-effort: a failed webhook is logged and the caller keeps going. Users who miss a workflow-done ping are mildly annoyed; that's the whole failure surface.

Backends:

  • zulip: knows the Zulip incoming-webhook JSON shape natively. Just pass the webhook URL (with stream + topic + api_key as URL params, per Zulip's docs).
  • webhook: generic HTTP POST with a Go-template body. Fits Slack, Discord, ntfy, or any custom endpoint that accepts JSON or text.
  • log: writes the notification to the server log. Always available; useful as a smoke test or as a fallback channel.

The Dispatcher reads a route table (event -> channels) and fans each Emit() call out to every subscribed channel in parallel. No retries, no dedup, no rate limit for now — add those the day we need them.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Dispatcher

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

Dispatcher owns the channel set + routing table and dispatches events to matching channels in parallel.

func New

New builds a Dispatcher from the given config block. Unknown backend kinds and malformed URLs are logged and skipped rather than failing server startup — a bad notification channel is not a reason to refuse to boot.

func (*Dispatcher) Emit

func (d *Dispatcher) Emit(ctx context.Context, msg Message)

Emit dispatches msg to every channel subscribed to msg.Event. Deliveries run in parallel; the call returns as soon as they are launched. Errors are logged inside each backend's Send — this method never returns one because callers on hot paths (workflow completion) shouldn't be blocked by a slow webhook.

The caller's ctx is INTENTIONALLY not passed down to the send goroutines. Callers on the workflow-status path build a ctx with a short timeout for their DB queries and defer-cancel it on return; since Emit doesn't wg.Wait, that cancel fires while the HTTP request is still in flight and kills it with "context canceled" (observed 2026-09-10 on the very first live workflow.terminal to Zulip). Each backend caps request duration via its own http.Client.Timeout, so context.Background here is both safe and necessary.

type EventType

type EventType string

EventType is the routing key. Backends never look at this — the Dispatcher does, matching against the configured routes.

const (
	// EventWorkflowTerminal fires when a workflow transitions to S or
	// F. Meta carries workflow_id, name, status, and counts.
	EventWorkflowTerminal EventType = "workflow.terminal"
)

type Log

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

Log is a Notifier that writes to the standard server log. Always available, no config beyond name — useful as a smoke test channel or a "notify me on the console" for a lab operator without a separate integration.

func NewLog

func NewLog(cfg config.NotificationChannel) *Log

func (*Log) Name

func (l *Log) Name() string

func (*Log) Send

func (l *Log) Send(_ context.Context, msg Message) error

type Message

type Message struct {
	Event    EventType
	Severity Severity
	Title    string
	Body     string
	// Meta is arbitrary key/value context. The Zulip backend ignores
	// it (Body carries the human-readable text); the generic webhook
	// backend exposes it to its body template as `.Meta.<key>`.
	Meta map[string]string
}

Message is one notification. Backends turn (Title, Body, Meta) into their own on-the-wire format. Keep Body compact — most channels line-truncate at some point.

type Notifier

type Notifier interface {
	// Name is the channel name from config — used in log lines and
	// as the routing key.
	Name() string
	// Send delivers the message. Errors are logged and swallowed by
	// the Dispatcher — a broken webhook must never bring down the
	// caller's code path.
	Send(ctx context.Context, msg Message) error
}

Notifier is one delivery target.

type Severity

type Severity string

Severity ranks the message. Backends can filter on it; the initial backends ignore it. Kept in the interface from day one so a future "warn+ only" channel doesn't force a signature change.

const (
	SeverityInfo Severity = "info"
	SeverityWarn Severity = "warn"
	SeverityCrit Severity = "crit"
)

type Webhook

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

Webhook is a generic HTTP-POST notifier with a templated body. The template runs on the Message plus a small "toJSON" helper, so most JSON-eating endpoints (Slack, Discord, ntfy, custom) fit with one config line.

Configuration:

url:      required, the endpoint URL
method:   optional, defaults to POST
template: optional Go text/template. Default: JSON with a
          conventional shape ({event, severity, title, body, meta}).
content_type: optional, defaults to application/json
headers:  optional map, added to every request (e.g. Authorization)

The template has access to:

{{.Event}} {{.Severity}} {{.Title}} {{.Body}} {{.Meta.foo}} …
{{toJSON .Body}}    -- JSON-quote a string
{{toJSON .Meta}}    -- JSON-serialise the meta map

func NewWebhook

func NewWebhook(cfg config.NotificationChannel) (*Webhook, error)

func (*Webhook) Name

func (w *Webhook) Name() string

func (*Webhook) Send

func (w *Webhook) Send(ctx context.Context, msg Message) error

type Zulip

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

Zulip is the incoming-webhook Notifier. Configure with the URL Zulip's "Incoming webhook (generic)" integration hands out — it looks like:

https://<subdomain>.zulipchat.com/api/v1/external/generic?api_key=...&stream=alerts&topic=scitq

The URL carries stream + topic + api_key as query params. We only need to POST `content` (the message body) and optionally `topic` (overrides the one baked into the URL). See: https://zulip.com/integrations/doc/generic

func NewZulip

func NewZulip(cfg config.NotificationChannel) (*Zulip, error)

NewZulip builds a Zulip notifier from a channel config entry. Required: cfg.URL. Optional: cfg.Options["topic"] for a per-channel topic override.

func (*Zulip) Name

func (z *Zulip) Name() string

func (*Zulip) Send

func (z *Zulip) Send(ctx context.Context, msg Message) error

type ZulipDM

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

ZulipDM is the Notifier that sends private (direct) messages via Zulip's `messages` REST API. Distinct from the `zulip` backend (which POSTs to /api/v1/external/generic for stream messages) — incoming-webhook bots CANNOT hit /api/v1/messages, so this backend requires a Zulip bot of type "Generic bot" whose bot_email + api_key are given in the channel config.

The recipient email is not baked into the channel — it's read from each Message's `Meta["run_by_email"]` at Send time. That's what makes DM routing per-workflow-owner work without a static channel-per-user setup: one `zulip-dm` channel serves every user whose scitq account has an email address set.

Config:

channels:
  - name: gmt-zulip-dm
    kind: zulip-dm
    url: "https://gmt.zulipchat.com"       # Zulip realm, no path
    options:
      bot_email: "scitq-bot@gmt.zulipchat.com"
      api_key: "${ZULIP_API_KEY}"
      # recipient_meta_key defaults to "run_by_email"; override if
      # a future event carries the recipient under a different key.
      recipient_meta_key: "run_by_email"

If the message's Meta lacks the recipient key (or has it blank), Send logs a one-liner and returns nil — a user with no email set is not an error, just a "no target for this notification". No retries; the caller doesn't wait on us anyway.

func NewZulipDM

func NewZulipDM(cfg config.NotificationChannel) (*ZulipDM, error)

func (*ZulipDM) Name

func (z *ZulipDM) Name() string

func (*ZulipDM) Send

func (z *ZulipDM) Send(ctx context.Context, msg Message) error

Jump to

Keyboard shortcuts

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