beacon

package module
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: GPL-3.0 Imports: 20 Imported by: 0

README

beacon

A small Go library for sending notifications and pushing telemetry. beacon gives a program one place to tell the outside world what is happening: fire an alert when something needs attention, and push health and status to a monitor on a schedule.

It is deliberately narrow. beacon knows about notification channels and telemetry sinks and nothing else, so it drops into any project without dragging along unrelated dependencies. Secret resolution is injected, so the host program supplies tokens and passwords its own way.

Status: early, and built first for Ballast, the label-driven backup tool. The API will move until it settles.

Two axes

  • Notifications: alerts to the channels people actually watch, including email, ntfy, Gotify, Telegram, Discord, Slack, Mattermost, Pushover, and Matrix, with a generic webhook or a templatable HTTP channel for anything else.
  • Telemetry: health and status push to a monitor, starting with Gatus external endpoints.

Design

beacon is configured through a plain struct and returns a small interface you call when something happens. It imports nothing from any particular application. A host wires its own events into it and provides a function that resolves secret names to values, so beacon never assumes where secrets live.

Testing

test/integration/ holds beacon's delivery tests: real end-to-end sends against throwaway ntfy, Gotify, and mailpit containers for the channels with a self-hostable server, and outbound-request-shape checks against an in-process catcher for the rest. See docs/TESTING.md for the coverage matrix and how to run it.

License

GPL-3.0-or-later. See LICENSE. Each source file carries an SPDX-License-Identifier: GPL-3.0-or-later header.

Documentation

Overview

Package beacon is a small library for sending notifications and pushing telemetry: alerts to the channels people watch, and health and status to a monitor. It imports nothing application-specific, and secret resolution is injected by the host program.

The API is under construction.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterBackend

func RegisterBackend(typ string, ctor BackendFactory)

RegisterBackend makes a notification backend type available to Config, keyed by typ (e.g. "ntfy", "discord"). It is meant to be called from an init function in the package implementing that backend. Registering the same type twice panics, since it almost always indicates two packages fighting over the same name.

func RegisterSink

func RegisterSink(typ string, ctor SinkFactory)

RegisterSink makes a telemetry sink type available to Config, keyed by typ (e.g. "gatus"). It is meant to be called from an init function in the package implementing that sink. Registering the same type twice panics.

Types

type Backend

type Backend interface {
	// Name identifies this backend instance, typically its registered type
	// (e.g. "ntfy"). Used in error messages and logs.
	Name() string

	// Send delivers a notification through this channel. Implementations
	// should resolve any secrets they need at send time, not at
	// construction time, so credential rotation does not require a
	// rebuild.
	Send(ctx context.Context, n Notification) error
}

Backend is one notification channel: email, ntfy, Discord, a generic webhook, and so on. A Backend is constructed by the BackendFactory registered under its Type and lives for the lifetime of a Beacon.

type BackendFactory

type BackendFactory func(settings map[string]string, resolve SecretResolver) (Backend, error)

BackendFactory builds a Backend from its configured settings. resolve is never nil; a host with no secret store still provides a resolver that returns an error for every name, so factories can call it unconditionally.

type Beacon

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

Beacon fans a notification out to configured channels and a health result out to configured telemetry sinks.

func New

func New(cfg Config, resolve SecretResolver) (*Beacon, error)

New builds a Beacon from cfg, constructing each configured channel and sink from the registry populated by RegisterBackend and RegisterSink. resolve may be nil if the host has no secrets to offer; New then uses a resolver that errors on every lookup.

New fails if a Channels or Telemetry entry names a Type that has not been registered, or if a factory returns an error.

func (*Beacon) Notify

func (b *Beacon) Notify(ctx context.Context, n Notification) error

Notify fans n out to every channel whose MinLevel is at or below n.Level. It attempts every eligible channel even if one fails, and returns the combined error via errors.Join (nil if all succeeded, or if no channel was eligible).

func (*Beacon) Report

func (b *Beacon) Report(ctx context.Context, h Health) error

Report fans h out to every configured telemetry sink. It attempts every sink even if one fails, and returns the combined error via errors.Join (nil if all succeeded, or if there are no sinks).

type ChannelConfig

type ChannelConfig struct {
	// Type selects the backend, matching a name passed to RegisterBackend
	// (e.g. "log", "ntfy", "discord").
	Type string
	// MinLevel is the lowest severity this channel receives. A
	// Notification below MinLevel is not sent to this channel.
	MinLevel Level
	// Settings is backend-specific configuration. Any value that names a
	// secret rather than holding it directly is resolved through the
	// SecretResolver by the backend itself, at send time, so credential
	// rotation takes effect without a rebuild or restart.
	Settings map[string]string
}

ChannelConfig configures one notification backend.

type Config

type Config struct {
	Channels  []ChannelConfig
	Telemetry []TelemetryConfig
}

Config is the full beacon configuration: the notification channels and telemetry sinks to build.

type DiscordBackend

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

DiscordBackend sends notifications to a Discord incoming webhook.

func (*DiscordBackend) Name

func (b *DiscordBackend) Name() string

