Documentation
¶
Overview ¶
Package notify tells people when their backups need attention.
The failure this exists for is the silent one: a Space that quietly stopped backing up months ago is worse than one that never started, because the family believes they are covered. Every event is therefore *recorded* first and delivered second — a durable record is the part the status board and the tests rely on, delivery is best-effort on top.
Audiences are split deliberately (decisions.md #15). A member of a Space hears about that Space: its run failed, its backups are stale. The operator hears about the machinery: a target is unreachable, credentials were rejected. Operator events carry **no space id, no user, no file counts** — the admin is a configuration actor, not a data actor, and must not learn which Spaces exist or how they are doing from a notification.
Nothing here ever formats key material, credentials, paths or file names: the only free text an event carries is the sanitized message the runner already deemed safe to show a user.
Index ¶
- Constants
- type Audience
- type Classification
- type Classifier
- type Clock
- type Event
- type Kind
- type LogSink
- type Monitor
- type MonitorDeps
- type MonitorOptions
- type Notifier
- type Options
- type Reporter
- type SMTPConfig
- type SMTPEnv
- type SMTPSink
- type Sink
- type StaleRule
- type Staleness
- type StateStore
- func (s *StateStore) Append(ctx context.Context, e Event) (Event, error)
- func (s *StateStore) List(ctx context.Context, spaceID string, limit int) ([]Event, error)
- func (s *StateStore) ListOperator(ctx context.Context, limit int) ([]Event, error)
- func (s *StateStore) PruneBefore(ctx context.Context, cutoff time.Time) (int, error)
- type Store
Constants ¶
const ( // DefaultMinStaleAfter is the floor on the staleness threshold, whatever // the schedule says. Nothing is reported stale sooner than this. DefaultMinStaleAfter = 36 * time.Hour // DefaultRepeatAfter is how long before an unchanged stale Space is // reported again. Nagging daily is how notifications get muted. DefaultRepeatAfter = 7 * 24 * time.Hour )
const DefaultSMTPTimeout = 30 * time.Second
DefaultSMTPTimeout bounds one mail delivery. A mail server that accepts the connection and then says nothing must not hold a scheduler slot, or the shutdown drain, for longer than this (review-2026-10.md F7).
const StaleLookback = 50
StaleLookback is how many of a Space's most recent backup records a caller must hand to Assess. It is a contract, not a tuning knob: two callers passing different windows could disagree about when the last success was.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Classification ¶
type Classification struct {
// MemberMessage is the user-safe text shown to the Space's members.
MemberMessage string
// Operational reports whether the operator has to act. When true, an
// anonymous operator event is recorded alongside the member event.
Operational bool
// OperatorMessage is the operator's text. It must not identify a Space or
// a user; the Reporter rejects the event if it somehow does.
OperatorMessage string
// Silent suppresses notification entirely. Some "failures" are not news to
// anybody — a run skipped because another was already going, or one cut
// short by shutdown.
Silent bool
}
Classification says what a failed run means and to whom.
func DefaultClassification ¶
func DefaultClassification() Classification
DefaultClassification is what an unrecognised failure means: tell the member something went wrong, do not speculate, do not page the operator.
type Classifier ¶
type Classifier func(err error) Classification
Classifier maps a run error onto a Classification.
type Event ¶
type Event struct {
ID string `json:"id"`
Kind Kind `json:"kind"`
Audience Audience `json:"audience"`
// SpaceID is set for space-scoped events only, and must be empty for
// operator events.
SpaceID string `json:"space_id,omitempty"`
// Message is user-safe text. It never contains internal detail.
Message string `json:"message"`
CreatedAt time.Time `json:"created_at"`
}
Event is one thing worth telling somebody about.
type Kind ¶
type Kind string
Kind classifies an event.
const ( // KindRunFailed reports one failed run of a Space. Space-scoped. KindRunFailed Kind = "run_failed" // KindBackupStale reports that a Space has had no successful backup for // longer than its schedule allows. This is the important one: it is what // catches failures nobody noticed. KindBackupStale Kind = "backup_stale" // Operator-scoped and deliberately anonymous. KindTargetUnavailable Kind = "target_unavailable" // KindStateUnreadable reports that stored records could not be decoded. // Operator-scoped: it carries a count and nothing else, because the key of // such a document contains the space id it belongs to (decisions.md #15). KindStateUnreadable Kind = "state_unreadable" )
type LogSink ¶
LogSink writes events to the service log. It is always wired: an operator reading logs must be able to see what users were told.
type Monitor ¶
type Monitor struct {
// contains filtered or unexported fields
}
Monitor reports Spaces whose backups have gone stale.
func NewMonitor ¶
func NewMonitor(deps MonitorDeps, opts MonitorOptions) (*Monitor, error)
NewMonitor constructs a Monitor.
type MonitorDeps ¶
type MonitorDeps struct {
Configs spacecfg.Store
Jobs jobs.Store
Events Store
Notifier *Notifier
Logger *slog.Logger
}
MonitorDeps are the Monitor's collaborators.
type MonitorOptions ¶
type MonitorOptions struct {
// MinStaleAfter is the floor on the threshold; zero uses the default.
MinStaleAfter time.Duration
// RepeatAfter is the re-notification interval; zero uses the default.
RepeatAfter time.Duration
}
MonitorOptions tunes staleness detection. The zero value is usable.
type Notifier ¶
type Notifier struct {
// contains filtered or unexported fields
}
Notifier records events and fans them out to sinks.
func (*Notifier) Notify ¶
Notify records an event and delivers it. Recording is what must succeed; delivery failures are logged and swallowed, because a notification nobody could email is still a notification the status board must show.
func (*Notifier) OperatorEvent ¶
OperatorEvent records an operational event. The signature has no space parameter on purpose: it is not possible to leak one through this path.
type Options ¶
type Options struct {
// Sinks deliver events; may be empty, in which case events are recorded
// only (and still visible through the API).
Sinks []Sink
// Clock is injected for deterministic tests.
Clock Clock
// Logger receives delivery diagnostics.
Logger *slog.Logger
}
Options configures a Notifier.
type Reporter ¶
type Reporter struct {
// contains filtered or unexported fields
}
Reporter records notifications for run outcomes.
func NewReporter ¶
func NewReporter(n *Notifier, classify Classifier, logger *slog.Logger) *Reporter
NewReporter constructs a Reporter. A nil classifier uses the default.
type SMTPConfig ¶
type SMTPConfig struct {
// Host and Port address the mail server, e.g. "smtp.example.org", 587.
Host string
Port int
// Username and Password authenticate; leave empty for an open relay on a
// trusted network.
Username string
Password string
// From is the envelope sender.
From string
// OperatorTo receives operator-audience events. When empty, operator events
// are recorded but not mailed.
OperatorTo string
// Timeout bounds one delivery, from dialling to QUIT. Zero uses
// DefaultSMTPTimeout.
Timeout time.Duration
}
SMTPConfig is the operator's mail configuration. The password arrives from a Secret-backed environment variable and is never logged.
func (SMTPConfig) Valid ¶
func (c SMTPConfig) Valid() bool
Valid reports whether the configuration can send anything.
type SMTPEnv ¶
type SMTPEnv struct {
Host string `env:"SMTP_HOST" doc:"Mail server for operator notifications."`
Port int `env:"SMTP_PORT" doc:"Mail server port, such as 587. 0 disables mail."`
Username string `env:"SMTP_USERNAME" doc:"Mail server login, if it requires one."`
Password config.Secret `env:"SMTP_PASSWORD" doc:"Mail server password."`
From string `env:"SMTP_FROM" doc:"Sender address."`
OperatorTo string `env:"NOTIFY_OPERATOR_EMAIL" doc:"Where operator notifications go. They never name a Space."`
}
SMTPEnv is the mail server operator notifications go through, as the environment declares it (internal/config). All of host, port, sender and recipient are needed; otherwise notifications are recorded and logged only.
func (SMTPEnv) Config ¶
func (e SMTPEnv) Config() SMTPConfig
Config is the SMTP sink's configuration from e.
type SMTPSink ¶
type SMTPSink struct {
// contains filtered or unexported fields
}
SMTPSink mails operator events.
func NewSMTPSink ¶
func NewSMTPSink(cfg SMTPConfig) (*SMTPSink, error)
NewSMTPSink constructs a sink from operator configuration.
type Sink ¶
type Sink interface {
// Deliver sends one event. Name it in errors, never its recipients' data.
Deliver(ctx context.Context, e Event) error
}
Sink delivers an event somewhere a human will see it. Delivery failures are never fatal: the record is already durable.
type StaleRule ¶
type StaleRule struct {
// MinStaleAfter is the floor on the threshold; zero or less uses the
// default.
MinStaleAfter time.Duration
}
StaleRule decides whether a Space's backups have gone stale. The zero value applies DefaultMinStaleAfter.
func (StaleRule) Assess ¶
Assess applies the rule. backups must be the Space's backup-kind history, newest first, read with a limit of StaleLookback.
A Space whose scheduled backups are switched off is not stale: it is off, and saying otherwise would be a warning nobody can clear except by turning backups on.
type Staleness ¶
type Staleness struct {
// Stale reports whether the Space has gone too long without a successful
// backup.
Stale bool
// Since is the last successful backup or, if there has never been one,
// when the Space was configured. Zero when there is nothing to measure
// from.
Since time.Time
}
Staleness is the rule's verdict.
type StateStore ¶
type StateStore struct {
// contains filtered or unexported fields
}
StateStore is a Store backed by durable state.
func NewStateStore ¶
func NewStateStore(st state.Store, clock Clock) *StateStore
NewStateStore returns a Store persisting to st.
func (*StateStore) ListOperator ¶
ListOperator returns operator events, newest first.
func (*StateStore) PruneBefore ¶
PruneBefore deletes events created before cutoff. Candidates are chosen from key names, so no document is read to decide.
type Store ¶
type Store interface {
// Append records an event, filling in id and timestamp when absent.
Append(ctx context.Context, e Event) (Event, error)
// List returns the events of one scope, newest first. A scope is a space
// id for member events; use ListOperator for operator events. A limit of
// zero or less means "all".
List(ctx context.Context, spaceID string, limit int) ([]Event, error)
// ListOperator returns operator events, newest first.
ListOperator(ctx context.Context, limit int) ([]Event, error)
// PruneBefore deletes events created before cutoff.
PruneBefore(ctx context.Context, cutoff time.Time) (int, error)
}
Store persists events.