notify

package
v0.0.0-...-acf2466 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package notify turns an outbox event into a message on a channel (§11, ADR-019).

It knows three things and nothing else: how severe an event is, how to word it, and how to hand it to a provider. Whether an event *should* be sent — routing, debouncing, quiet hours — belongs to the dispatcher, which owns the rules and the delivery history.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Supported

func Supported(kind string) bool

Supported reports whether a channel kind can actually deliver.

func ValidateConfig

func ValidateConfig(kind string, cfg Config) error

ValidateConfig checks a channel's configuration before it is stored. A channel is validated for the transport it claims to be: accepting an SMTP channel with no host would create an alerting path that can only fail at the moment it is needed.

Types

type Config

type Config struct {
	URL      string          `json:"url,omitempty"`
	SMTP     *SMTPConfig     `json:"smtp,omitempty"`
	Resend   *ResendConfig   `json:"resend,omitempty"`
	Telegram *TelegramConfig `json:"telegram,omitempty"`
	Pushover *PushoverConfig `json:"pushover,omitempty"`
}

Config is the decrypted configuration of a channel. Only the fields the channel's kind needs are set. The whole struct is envelope-encrypted at rest (ADR-003): a bot token and an SMTP password are credentials, not settings.

type Dispatcher

type Dispatcher struct {
	Store   NotificationStore
	Keyring *envelope.Keyring
	Sender  *Sender
	Logger  *slog.Logger
}

Dispatcher turns outbox events into deliveries (ADR-019). It runs on the elected scheduler leader, so its cursor needs no locking.

The outbox is the single source of truth: notifications are one consumer among others (SSE reads the same table), which is why the cursor lives here and not in the outbox rows.

func (*Dispatcher) Dispatch

func (d *Dispatcher) Dispatch(ctx context.Context)

Dispatch reads the events published since the last pass and delivers them. The cursor only advances over events that were fully considered: an event whose delivery failed keeps its row (status failed) and is not re-read, but an event we never got to is read again next time.

func (*Dispatcher) FlushDigests

func (d *Dispatcher) FlushDigests(ctx context.Context)

FlushDigests sends one grouped message per digest rule whose window has elapsed (ADR-019 §4): "what did not need to wake me up, told once".

The pending deliveries ARE the queue — there is no second table to keep in sync, and a crash mid-flush simply leaves them pending for the next pass.

type Event

type Event struct {
	Type       string         `json:"event_type"`
	Severity   string         `json:"severity"`
	OccurredAt time.Time      `json:"occurred_at"`
	TeamUUID   string         `json:"team_uuid,omitempty"`
	Resource   string         `json:"resource_uuid,omitempty"`
	Payload    map[string]any `json:"payload,omitempty"`
	// Suppressed counts the events this one stands for: a debounced alert
	// says "and 12 others" instead of hiding them (ADR-019).
	Suppressed int `json:"suppressed_count,omitempty"`
}

Event is what a channel is asked to deliver.

func (Event) Text

func (e Event) Text() string

Text includes the event URL for plain-text transports.

type Kind

type Kind string

Kind is the provider a channel talks to.

const (
	KindWebhook  Kind = "webhook"
	KindSlack    Kind = "slack"
	KindDiscord  Kind = "discord"
	KindSMTP     Kind = "smtp"
	KindResend   Kind = "resend"
	KindTelegram Kind = "telegram"
	KindPushover Kind = "pushover"
)

The channel kinds that can deliver today; the schema declares more.

type NotificationStore