Name returns "discord".

func (*DiscordBackend) Send

Send POSTs n to the configured webhook as an embed: title, description, and a color keyed off Level. If n.Title is empty there is nothing meaningful to title an embed with, so Send falls back to a plain content message instead.

type GatusSink

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

GatusSink pushes health results to a Gatus external endpoint, Gatus's push-based heartbeat API for monitoring things Gatus cannot reach or poll itself (a cron job, a background worker, a service behind a firewall). Gatus treats the endpoint as unhealthy if it does not receive a push within its configured interval, so Report should be called on a dead-man's-switch schedule even when h.OK is true.

func (*GatusSink) Name

func (s *GatusSink) Name() string

Name returns "gatus".

func (*GatusSink) Report

func (s *GatusSink) Report(ctx context.Context, h Health) error

Report POSTs h to the configured external endpoint's push URL. success is h.OK; duration is encoded as a Go duration string (e.g. "250ms"); error is included, carrying h.Message, only when h.OK is false. All three are sent as URL query parameters, since Gatus's external-endpoint API takes no request body.

type GotifyBackend

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

GotifyBackend sends notifications to a self-hosted Gotify server.

func (*GotifyBackend) Name

func (b *GotifyBackend) Name() string

Name returns "gotify".

func (*GotifyBackend) Send

Send POSTs n to the Gotify /message endpoint, authenticated with the app token via the X-Gotify-Key header. Priority uses the same 1-5 scale shared with ntfy, which sits comfortably inside Gotify's 0-10 range.

type HTTPBackend added in v0.1.2

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

HTTPBackend is the generic templatable HTTP channel: it renders a caller-supplied body template into an HTTP POST and, optionally, checks a named field of the JSON response to decide whether the delivery actually succeeded. It makes no assumption about the target's request or response shape, so any service that accepts an HTTP POST can be reached by configuration alone, without a bespoke backend and without a new dependency.

It complements WebhookBackend: "webhook" sends beacon's own fixed JSON payload (optionally HMAC-signed), while "http" lets the caller define the exact bytes on the wire. Reach for "http" when a receiver dictates its own payload shape.

func (*HTTPBackend) Name added in v0.1.2

func (b *HTTPBackend) Name() string

Name returns "http".

func (*HTTPBackend) Send added in v0.1.2

func (b *HTTPBackend) Send(ctx context.Context, n Notification) error

Send renders the body template for n and POSTs it. A transport error or a non-2xx status is a failure. When success_field is configured, the JSON response is parsed and the field is checked too, so a service that returns 200 while reporting a failure in its body (a common pattern) is not mistaken for a successful delivery.

type Health

type Health struct {
	// Name identifies what this health result is about, e.g. a service or
	// job name.
	Name string
	// OK reports whether the thing being monitored is healthy.
	OK bool
	// Message is a short human-readable detail, especially useful when
	// OK is false.
	Message string
	// Duration is how long the underlying operation took, if applicable.
	Duration time.Duration
}

Health is one telemetry result to push to a monitor.

type Level

type Level int

Level is the severity of a Notification, ordered from least to most urgent.

const (
	// LevelInfo is routine, non-urgent information.
	LevelInfo Level = iota
	// LevelWarning is something worth a look but not on fire.
	LevelWarning
	// LevelError is something that needs attention now.
	LevelError
)

func (Level) String

func (l Level) String() string

String returns the lowercase name of the level, e.g. "warning".

type LogBackend

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

LogBackend is the built-in, always-available notification backend. It writes notifications either as plain lines to an io.Writer or as structured records through log/slog, and depends on nothing outside the standard library. It is registered under the type "log", and is the floor every beacon.Config can fall back to when no other channel is configured or reachable.

func NewLogBackend

func NewLogBackend(w io.Writer) *LogBackend

NewLogBackend returns a LogBackend that writes one formatted line per notification to w.

func NewSlogBackend

func NewSlogBackend(logger *slog.Logger) *LogBackend

NewSlogBackend returns a LogBackend that emits each notification as a structured record through logger, at a level matching Notification.Level.

func (*LogBackend) Name

func (l *LogBackend) Name() string

Name returns "log".

func (*LogBackend) Send

func (l *LogBackend) Send(ctx context.Context, n Notification) error

Send writes n to the configured writer or slog logger. It never returns an error: the log backend is the always-on floor and is not allowed to be the reason a notification is lost.

type MatrixBackend

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

MatrixBackend sends notifications to a Matrix room as a bot user, via the client-server API's room-send endpoint. It only sends: the bot account named by token_secret must already be joined to the room, since this backend has no way to accept or act on an invite.

func (*MatrixBackend) Name

func (b *MatrixBackend) Name() string

Name returns "matrix".

func (*MatrixBackend) Send

Send PUTs n to the room-send endpoint for the configured room, combining Title and Body into the message's "body" field. Every call generates a fresh transaction ID: Matrix homeservers deduplicate room-send requests by (access token, transaction ID), so reusing one across sends would cause every notification after the first to be silently dropped instead of posted.

type MattermostBackend

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

MattermostBackend sends notifications to a Mattermost incoming webhook. Mattermost's incoming webhooks accept the same JSON shape as Slack's, so this backend reuses buildSlackPayload from slack.go rather than duplicating it.

func (*MattermostBackend) Name

func (b *MattermostBackend) Name() string

Name returns "mattermost".

func (*MattermostBackend) Send

Send POSTs n to the configured Mattermost incoming webhook.

type Notification

type Notification struct {
	// Title is a short summary line.
	Title string
	// Body is the longer message, if any.
	Body string
	// Level is the severity, used both for display and for routing: a
	// channel only receives notifications at or above its MinLevel.
	Level Level
	// Tags are free-form labels a backend may use for routing or display
	// (e.g. topic names for ntfy).
	Tags []string
	// Fields are structured key/value details a backend may render
	// alongside the body (e.g. a service name, a duration, an exit code).
	Fields map[string]string
}

Notification is one alert to send to the channels people watch.

type NtfyBackend

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

NtfyBackend sends notifications to an ntfy server (ntfy.sh or self-hosted) as a plain HTTP POST to a topic.

func (*NtfyBackend) Name

func (b *NtfyBackend) Name() string

Name returns "ntfy".

func (*NtfyBackend) Send

func (b *NtfyBackend) Send(ctx context.Context, n Notification) error

Send POSTs n to the configured ntfy topic. The request body is n.Body, falling back to n.Title if there is no body. Title, Tags, and Priority are sent as ntfy headers.

type PushoverBackend

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

PushoverBackend sends notifications through Pushover.

func (*PushoverBackend) Name

func (b *PushoverBackend) Name() string

Name returns "pushover".

func (*PushoverBackend) Send

Send posts n to the Pushover messages API. Message is required by Pushover, so Send falls back to Title when Body is empty. Priority is deliberately capped below Pushover's emergency tier (2), which requires additional retry/expire parameters this backend does not manage.

type SMTPBackend

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

SMTPBackend sends notifications as email through an SMTP relay.

func (*SMTPBackend) Name

func (b *SMTPBackend) Name() string

Name returns "smtp".

func (*SMTPBackend) Send

func (b *SMTPBackend) Send(ctx context.Context, n Notification) error

Send dials the configured relay and delivers n as an email. Credentials, when configured, are resolved fresh on every call so rotating them never requires a rebuild or restart.

type SecretResolver

type SecretResolver func(name string) (string, error)

SecretResolver resolves a secret by name to its value. The host program supplies one so beacon never assumes where secrets live. A nil resolver is treated as "no secrets available": New replaces it with a resolver that returns an error for every name, so backend and sink factories can call resolve unconditionally.

type SinkFactory

type SinkFactory func(settings map[string]string, resolve SecretResolver) (TelemetrySink, error)

SinkFactory builds a TelemetrySink from its configured settings, with the same resolve contract as BackendFactory.

type SlackBackend

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

SlackBackend sends notifications to a Slack incoming webhook.

func (*SlackBackend) Name

func (b *SlackBackend) Name() string

Name returns "slack".

func (*SlackBackend) Send

func (b *SlackBackend) Send(ctx context.Context, n Notification) error

Send POSTs n to the configured Slack incoming webhook.

type TelegramBackend

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

TelegramBackend sends notifications through a Telegram bot to a chat.

func (*TelegramBackend) Name

func (b *TelegramBackend) Name() string

Name returns "telegram".

func (*TelegramBackend) Send

Send POSTs n to the bot's sendMessage endpoint. Title and Body are combined into a single text field, since Telegram has no separate subject.

type TelemetryConfig

type TelemetryConfig struct {
	// Type selects the sink, matching a name passed to RegisterSink
	// (e.g. "gatus").
	Type string
	// Settings is sink-specific configuration, with the same
	// secret-naming and send-time resolution convention as
	// ChannelConfig.Settings.
	Settings map[string]string
}

TelemetryConfig configures one telemetry sink.

type TelemetrySink

type TelemetrySink interface {
	// Name identifies this sink instance, typically its registered type.
	Name() string

	// Report pushes a health result to this sink.
	Report(ctx context.Context, h Health) error
}

TelemetrySink is one telemetry target: a monitor that wants health and status pushed to it on a schedule or on change, such as a Gatus external endpoint.

type WebhookBackend

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

WebhookBackend sends notifications as a JSON POST to an arbitrary URL, for services with no dedicated backend of their own. If a signing secret is configured, the request carries an X-Beacon-Signature header so the receiver can verify it came from this beacon.

func (*WebhookBackend) Name

func (b *WebhookBackend) Name() string

Name returns "webhook".

func (*WebhookBackend) Send

Send POSTs n to the configured URL as a webhookPayload. The body is marshaled once, so a configured signature is guaranteed to match the exact bytes transmitted.

Directories

Path Synopsis
Package beacontest provides Capture, a capturing beacon.Backend, and New, a *beacon.Beacon wired to it.
Package beacontest provides Capture, a capturing beacon.Backend, and New, a *beacon.Beacon wired to it.

Jump to

Keyboard shortcuts

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