type NotificationStore interface {
	GetNotificationCursor(context.Context) (int64, error)
	ListOutboxEventsAfter(context.Context, store.ListOutboxEventsAfterParams) ([]store.OutboxEvent, error)
	SetNotificationCursor(context.Context, int64) error
	ResolveProjectEnvironmentOfResource(context.Context, pgtype.UUID) (store.ResolveProjectEnvironmentOfResourceRow, error)
	MatchNotificationRules(context.Context, store.MatchNotificationRulesParams) ([]store.MatchNotificationRulesRow, error)
	CreateNotificationDelivery(context.Context, store.CreateNotificationDeliveryParams) (store.NotificationDelivery, error)
	FinishNotificationDelivery(context.Context, store.FinishNotificationDeliveryParams) error
	GetNotificationChannelByID(context.Context, int64) (store.NotificationChannel, error)
	LastSentDelivery(context.Context, int64) (pgtype.Timestamptz, error)
	CountSuppressedSince(context.Context, store.CountSuppressedSinceParams) (int64, error)
	ListDigestRulesDue(context.Context) ([]store.ListDigestRulesDueRow, error)
	ListPendingDigestDeliveries(context.Context, int64) ([]store.ListPendingDigestDeliveriesRow, error)
	MarkDigestDeliveriesFailed(context.Context, store.MarkDigestDeliveriesFailedParams) error
	MarkDigestDeliveriesSent(context.Context, []int64) error
	SetRuleDigestFlushed(context.Context, int64) error
}

NotificationStore is the narrow database boundary used by the dispatcher. Keeping it here makes the routing and noise-control rules unit-testable without starting PostgreSQL.

type PushoverConfig

type PushoverConfig struct {
	Token   string `json:"token"`
	UserKey string `json:"user_key"`
}

PushoverConfig posts to the Pushover message API.

type ResendConfig

type ResendConfig struct {
	APIKey string   `json:"api_key"`
	From   string   `json:"from"`
	To     []string `json:"to"`
}

ResendConfig posts to the Resend HTTP API.

type SMTPConfig

type SMTPConfig struct {
	Host       string   `json:"host"`
	Port       int      `json:"port"`
	Username   string   `json:"username,omitempty"`
	Password   string   `json:"password,omitempty"`
	From       string   `json:"from"`
	To         []string `json:"to"`
	Encryption string   `json:"encryption"` // starttls | tls | none
}

SMTPConfig talks to a mail relay. `Encryption` is explicit rather than guessed from the port: a channel that silently downgrades to plaintext would put the password and the alert on the wire in clear, and nothing would say so.

type Sender

type Sender struct {
	HTTP *http.Client
}

Sender delivers events to a channel.

func New

func New() *Sender

New builds a sender. The timeout is short: a channel that hangs must not hold the dispatcher, and a missed alert is retried on the next pass.

The HTTP client is SSRF-guarded (safedial): a notification channel's webhook URL is set by team members (notifications:manage), so it is attacker- influenceable — the classic SSRF vector is a "test channel" call pointed at 169.254.169.254. Blocking non-public destinations here closes it. (The SMTP dial below is NOT guarded: the relay is instance-root configuration and may legitimately be an internal host.)

func (*Sender) Send

func (s *Sender) Send(ctx context.Context, kind string, cfg Config, e Event) error

Send posts the event to the channel. The body shape is the provider's; the event itself is always included so a webhook consumer gets the structured data, not only a sentence.

type Severity

type Severity int

Severity orders how much an event deserves to wake someone up (ADR-019).

const (
	SeverityInfo Severity = iota
	SeverityWarning
	SeverityCritical
)

The severity ladder: info is routine, critical wakes someone up.

func ParseSeverity

func ParseSeverity(s string) Severity

ParseSeverity reads the enum stored on a rule.

func SeverityOf

func SeverityOf(eventType string) Severity

SeverityOf classifies an event type. The taxonomy is the load-bearing part of ADR-019: a critical event wrongly classified as info would be deferred into a digest, which is the one failure mode the ADR calls out. So the default is deliberately NOT info — an unknown event that mentions a failure is treated as a failure.

func (Severity) String

func (s Severity) String() string

type TelegramConfig

type TelegramConfig struct {
	BotToken string `json:"bot_token"`
	ChatID   string `json:"chat_id"`
	TopicID  string `json:"topic_id,omitempty"`
}

TelegramConfig posts to the Bot API.

Jump to

Keyboard shortcuts

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