alerting

package
v0.2.2 Latest Latest
Warning

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

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

Documentation

Overview

Package alerting implements threshold rules over internal/telemetry's metrics and logs, and crashloop detection as a built-in rule kind sharing the same evaluate/notify path rather than a separate mechanism.

Index

Constants

View Source
const (
	CertRenewalOK      = "ok"
	CertRenewalStalled = "stalled"
)

Certificate renewal states reported by GET /api/v1/certificates.

View Source
const (
	SlackApprovalActionApprove = "deploy_approval_approve"
	SlackApprovalActionReject  = "deploy_approval_reject"
)

SlackApprovalActionApprove and SlackApprovalActionReject are the action_id every Approve/Deny button carries, and the one shared constant internal/api/chat_interactions.go dispatches on: a repeated string literal for the same action_id in two packages would risk the button and the handler silently drifting apart.

View Source
const (
	DiscordApprovalCustomIDApprove = "deploy_approval_approve"
	DiscordApprovalCustomIDReject  = "deploy_approval_reject"
)

DiscordApprovalCustomIDApprove/Reject prefix a button's custom_id; chat_interactions.go splits on ":" to recover the approval ID.

View Source
const (
	EventFired      = "fired"
	EventResolved   = "resolved"
	EventFlapping   = "flapping"
	EventFlapEnded  = "flap_ended"
	EventGroupFlush = "group"
)

History event names.

View Source
const (
	OutcomeSent        = "sent"
	OutcomeSilenced    = "silenced"
	OutcomeGrouped     = "grouped"
	OutcomeInhibited   = "inhibited"
	OutcomeFailed      = "failed"
	OutcomeRateLimited = "ratelimited"
	OutcomeFlapping    = "flapping"
	OutcomeSkipped     = "skipped"
)

History outcome names: what happened to the notification.

View Source
const (
	DefaultLogRuleEvalEvery = time.Minute
	DefaultLogRuleMaxWindow = time.Hour
	DefaultLogRuleMaxRows   = 5000
	DefaultLogRuleSamples   = 5
)

Env vars and defaults bounding log rule evaluation.

View Source
const (
	ScopeAll  = "all"
	ScopeApp  = "app"
	ScopeNode = "node"
)

Maintenance window scopes.

View Source
const (
	DefaultNodeCertWarning  = 21 * 24 * time.Hour
	DefaultNodeCertCritical = 7 * 24 * time.Hour
)

Defaults for the node certificate thresholds. Healthy agents renew with a third of a 90 day lifetime left, so a certificate inside 21 days means renewal has been failing for over a week.

View Source
const (
	DefaultFlapThreshold    = 5
	DefaultFlapWindow       = 30 * time.Minute
	DefaultRateLimit        = 30
	DefaultRateWindow       = 10 * time.Minute
	DefaultHistoryRetention = 30 * 24 * time.Hour
)

Defaults for NoiseConfig, overridable per environment in cmd/levelrail.

View Source
const (
	SeverityInfo     = "info"
	SeverityWarning  = "warning"
	SeverityCritical = "critical"
)

Rule severities, used by silence matchers and shown in history.

View Source
const (
	SilencePending = "pending"
	SilenceActive  = "active"
	SilenceExpired = "expired"
)

Silence status values.

View Source
const (
	SLOAvailability = "availability"
	SLOLatency      = "latency"
)

SLO objective kinds.

View Source
const (
	DefaultSLOWindow      = 30 * 24 * time.Hour
	DefaultSLOMinRequests = 10
	DefaultSLOEvalEvery   = time.Minute
)

Defaults follow the Google SRE workbook multiwindow multi-burn-rate table.

View Source
const (
	DefaultCertExpiringDays         = 14
	DefaultDomainNotResolvingWindow = 30 * time.Minute
)

Defaults for the traffic kinds; a rule's own fields override them.

View Source
const DefaultBackupMissingGracePeriod = 6 * time.Hour

DefaultBackupMissingGracePeriod is how long a database's or service volume's last successful backup can trail its schedule's own expected interval before EvaluateBackupMissing treats it as overdue rather than evaluation-tick jitter, when a rule's own ForDuration (reused as this grace period, see Rule's own doc comment) is unset. Matches DefaultCertRenewalStalledThreshold's own value: a scheduled backup, like a certificate renewal, should already be long done by the time its own expected interval elapses, so several hours of slack past that is a genuine anomaly, not noise.

View Source
const DefaultCertExpiryWarningWindow = 14 * 24 * time.Hour

DefaultCertExpiryWarningWindow is how far ahead of a certificate's NotAfter ListCertificates starts reporting "expiring_soon" instead of "healthy" when no override is configured. Also GET /api/v1/certificates' own default (api.Router.certExpiryWarningWindow), so the dashboard's TLS card and a kind=cert_expiry rule always agree on when a certificate first becomes a concern.

View Source
const DefaultCertRenewalStalledThreshold = 6 * time.Hour

DefaultCertRenewalStalledThreshold is how long a certificate can sit in "expiring_soon" or "expired" with an unchanged NotAfter before EvaluateCertExpiry treats it as a stronger "renewal appears stalled" signal rather than a plain expiry warning. Caddy's own ACME renewal starts at roughly a third of a certificate's remaining lifetime (~30 days out for a 90-day Let's Encrypt cert), well before the 14-day warning window even opens, so a real renewal attempt should already be long done by the time a certificate enters that window; several hours of the window elapsing with zero movement on NotAfter is a genuine anomaly, not just evaluation-tick jitter.

View Source
const DefaultControlPlaneBackupMaxAge = 3 * 24 * time.Hour

DefaultControlPlaneBackupMaxAge matches the doctor check's warning age.

View Source
const DefaultDomainHealthCheckInterval = 5 * time.Minute

DefaultDomainHealthCheckInterval is how often a kind=domain_health rule performs a real DNS check, independent of Engine's own tick cadence, when no override is configured. The failure mode this rule exists to catch (a CNAME silently repointed away) unfolds over months, so checking far more often than Engine's 30s tick buys nothing but outbound DNS traffic; five minutes still catches drift long before "six months later."

View Source
const DefaultNodeCPUThresholdPercent = 80.0

DefaultNodeCPUThresholdPercent is how much summed CPU a node's placed containers can use before a kind=node_resource_usage rule fires on its CPU signal, when no override is configured. cpu_percent (collector.go's sampleValues) is already expressed on the same 0-100 scale nodeSummableMetrics (internal/api/node_metrics.go) sums across containers for the dashboard, so this threshold compares directly against that same already-user-facing number.

View Source
const DefaultNodeDiskSpaceThresholdPercent = 90.0

DefaultNodeDiskSpaceThresholdPercent is how full (percent of disk used) a node can get before a kind=node_disk_space rule fires, when no override is configured. HostDiskCollector only ever reports total and used bytes, so percent-used is the natural signal to threshold on (unlike doctorCheckDiskSpace's fixed free-byte floor, which does not scale across wildly different disk sizes).

View Source
const DefaultNodeMemoryThresholdBytes = 4 * 1024 * 1024 * 1024

DefaultNodeMemoryThresholdBytes is how much summed memory a node's placed containers can use before a kind=node_resource_usage rule fires on its memory signal, when no override is configured. Unlike disk (HostDiskCollector) there is no real host memory total anywhere in this codebase to divide by: memory_limit_bytes is deliberately excluded from nodeSummableMetrics because an unconstrained container's reported limit approximates host total memory, not a real per-container cap, so summing it would overcount rather than give an honest denominator (see nodeSummableMetrics' own doc comment). An absolute-bytes floor is the only honest signal available today; 4 GiB is a generic starting point operators are expected to tune to their own node capacity via APP_ALERT_NODE_MEMORY_THRESHOLD_BYTES.

View Source
const DefaultPatchStatusThreshold = 1

DefaultPatchStatusThreshold is how many pending security patches a node can have before a kind=patch_status rule fires, when no override is configured. hostpatch.go only ever reports a point-in-time count, never a per-package age, so count is the only signal this kind can evaluate; 1 means "any security patch pending at all," the same zero-tolerance default GET /api/v1/nodes/{id}/patch-status's dashboard card already implies by showing Security > 0 as a concern.

View Source
const EventSLOBurnWouldRollback = "slo_burn_would_rollback"

EventSLOBurnWouldRollback is dry-run mode's history event name, distinct from EventFired/EventResolved (a notification transition): this records that MaybeAutoRollbackOnSLOBurn evaluated a firing rule and would have rolled back in auto mode, without writing desired state.

View Source
const MaxMaintenanceDuration = 30 * 24 * time.Hour

MaxMaintenanceDuration bounds a single window so a typo cannot silence alerts for months.

Variables

View Source
var ErrDeployTargetNotFound = errors.New("alerting: deploy target not found")

ErrDeployTargetNotFound is returned by GetDeployTarget when no target has that ID.

View Source
var ErrInteractiveApprovalUnsupported = fmt.Errorf("alerting: send approval request: channel kind does not support interactive approval")

ErrInteractiveApprovalUnsupported is returned by SendApprovalRequest for any channel kind other than Slack or Discord: InteractiveApprovals never applies to one (see notificationChannelsSupportingInteractiveApprovals in internal/api/notification_channels.go, the matching validation at save time), so reaching this in practice means a row was written before that validation existed.

View Source
var ErrMaintenanceWindowNotFound = errors.New("alerting: maintenance window not found")

ErrMaintenanceWindowNotFound is returned when no window has that ID.

View Source
var ErrNotificationChannelNotFound = errors.New("alerting: notification channel not found")

ErrNotificationChannelNotFound is returned by GetNotificationChannel and DeleteNotificationChannel when id doesn't match any row.

View Source
var ErrRuleNotFound = errors.New("alerting: rule not found")

ErrRuleNotFound is returned by GetRule when no rule has that ID.

View Source
var ErrSilenceNotFound = errors.New("alerting: silence not found")

ErrSilenceNotFound is returned when no silence has that ID.

Functions

func BackupOverdue added in v0.2.0

func BackupOverdue(history []store.BackupHistory, schedule string, grace time.Duration, now time.Time) (overdue bool, anchor time.Time, anchorIsSuccess bool, err error)

BackupOverdue reports whether the newest history (newest first, non-empty) trails its cron schedule by more than one expected interval plus grace. A target that never succeeded anchors on its oldest attempt.

func BudgetRemaining added in v0.2.0

func BudgetRemaining(bad, total, target float64) float64

BudgetRemaining is the fraction of the error budget left (1 untouched, 0 spent, negative overspent). No traffic leaves it untouched.

func BurnRate added in v0.2.0

func BurnRate(bad, total, target float64) float64

BurnRate is how many times faster than sustainable the error budget is being spent: the observed bad ratio divided by the allowed one. No traffic burns nothing.

func CertExpiryStatus

func CertExpiryStatus(notAfter, now time.Time, warningWindow time.Duration) string

CertExpiryStatus buckets notAfter, relative to now, into the three states a certificate can be in: "expired" once notAfter has passed or is passing this instant, "expiring_soon" within warningWindow of it, "healthy" otherwise. A notAfter exactly warningWindow away is still "healthy": the window is when to start warning, not an inclusive boundary.

func CertRenewalStates added in v0.2.0

func CertRenewalStates(infos []CertInfo, obs []CertExpiryObservation, threshold time.Duration, now time.Time) map[string]string

CertRenewalStates maps each certificate's domain to CertRenewalOK or CertRenewalStalled. An expired certificate is always stalled (a renewal would have replaced it). An expiring_soon one is stalled once an observation shows the same NotAfter unchanged for at least threshold, the same test EvaluateCertExpiry applies.

func DeleteCertificateForDomain added in v0.2.0

func DeleteCertificateForDomain(ctx context.Context, source CertSource, deleter CertDeleter, domain string) (found bool, err error)

DeleteCertificateForDomain removes every stored certmagic entry matching domain (by CommonName or SAN), so the next reconcile pass finds none and re-issues a fresh one. found is false, nil when nothing matched: a safe no-op, not an error, since reconcile re-attempts issuance for every configured subject regardless.

func EvaluateLogRule added in v0.2.0

func EvaluateLogRule(ctx context.Context, src LogRuleSource, r Rule, policy LogRulePolicy, now time.Time) (Rule, LogRuleResult, error)

EvaluateLogRule runs one log_pattern or log_level_rate rule: it reads at most policy.MaxRows of the newest lines in the window, matches them and advances the rule's state.

func IsPlatformKind added in v0.2.0

func IsPlatformKind(k Kind) bool

IsPlatformKind reports whether a rule kind watches the whole control plane rather than one app.

func IsTrafficKind added in v0.2.0

func IsTrafficKind(k Kind) bool

IsTrafficKind reports whether k is one of the traffic kinds above.

func MaybeAutoRollback added in v0.2.0

func MaybeAutoRollback(ctx context.Context, st AutoRollbackStore, nudger deploy.ReconcileNudger, tracker *AutoRollbackTracker, resourceID string, logger *slog.Logger)

MaybeAutoRollback checks whether resourceID's app has opted into automatic crashloop rollback (store.DesiredService. AutoRollbackOnCrashloop) and, if so, triggers a deploy back to the most recent different successful image via deploy.TriggerImageDeploy, the identical path a manual "Rollback to this build" action already uses (see that function's own doc comment). Called both on a KindCrashloop rule's pending-to-firing transition (Engine.Tick's becameFiring) and on every tick a rule stays continuously firing (Engine.Tick's stillFiring, gated on tracker.armed so it only costs a store round trip for a service that has already auto-rolled-back at least once): tracker's alreadyHandled check below is what still prevents firing twice for the same incident in both cases, a rule staying Firing is no longer, by itself, proof that nothing new has happened, since a second bad deploy inside the same RestartWindow never produces a fresh transition.

Failures and "nothing to do" cases are logged, never returned: a crashloop rule's own notification already fired regardless of what happens here, and an automatic safety net that panics or blocks evaluation of the next rule would be worse than one that occasionally leaves a crashloop to alert-only.

func MaybeAutoRollbackOnSLOBurn added in v0.2.0

func MaybeAutoRollbackOnSLOBurn(ctx context.Context, st SLOAutoRollbackStore, history SLOBurnHistoryRecorder, nudger deploy.ReconcileNudger, tracker *AutoRollbackTracker, r Rule, notice string, logger *slog.Logger)

MaybeAutoRollbackOnSLOBurn checks whether r's app has opted into SLO burn auto-rollback (store.DesiredService.AutoRollbackOnSLOBurn) and, if so, acts according to its configured mode:

  • store.AutoRollbackSLOBurnAuto: rolls back immediately via deploy.TriggerImageDeploy, the identical path MaybeAutoRollback (crashloop.go) already uses for a crashloop.
  • store.AutoRollbackSLOBurnDryRun: never deploys, only records a "would have rolled back" history event.
  • store.AutoRollbackSLOBurnPauseForHuman: never deploys directly, opens a pending store.DeployApproval for the rollback image instead, so a human approves it through the existing deploy- approvals flow (internal/api/deploy_approvals.go) exactly like any other gated deploy.

Called both on a KindSLOBurn rule's pending-to-firing transition and on every tick it stays firing (Engine.Tick, mirroring MaybeAutoRollback's own becameFiring/stillFiring wiring there): tracker's alreadyHandled check is what prevents repeating the same action for the same incident in both cases. auto mode records the rollback *target* image (current desired state converges to it, the same dedup MaybeAutoRollback already relies on); dry_run and pause_for_human record the *current* (bad) image instead, since neither one changes desired state, so "nothing has changed since" is what "already handled" has to mean for them.

Failures and "nothing to do" cases are logged, never returned: this runs as a side effect of Engine.Tick evaluating one rule among many, see MaybeAutoRollback's own doc comment for why that call must never block or fail evaluation of the rest.

func NewDeployTargetID

func NewDeployTargetID() (string, error)

NewDeployTargetID generates a random, URL-safe deploy-target identifier, the same shape NewRuleID already mints for alert_rules: exported so internal/api can assign one when creating a target from a request body that doesn't include one, the identical "ID is never caller-chosen data" reasoning NewRuleID's own doc comment gives.

func NewHistoryID added in v0.2.0

func NewHistoryID() (string, error)

NewHistoryID generates a random history identifier.

func NewMaintenanceWindowID added in v0.2.0

func NewMaintenanceWindowID() (string, error)

NewMaintenanceWindowID generates a random window identifier.

func NewNotificationChannelID

func NewNotificationChannelID() (string, error)

NewNotificationChannelID mirrors NewDeployTargetID's exact shape.

func NewNotificationDeliveryID

func NewNotificationDeliveryID() (string, error)

NewNotificationDeliveryID mirrors NewNotificationChannelID's exact shape.

func NewRuleID

func NewRuleID() (string, error)

NewRuleID generates a random, URL-safe rule identifier. Exported so internal/api can assign one when creating a rule from a request body that doesn't include one, matching how a rule's ID is never caller-chosen data (avoids a client picking a colliding or predictable ID).

func NewSilenceID added in v0.2.0

func NewSilenceID() (string, error)

NewSilenceID generates a random silence identifier.

Types

type ACMEFailureSource added in v0.2.0

type ACMEFailureSource interface {
	ACMEFailing(domain string) (errText string, failing bool)
}

ACMEFailureSource reports whether the CA's last attempt for a domain failed.

func NewSuppressedACMESource added in v0.2.0

func NewSuppressedACMESource(inner ACMEFailureSource, suppress CertSuppressor) ACMEFailureSource

NewSuppressedACMESource reports no failure for hostnames suppress names, so a stale attempt made before a proxy took over TLS never raises an alert.

type AlertContext added in v0.2.0

type AlertContext struct {
	RuleID   string
	Kind     string
	App      string
	NodeID   string
	NodeName string
	Labels   map[string]string
	Severity string
}

AlertContext is what a silence matcher or maintenance window is evaluated against: the rule plus where its app is placed.

type AppDomainSource

type AppDomainSource interface {
	GetDesiredService(ctx context.Context, name string) (*store.DesiredService, error)
}

AppDomainSource is the narrow store surface EvaluateDomainHealth needs: which domains a domain_health rule's own app currently has configured. *store.DB satisfies this structurally.

type ApprovalRequestInfo added in v0.2.0

type ApprovalRequestInfo struct {
	ApprovalID  string
	ServiceName string
	// Action is one of store.DeployApprovalAction* ("deploy" or
	// "promote"), duplicated here as a plain string so this package
	// doesn't need to import internal/store just for two constants.
	Action        string
	Image         string
	RequestedBy   string
	EnvironmentID string
}

ApprovalRequestInfo is what SendApprovalRequest needs to render an interactive message: just enough to identify the gated change and let an approver decide without opening the dashboard. ApprovalID is the value every button carries back on click (deploy_approval.go's own opaque ID), never anything an approver would need to trust blind.

type AutoRollbackStore added in v0.2.0

type AutoRollbackStore interface {
	deploy.ImageDeployStore
	GetDesiredService(ctx context.Context, name string) (*store.DesiredService, error)
	ListDeployAttempts(ctx context.Context, serviceName string) ([]store.DeployAttempt, error)
}

AutoRollbackStore is the narrow store surface MaybeAutoRollback needs: read a service's current desired state (to check the opt-in flag and current image) and its deploy history (to find a prior known-good tag), plus deploy.ImageDeployStore's write surface to actually trigger the rollback through the same path a manual one uses. *store.DB satisfies this structurally.

type AutoRollbackTracker added in v0.2.0

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

AutoRollbackTracker records, per resourceID, the image MaybeAutoRollback most recently rolled a service's desired state back to. A KindCrashloop rule's own Firing bool is *not* enough of a debounce on its own: once a rollback fixes one bad deploy, the rule can stay continuously Firing (never re-transitioning through becameFiring) simply because the prior episode's restarts are still inside RestartWindow, even after a second, genuinely different bad image starts crash-looping. This tracker gives MaybeAutoRollback a second, independent signal for that case: if the service's current desired image no longer matches the image it was last rolled back to, something changed the desired state since (a new deploy), so it is safe, and necessary, to roll back again. If the current image still matches, nothing has changed since the last rollback, the ongoing Firing state is still the same incident, and MaybeAutoRollback must not fire again.

In-memory only, the same shape as RestartTracker above: a control-plane restart already zeroes RestartTracker's own counts, which independently forces every crashloop rule back through a fresh pending-to-firing transition before it can fire at all, so losing this map's entries on restart never produces a wrong rollback decision, at worst a rule revisits becameFiring once more before this fast path resumes tracking it.

func NewAutoRollbackTracker added in v0.2.0

func NewAutoRollbackTracker() *AutoRollbackTracker

NewAutoRollbackTracker builds an empty AutoRollbackTracker.

type BackupSource added in v0.2.0

type BackupSource interface {
	GetDesiredDatabase(ctx context.Context, name string) (*store.DesiredDatabase, error)
	ListBackupHistory(ctx context.Context, databaseName string, limit int, before *time.Time) ([]store.BackupHistory, error)
	GetServiceVolumeBackupSchedule(ctx context.Context, serviceName, volumeName string) (store.ServiceVolumeBackupConfig, error)
	ListServiceVolumeBackupHistory(ctx context.Context, serviceName, volumeName string, limit int, before *time.Time) ([]store.BackupHistory, error)
}

BackupSource is the narrow store surface a backup-missing evaluation needs: the current schedule config for a database or service volume, and enough recent history to find its last successful run. *store.DB satisfies this structurally.

type BudgetSource added in v0.2.0

type BudgetSource interface {
	BudgetStatuses(ctx context.Context) ([]usagecost.BudgetStatus, error)
}

BudgetSource reports each budget's month to date position. The api Router satisfies it.

type BurnTier added in v0.2.0

type BurnTier struct {
	Name   string        `json:"name"`
	Factor float64       `json:"factor"`
	Long   time.Duration `json:"long"`
	Short  time.Duration `json:"short"`
	Page   bool          `json:"page"`
}

BurnTier fires when the burn rate is at least Factor over both Long and Short. The short window makes the alert reset quickly once the burn stops.

type BurnWindowStat added in v0.2.0

type BurnWindowStat struct {
	Window   time.Duration `json:"window"`
	Requests float64       `json:"requests"`
	Bad      float64       `json:"bad"`
	Burn     float64       `json:"burn_rate"`
}

BurnWindowStat is the observed burn over one window.

type CertDeleter added in v0.2.0

type CertDeleter interface {
	DeleteCertStorageValue(ctx context.Context, key string) error
}

CertDeleter is the narrow storage surface DeleteCertificateForDomain needs beyond CertSource's two read methods: *store.DB satisfies this structurally, same as CertSource.

type CertExpiryObservation

type CertExpiryObservation struct {
	RuleID           string
	Domain           string
	Status           string
	NotAfter         time.Time
	EpisodeNotAfter  time.Time
	EpisodeStartedAt time.Time
	ObservedAt       time.Time
}

CertExpiryObservation is what EvaluateCertExpiry persists per (rule, domain) after every tick, so the next tick can tell "still stuck in the same unrenewed episode" apart from "just entered a fresh expiry warning because a real renewal landed." See EvaluateCertExpiry's own doc comment for how EpisodeNotAfter/EpisodeStartedAt are used.

type CertExpiryObservationStore

type CertExpiryObservationStore interface {
	UpsertCertExpiryObservation(ctx context.Context, o CertExpiryObservation) error
	ListCertExpiryObservations(ctx context.Context, ruleID string) ([]CertExpiryObservation, error)
}

CertExpiryObservationStore is the narrow store surface EvaluateCertExpiry needs to remember what it saw last tick, per domain. *DB satisfies this structurally.

type CertInfo

type CertInfo struct {
	Domain    string
	SANs      []string
	Issuer    string
	NotBefore time.Time
	NotAfter  time.Time
	// Status is "healthy", "expiring_soon", or "expired"; see
	// CertExpiryStatus.
	Status string
}

CertInfo is one stored certificate's expiry-relevant fields, the single computation both GET /api/v1/certificates (internal/api) and a kind=cert_expiry alert rule read via ListCertificates, so the two can never silently disagree about a certificate's status.

func ListCertificates

func ListCertificates(ctx context.Context, source CertSource, warningWindow time.Duration, now time.Time, logger *slog.Logger) ([]CertInfo, error)

ListCertificates returns every certificate currently in source, parsed from its own stored PEM bytes, sorted by domain. A control plane that has never issued a certificate returns an empty slice, not an error. A single malformed or since-deleted entry is skipped and logged, never fails the whole list, the same "one broken resource must not block the rest" shape used throughout this codebase.

type CertSource

type CertSource interface {
	ListCertStorageKeys(ctx context.Context, prefix string, recursive bool) ([]string, error)
	GetCertStorageValue(ctx context.Context, key string) (*store.CertStorageValue, error)
}

CertSource is the narrow storage surface a cert-expiry evaluation needs: the same two-method shape internal/api.CertStore already defines. Duplicated here rather than imported because internal/api imports internal/alerting (for AlertRules et al.), so the reverse import would cycle; *store.DB satisfies both interfaces structurally.

func NewSuppressedCertSource added in v0.2.0

func NewSuppressedCertSource(inner CertSource, suppress CertSuppressor) CertSource

NewSuppressedCertSource hides stored certificates whose hostname suppress reports, so cert expiry and renewal rules never fire on a certificate visitors do not receive. A nil suppress returns inner unchanged.

type CertSuppressor added in v0.2.0

type CertSuppressor func(ctx context.Context) func(domain string) bool

CertSuppressor returns, for one evaluation, whether a stored certificate's hostname must be ignored because a proxy terminates its TLS.

type ChangeSource added in v0.2.0

type ChangeSource interface {
	Collect(ctx context.Context, app string, until time.Time) changes.Result
}

ChangeSource reports what changed on an app before an alert fired. *changes.Aggregator satisfies it.

type Comparator

type Comparator string

Comparator is how a threshold Rule compares the latest sample value against its Threshold.

const (
	GreaterThan    Comparator = ">"
	LessThan       Comparator = "<"
	GreaterOrEqual Comparator = ">="
	LessOrEqual    Comparator = "<="
)

The four comparators a threshold Rule can use.

type ControlPlaneBackupSource added in v0.2.0

type ControlPlaneBackupSource interface {
	Newest() (time.Time, bool, error)
}

ControlPlaneBackupSource reports when the newest control plane snapshot was taken.

type ControlPlaneDRSource added in v0.2.0

type ControlPlaneDRSource interface {
	DRProblem(now time.Time) string
}

ControlPlaneDRSource is an optional extension of ControlPlaneBackupSource: DRProblem describes a failed or overdue off-box backup or restore drill, or returns "" when healthy.

type DB

type DB struct {
	*sql.DB
}

DB wraps a *sql.DB opened against alerting.db, a dedicated SQLite file separate from both internal/store's levelrail.db and internal/telemetry's telemetry.db, the same write-isolation reasoning ADR 009 already applied: alert rule writes (evaluation state updates on every tick) are a different, independent write pattern from either desired-state config or metric/log ingestion.

func Open

func Open(ctx context.Context, path string) (*DB, error)

Open opens (creating if needed) the SQLite database at path, applies pragmas, and runs every pending migration, matching internal/store and internal/telemetry's own Open functions exactly.

func (*DB) CreateSilence added in v0.2.0

func (db *DB) CreateSilence(ctx context.Context, s Silence) error

CreateSilence inserts a new silence. Never updates in place: expiring early is ExpireSilence, so history stays intact.

func (*DB) DeleteDeployTarget

func (db *DB) DeleteDeployTarget(ctx context.Context, id string) error

DeleteDeployTarget removes a deploy target by ID. Deleting a target that doesn't exist is not an error, the same idempotent-delete convention DeleteRule already follows.

func (*DB) DeleteMaintenanceWindow added in v0.2.0

func (db *DB) DeleteMaintenanceWindow(ctx context.Context, id string) error

DeleteMaintenanceWindow removes a window; deleting a missing one is a no-op.

func (*DB) DeleteNotificationChannel

func (db *DB) DeleteNotificationChannel(ctx context.Context, id string) error

DeleteNotificationChannel removes a channel row. Attached deploy_notify_targets rows have channel_id cleared automatically by migrations/0004's ON DELETE SET NULL, never blocking this delete.

func (*DB) DeleteRule

func (db *DB) DeleteRule(ctx context.Context, id string) error

DeleteRule removes a rule by ID. Deleting a rule that doesn't exist is not an error, matching internal/store.DeleteDesiredService's own idempotent-delete convention.

func (*DB) ExpireSilence added in v0.2.0

func (db *DB) ExpireSilence(ctx context.Context, id string, now time.Time) (*Silence, error)

ExpireSilence ends a silence at now. Expiring an already-expired silence is a no-op; the row is kept for history.

func (*DB) GetDeployTarget

func (db *DB) GetDeployTarget(ctx context.Context, id string) (*DeployTarget, error)

GetDeployTarget returns the deploy target with this ID, or ErrDeployTargetNotFound.

func (*DB) GetMaintenanceWindow added in v0.2.0

func (db *DB) GetMaintenanceWindow(ctx context.Context, id string) (*MaintenanceWindow, error)

GetMaintenanceWindow returns one window or ErrMaintenanceWindowNotFound.

func (*DB) GetNotificationChannel

func (db *DB) GetNotificationChannel(ctx context.Context, id string) (*NotificationChannel, error)

GetNotificationChannel returns the channel with this ID, or ErrNotificationChannelNotFound.

func (*DB) GetRule

func (db *DB) GetRule(ctx context.Context, id string) (*Rule, error)

GetRule returns the rule with this ID, or ErrRuleNotFound.

func (*DB) GetSilence added in v0.2.0

func (db *DB) GetSilence(ctx context.Context, id string) (*Silence, error)

GetSilence returns one silence or ErrSilenceNotFound.

func (*DB) ListActiveSilences added in v0.2.0

func (db *DB) ListActiveSilences(ctx context.Context, now time.Time) ([]Silence, error)

ListActiveSilences returns only the silences applying at now.

func (*DB) ListAllCertExpiryObservations added in v0.2.0

func (db *DB) ListAllCertExpiryObservations(ctx context.Context) ([]CertExpiryObservation, error)

ListAllCertExpiryObservations returns every observation row across all rules, so a reader with no rule in hand can see episodes any kind=cert_expiry rule has recorded.

func (*DB) ListCertExpiryObservations

func (db *DB) ListCertExpiryObservations(ctx context.Context, ruleID string) ([]CertExpiryObservation, error)

ListCertExpiryObservations returns every observation row for ruleID, one per domain, unordered.

func (*DB) ListDeployTargetsForResource

func (db *DB) ListDeployTargetsForResource(ctx context.Context, resourceID string) ([]DeployTarget, error)

ListDeployTargetsForResource returns every deploy target scoped to resourceID, ordered by created_at, regardless of enabled state: the same "list everything, let the caller decide what to show or use" shape ListRulesForResource already establishes, used both by internal/api's CRUD handlers (list including disabled targets, so an operator can see and re-enable a paused one) and by Dispatch below (which filters to enabled targets itself).

func (*DB) ListEnabledRules

func (db *DB) ListEnabledRules(ctx context.Context) ([]Rule, error)

ListEnabledRules is what the evaluator (evaluate.go, crashloop.go) actually loops over each tick: a disabled rule is skipped entirely, not evaluated-but-not-notified, so a paused rule doesn't even pay for a metrics query.

func (*DB) ListHistory added in v0.2.0

func (db *DB) ListHistory(ctx context.Context, f HistoryFilter) ([]HistoryEntry, error)

ListHistory returns entries newest first.

func (*DB) ListMaintenanceWindows added in v0.2.0

func (db *DB) ListMaintenanceWindows(ctx context.Context) ([]MaintenanceWindow, error)

ListMaintenanceWindows returns every window ordered by name.

func (*DB) ListNotificationChannels

func (db *DB) ListNotificationChannels(ctx context.Context) ([]NotificationChannel, error)

ListNotificationChannels returns every channel, oldest first (creation order, the same order the settings page listing them wants).

func (*DB) ListNotificationDeliveries

func (db *DB) ListNotificationDeliveries(ctx context.Context, channelID string, limit int, before *time.Time) ([]NotificationDelivery, error)

ListNotificationDeliveries returns up to limit delivery records for channelID, most recent first, cursor-paginated by before (an optional exclusive created_at cutoff), the same shape store.ListAuditEntries already uses. Callers are responsible for clamping limit.

func (*DB) ListRules

func (db *DB) ListRules(ctx context.Context) ([]Rule, error)

ListRules returns every rule, ordered by name.

func (*DB) ListRulesForResource

func (db *DB) ListRulesForResource(ctx context.Context, resourceID string) ([]Rule, error)

ListRulesForResource returns every rule scoped to resourceID, ordered by name, regardless of enabled state. internal/api's alert-rule handlers use this to list only one app's own rules rather than every rule in the database; alert_rules already carries idx_alert_rules_resource (migrations/0001_alert_rules.sql) for exactly this lookup.

func (*DB) ListSilences added in v0.2.0

func (db *DB) ListSilences(ctx context.Context, now time.Time, includeExpired bool, limit int) ([]Silence, error)

ListSilences returns silences newest first, capped at limit. When includeExpired is false only pending and active ones at now are returned.

func (*DB) PruneHistory added in v0.2.0

func (db *DB) PruneHistory(ctx context.Context, cutoff time.Time) (int64, error)

PruneHistory deletes entries older than cutoff and returns how many went.

func (*DB) RecordHistory added in v0.2.0

func (db *DB) RecordHistory(ctx context.Context, e HistoryEntry) error

RecordHistory appends one entry. Insert-only.

func (*DB) RecordNotificationDelivery

func (db *DB) RecordNotificationDelivery(ctx context.Context, d NotificationDelivery) error

RecordNotificationDelivery persists one delivery attempt. Insert-only: a delivery record is never updated once written.

func (*DB) SaveDeployTarget

func (db *DB) SaveDeployTarget(ctx context.Context, t DeployTarget) error

SaveDeployTarget creates or fully replaces a deploy target's configuration, the same upsert shape SaveRule already uses.

func (*DB) SaveMaintenanceWindow added in v0.2.0

func (db *DB) SaveMaintenanceWindow(ctx context.Context, w MaintenanceWindow) error

SaveMaintenanceWindow creates or fully replaces a window.

func (*DB) SaveNotificationChannel

func (db *DB) SaveNotificationChannel(ctx context.Context, c NotificationChannel) error

SaveNotificationChannel creates or fully replaces a channel's configuration, the same upsert shape SaveDeployTarget already uses.

func (*DB) SaveRule

func (db *DB) SaveRule(ctx context.Context, r Rule) error

SaveRule creates or fully replaces a rule's configuration. Does not touch evaluation state (Firing/FiringSince/etc.): those are only ever written by UpdateState, so an operator editing a rule's threshold doesn't accidentally reset its current firing status.

func (*DB) UpdateState

func (db *DB) UpdateState(ctx context.Context, id string, pendingSince, firingSince *time.Time, firing bool, evaluatedAt time.Time, value *float64) error

UpdateState persists a rule's new evaluation state after one evaluator pass. The only method that writes Pending/Firing/ LastEvaluated/LastValue; SaveRule above never touches them.

func (*DB) UpsertCertExpiryObservation

func (db *DB) UpsertCertExpiryObservation(ctx context.Context, o CertExpiryObservation) error

UpsertCertExpiryObservation creates or replaces the (rule_id, domain) row.

type DeliveryRecorder

type DeliveryRecorder interface {
	RecordNotificationDelivery(ctx context.Context, d NotificationDelivery) error
}

DeliveryRecorder is the narrow surface Engine and DeployDispatcher need to persist a delivery attempt. *DB satisfies it structurally.

type DeployDispatcher

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

DeployDispatcher sends a deploy-outcome notification to every enabled DeployTarget scoped to one app. Built once in cmd/levelrail/main.go (mirroring how alertingNewNotifier is built once and shared by alerting.Engine) and shared by reference into internal/webhook and internal/api, the three real deploy-trigger call sites this task's own task description names.

func NewDeployDispatcher

func NewDeployDispatcher(db *DB, client *http.Client, sender email.Sender, pushSender PushSender, logger *slog.Logger) *DeployDispatcher

NewDeployDispatcher builds a DeployDispatcher. client and logger default if nil; sender may be nil (an email-kind target then fails with a clear "not configured" error), and pushSender may be nil with the identical failure shape for a webpush-kind target.

func (*DeployDispatcher) Dispatch

func (d *DeployDispatcher) Dispatch(ctx context.Context, resourceID string, ev DeployOutcome)

Dispatch sends ev to every enabled DeployTarget scoped to resourceIDForApp(ev.AppName)'s resource ID (resourceID, computed by the caller: this package has no dependency on internal/api's own resourceIDForApp helper, matching how it already has none on that package for alert rules). Must only be called once a deploy attempt has reached a terminal status (succeeded or failed), never for "running": there is nothing sensible to notify about an attempt that is still in progress, and none of the three real call sites (internal/webhook's beginDeployAttempt finish closure, internal/api/deploys.go's recordPlainDeployAttempt, internal/api/builds.go's beginBuildDeployAttempt finish closure) ever call this before FinishDeployAttempt has already persisted a terminal status.

A lookup failure or an individual target's send failure is logged, never returned: the deploy attempt this notification describes has already finished and already been persisted by the time Dispatch runs, so a notification failing to send must never be surfaced as if the deploy itself had failed, the identical "must never block the real operation" reasoning Engine.dispatch's own doc comment gives one layer up for alert rules.

func (*DeployDispatcher) SendApprovalRequest added in v0.2.0

func (d *DeployDispatcher) SendApprovalRequest(ctx context.Context, c NotificationChannel, info ApprovalRequestInfo) error

SendApprovalRequest posts one deploy-approval-requested message to c, with real Approve/Deny buttons for NotifySlack and NotifyDiscord. Records delivery history the same way Dispatch does for a deploy outcome, under trigger "deploy-approval-requested".

func (*DeployDispatcher) SendNotice added in v0.2.0

func (d *DeployDispatcher) SendNotice(ctx context.Context, kind NotifyKind, notifyURL, text string) error

SendNotice sends a plain-text notice through kind/notifyURL, using the same per-kind payload logic and SSRF-guarded client as SendTest.

func (*DeployDispatcher) SendTest

func (d *DeployDispatcher) SendTest(ctx context.Context, kind NotifyKind, notifyURL string) error

SendTest fires a real test notification through kind/notifyURL, reusing this dispatcher's own HTTP client and email sender so a passing test predicts a real deploy notification will send the same way.

func (*DeployDispatcher) WithLogSources added in v0.2.0

func (d *DeployDispatcher) WithLogSources(runtime LogsSource, build DeployLogSource) *DeployDispatcher

WithLogSources lets the dispatcher attach the tail of the failing build or container output to a failed deploy notification. Either may be nil.

type DeployLogSource added in v0.2.0

type DeployLogSource interface {
	QueryDeployLog(ctx context.Context, attemptID string) ([]telemetry.DeployLogEntry, error)
}

DeployLogSource reads a deploy attempt's build output; *telemetry.DB satisfies it.

type DeployOutcome

type DeployOutcome struct {
	// AppName is the deploy's service name (store.DeployAttempt.ServiceName),
	// used both in the message text and to look up which DeployTarget
	// rows apply (resourceIDForApp(AppName) is computed by the caller,
	// internal/api and internal/webhook, the same way it already is for
	// alert rules; this package only ever sees the resulting resource ID).
	AppName string
	// Image is the tag this attempt deployed or tried to deploy
	// (store.DeployAttempt.Image).
	Image string
	// Succeeded is store.DeployAttempt.Status == succeeded. Never called
	// for the non-terminal "running" status: see Dispatch's own doc
	// comment.
	Succeeded bool
	// Error is store.DeployAttempt.Error: only meaningful, and only ever
	// non-empty, when !Succeeded.
	Error string
	// AttemptID is the deploy attempt, used to read a failed build's output.
	AttemptID string
	// LogLines is the tail of the failing build or container output. The
	// dispatcher fills it for a failed deploy when a log source is set.
	LogLines []string
}

DeployOutcome is what Dispatch sends: one finished deploy attempt's terminal outcome, everything summaryDeployText and deployGenericPayload need to describe it. Deliberately not alerting.Event: see this file's own package-level doc comment for why forcing this through Event/Rule would be the wrong fit.

type DeployTarget

type DeployTarget struct {
	ID         string
	ResourceID string
	// ChannelID attaches an already-connected NotificationChannel; empty
	// for legacy rows, which use NotifyURL/NotifyKind below directly.
	ChannelID  string
	NotifyURL  string
	NotifyKind NotifyKind
	Enabled    bool
}

DeployTarget is a notify destination for deploy-outcome events, scoped to one app (ResourceID, matching Rule.ResourceID's own "service:<name>" convention) via internal/api's own resourceIDForApp. Deliberately much narrower than Rule: no metric/comparator/threshold, no restart-count/window, no evaluation state (pending_since/firing/firing_since/last_evaluated_at/last_value), because none of that describes anything about "where to send a deploy's outcome."

type DomainCheckSource

type DomainCheckSource interface {
	CheckDomainStatus(ctx context.Context, domain string) (status string, err error)
}

DomainCheckSource is the narrow surface EvaluateDomainHealth needs: the same DNS check GET /api/v1/apps/{name}/domains/{domain}/check itself runs (internal/api's runDomainCheck), reused through this interface rather than reimplemented here, so the dashboard's "Check now" button and a domain_health rule can never silently disagree on what "connected" means. *api.Router satisfies this via CheckDomainStatus.

type DomainListSource added in v0.2.0

type DomainListSource interface {
	ListServiceDomains(ctx context.Context) ([]store.ServiceDomain, error)
}

DomainListSource lists every configured domain. *store.DB satisfies it.

type Engine

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

Engine evaluates every enabled rule on an interval, persists each rule's updated state, and notifies only on a firing/resolved transition, never on every tick a rule stays in the same state (the same reasoning already documented on Event.Resolved: repeated identical notifications train an operator to ignore the channel).

func NewEngine

func NewEngine(rules RuleStore, metrics MetricsSource, logs LogsSource, tracker *RestartTracker, certs CertSource, scheduledTasks ScheduledTaskSource, certExpiryWarningWindow, certRenewalStalledThreshold time.Duration, nodes NodeSource, patchStatusThreshold, nodeDiskSpaceThreshold float64, nodeServices NodeServiceSource, nodeCPUThreshold, nodeMemoryThreshold float64, domainApps AppDomainSource, domainChecker DomainCheckSource, domainHealthCheckInterval time.Duration, backups BackupSource, backupMissingGracePeriod time.Duration, newNotifier func(Rule) Notifier, logger *slog.Logger) *Engine

NewEngine builds an Engine. newNotifier defaults to a Notifier with no email capability configured if nil; a real caller passes a closure capturing an email.Sender instead. certs may be nil if no cert storage is configured; a kind=cert_expiry rule then logs a warning and is skipped each tick rather than evaluated. nodes and scheduledTasks may likewise be nil, in which case a kind=patch_status or kind=scheduled_task_failure rule is skipped the same way. certExpiryWarningWindow and certRenewalStalledThreshold fall back to DefaultCertExpiryWarningWindow/DefaultCertRenewalStalledThreshold when passed as 0; patchStatusThreshold falls back to DefaultPatchStatusThreshold, nodeDiskSpaceThreshold falls back to DefaultNodeDiskSpaceThresholdPercent, and nodeCPUThreshold/ nodeMemoryThreshold fall back to DefaultNodeCPUThresholdPercent/ DefaultNodeMemoryThresholdBytes, all the same way. nodeServices may be nil, in which case a kind=node_resource_usage rule is skipped the same way a kind=patch_status rule is when nodes is nil. domainApps and domainChecker may likewise be nil, in which case a kind=domain_health rule is skipped the same way; domainHealthCheckInterval falls back to DefaultDomainHealthCheckInterval when passed as 0. backups may likewise be nil, in which case a kind=backup_missing rule is skipped the same way; backupMissingGracePeriod falls back to DefaultBackupMissingGracePeriod when passed as 0.

func (*Engine) EvaluateCertRenewalStalled added in v0.2.0

func (e *Engine) EvaluateCertRenewalStalled(ctx context.Context, r Rule, now time.Time) (Rule, []string, error)

EvaluateCertRenewalStalled runs one KindCertRenewalStalled rule. It keeps its own expiry episodes (keyed by the rule ID) through EvaluateCertExpiry.

func (*Engine) EvaluateDomainNotResolving added in v0.2.0

func (e *Engine) EvaluateDomainNotResolving(ctx context.Context, r Rule, now time.Time) (Rule, []string, error)

EvaluateDomainNotResolving runs one KindDomainNotResolving rule.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context, interval time.Duration) error

Run calls Tick on interval until ctx is done, matching the shape of every other periodic loop in this codebase (telemetry.Collector.Run, cmd/levelrail's retention sweeps).

func (*Engine) SetAutoRollback added in v0.2.0

func (e *Engine) SetAutoRollback(st AutoRollbackStore, nudger deploy.ReconcileNudger)

SetAutoRollback wires crashloop auto-rollback into e: once set, a KindCrashloop rule that transitions to firing, or stays firing across a desired-image change, checks its app's own AutoRollbackOnCrashloop opt-in and, if set, rolls back automatically (MaybeAutoRollback, crashloop.go). A setter rather than a NewEngine parameter deliberately: this keeps every existing call site and test unaffected by an optional dependency most callers don't need, the same "nil until explicitly wired" shape st/nudger already have inside MaybeAutoRollback itself.

func (*Engine) SetBudgets added in v0.2.0

func (e *Engine) SetBudgets(src BudgetSource)

SetBudgets enables kind=budget_threshold rules.

func (*Engine) SetChanges added in v0.2.0

func (e *Engine) SetChanges(src ChangeSource, link func(app string) string)

SetChanges attaches a "recent changes" section to firing app alerts. link, when non-nil, maps an app name to its dashboard URL.

func (*Engine) SetControlPlaneBackups added in v0.2.0

func (e *Engine) SetControlPlaneBackups(src ControlPlaneBackupSource, maxAge time.Duration)

SetControlPlaneBackups enables kind=control_plane_backup_stale rules. It is left unset when scheduled control plane backups are disabled, so those rules stay quiet. maxAge <= 0 falls back to DefaultControlPlaneBackupMaxAge.

func (*Engine) SetLogArchive added in v0.2.0

func (e *Engine) SetLogArchive(src LogArchiveSource)

SetLogArchive enables kind=log_archive_stale rules.

func (*Engine) SetNodeCertThresholds added in v0.2.0

func (e *Engine) SetNodeCertThresholds(warning, critical time.Duration)

SetNodeCertThresholds sets the default warning window and critical threshold for kind=node_cert_expiring rules. Zero keeps the defaults.

func (*Engine) SetNoiseControl added in v0.2.0

func (e *Engine) SetNoiseControl(n *NoiseControl)

SetNoiseControl enables silences, grouping, flapping control and alert history. Unset, every transition notifies directly.

func (*Engine) SetRestoreDrills added in v0.2.0

func (e *Engine) SetRestoreDrills(src RestoreDrillSource)

SetRestoreDrills enables kind=restore_drill_failed rules.

func (*Engine) SetSLOBurnAutoRollback added in v0.2.0

func (e *Engine) SetSLOBurnAutoRollback(st SLOAutoRollbackStore, nudger deploy.ReconcileNudger, history SLOBurnHistoryRecorder)

SetSLOBurnAutoRollback wires SLO-burn auto-rollback into e: once set, a KindSLOBurn rule that transitions to firing, or stays firing across a desired-image change, checks its app's own AutoRollbackOnSLOBurn mode and acts accordingly (MaybeAutoRollbackOnSLOBurn, slo_burn_rollback.go). A setter rather than a NewEngine parameter, and history a separate argument from st, for the same reasons SetAutoRollback already documents plus one more: st and history are always two different *DB instances in this codebase (store.DB vs alerting.DB, see alerting.DB's own doc comment on why they're separate databases).

func (*Engine) SetTrafficSources added in v0.2.0

func (e *Engine) SetTrafficSources(domains DomainListSource, acme ACMEFailureSource)

SetTrafficSources enables domain_not_resolving and the ACME half of cert_renewal_stalled. Either may be nil.

func (*Engine) SetVersionSkew added in v0.2.0

func (e *Engine) SetVersionSkew(settings VersionSkewSettingsSource, fetchers upgrade.Fetchers)

SetVersionSkew enables kind=version_skew rules: settings supplies the configured update channel, fetchers is the shared GitHub lookup (kit/upgrade), the same one GET /api/v1/updates and internal/updatecheck.Scheduler both use.

func (*Engine) Tick

func (e *Engine) Tick(ctx context.Context) error

Tick evaluates every enabled rule once. Errors from individual rules (a metrics query failing, a notification failing to send) are collected and joined, never stopping evaluation of the remaining rules: the same "one broken resource must not block the rest" principle reconcile.Engine.ReconcileAll already applies to controllers, applied here to alert rules.

type Event

type Event struct {
	Rule Rule
	// Resolved is true when this event is "the rule stopped firing,"
	// false when it's "the rule started firing." Sending a resolved
	// notification (not just a firing one) is deliberate: an operator
	// who only ever hears about problems starting, never ending, learns
	// to distrust the channel or mute it, exactly the alert-fatigue
	// failure mode a useful alerting feature has to avoid.
	Resolved bool
	// LogLines is populated only for a firing (not resolved) crashloop
	// event: the last up-to-200 lines of the failing container's logs.
	// Nil for threshold rules and for resolved events.
	LogLines []string
	// CertNotices is populated only for a firing (not resolved)
	// cert_expiry event: one line per non-healthy certificate, from
	// EvaluateCertExpiry, flagging a stalled-looking renewal specially.
	// Nil for every other rule kind and for resolved events.
	CertNotices []string
	// PatchNotices is populated only for a firing (not resolved)
	// patch_status event: one line per node over its security-patch
	// threshold, from EvaluatePatchStatus. Nil for every other rule kind
	// and for resolved events.
	PatchNotices []string
	// DiskSpaceNotices is populated only for a firing (not resolved)
	// node_disk_space event: one line per node over its disk-usage
	// threshold, from EvaluateNodeDiskSpace. Nil for every other rule
	// kind and for resolved events.
	DiskSpaceNotices []string
	// NodeOfflineNotices is populated only for a firing node_offline
	// event: one line per offline node, from EvaluateNodeOffline.
	NodeOfflineNotices []string
	// NodeCertNotices is populated only for a firing node_cert_expiring
	// event: one line per node whose agent certificate needs attention.
	NodeCertNotices []string
	// ResourceUsageNotices is populated only for a firing (not resolved)
	// node_resource_usage event: one line per node over its CPU and/or
	// memory threshold, from EvaluateNodeResourceUsage. Nil for every
	// other rule kind and for resolved events.
	ResourceUsageNotices []string
	// TaskFailureNotice is populated only for a firing (not resolved)
	// scheduled_task_failure event: the failing task's command,
	// consecutive-failure count, and last status, from
	// EvaluateScheduledTaskFailure. Empty for every other rule kind and
	// for resolved events.
	TaskFailureNotice string
	// DomainHealthNotices is populated only for a firing (not resolved)
	// domain_health event: one line per unhealthy domain on this rule's
	// own app, from EvaluateDomainHealth. Nil for every other rule kind
	// and for resolved events.
	DomainHealthNotices []string
	// BackupMissingNotice is populated only for a firing (not resolved)
	// backup_missing event: the watched database's or service volume's
	// overdue summary, from EvaluateBackupMissing. Empty for every other
	// rule kind and for resolved events.
	BackupMissingNotice string
	// VersionSkewNotice is populated only for a firing (not resolved)
	// version_skew event: the running version vs. the configured
	// channel's latest, from EvaluateVersionSkew. Empty for every other
	// rule kind and for resolved events.
	VersionSkewNotice string
	// BudgetNotices is populated only for a firing budget_threshold event:
	// one line per budget that crossed a threshold.
	BudgetNotices []string

	// SLONotice is set only for a firing slo_burn event: which burn-rate tier
	// tripped and how much error budget is left.
	SLONotice string

	// LogNotice is set for a firing log_pattern or log_level_rate event: the
	// count or rate that tripped. LogLines then holds the matching samples.
	LogNotice string

	// Changes is what changed on the app shortly before it fired; set for
	// firing app-scoped events only. ChangesLink points at its dashboard page.
	Changes     *changes.Result
	ChangesLink string

	// Headline replaces the default first line for flapping and grouped
	// messages; GroupNotices lists the alerts folded into a grouped one.
	Headline     string
	GroupNotices []string
	GroupCount   int
}

Event is what a firing (or resolved) rule hands to a Notifier: enough context to write a useful message without the notifier needing to go query anything itself.

type EventSource

type EventSource interface {
	Events(ctx context.Context) (<-chan docker.Event, <-chan error)
}

EventSource is the narrow Docker surface RestartTracker needs. *docker.Client satisfies this structurally (it already implements Runtime, which embeds Events).

type HistoryEntry added in v0.2.0

type HistoryEntry struct {
	ID         string
	At         time.Time
	RuleID     string
	RuleName   string
	RuleKind   string
	ResourceID string
	App        string
	Node       string
	Severity   string
	Event      string
	Outcome    string
	Detail     string
	SilenceID  string
	ChannelID  string
	Error      string
}

HistoryEntry is one alert state change and what became of its notification.

type HistoryFilter added in v0.2.0

type HistoryFilter struct {
	App     string
	RuleID  string
	Outcome string
	Event   string
	Since   time.Time
	Before  *time.Time
	Limit   int
}

HistoryFilter narrows ListHistory. Zero fields match everything.

type Kind

type Kind string

Kind distinguishes what a Rule evaluates.

const (
	// KindLogPattern fires when lines matching a text, regex or field filter
	// reach Count within Window.
	KindLogPattern Kind = "log_pattern"
	// KindLogLevelRate fires when lines at or above Level arrive faster than
	// PerMinute over Window.
	KindLogLevelRate Kind = "log_level_rate"
)

Log rule kinds. Both are app-scoped and evaluated from the node-local log store on a bounded cadence, never over an unbounded range.

const (
	KindThreshold            Kind = "threshold"
	KindCrashloop            Kind = "crashloop"
	KindCertExpiry           Kind = "cert_expiry"
	KindPatchStatus          Kind = "patch_status"
	KindScheduledTaskFailure Kind = "scheduled_task_failure"
	// KindScheduledTaskMissed and KindScheduledTaskRunningLong watch one
	// job's schedule health: skipped occurrences and a run in flight past
	// its warning threshold (scheduled_task_health.go).
	KindScheduledTaskMissed      Kind = "scheduled_task_missed"
	KindScheduledTaskRunningLong Kind = "scheduled_task_running_long"
	KindNodeDiskSpace            Kind = "node_disk_space"
	KindNodeResourceUsage        Kind = "node_resource_usage"
	KindDomainHealth             Kind = "domain_health"
	KindBackupMissing            Kind = "backup_missing"

	// KindControlPlaneBackupStale is platform-wide: it watches the newest control plane snapshot's age.
	KindControlPlaneBackupStale Kind = "control_plane_backup_stale"
	KindNodeOffline             Kind = "node_offline"

	// KindNodeCertExpiring is platform-wide: it fires while any node's agent
	// certificate is close to expiry or expired (node_cert_expiring.go).
	KindNodeCertExpiring Kind = "node_cert_expiring"

	// KindLogArchiveStale is platform-wide: it fires when a log archive policy fails or stops succeeding.
	KindLogArchiveStale Kind = "log_archive_stale"
	// KindRestoreDrillFailed is platform-wide: it fires while a resource's latest restore drill is failed.
	KindRestoreDrillFailed Kind = "restore_drill_failed"

	// KindVersionSkew is platform-wide: it fires while the running build
	// is behind the configured update channel's latest release
	// (update_settings.channel, migrations/0258_update_settings.sql).
	KindVersionSkew Kind = "version_skew"

	// KindBudgetThreshold is platform-wide: it fires while any project or
	// team budget is at its warn level, over its limit, or forecast to
	// overrun (budget_threshold.go).
	KindBudgetThreshold Kind = "budget_threshold"
)

The nine rule kinds this package evaluates; see Rule's own doc comment for which fields each uses. KindCertExpiry, KindPatchStatus, KindNodeDiskSpace, and KindNodeResourceUsage use none of Rule's threshold/crashloop fields: they watch every certificate (cert_expiry.go), every node's patch status (patch_status.go), every node's disk usage (disk_space.go), or every node's CPU/memory usage (node_resource_usage.go) platform-wide, not a single Metric or RestartWindow, so ResourceID on any of the four is only ever a display label, not something their evaluator filters by. KindDomainHealth is app-scoped like KindThreshold/KindCrashloop (ResourceID picks out a real app), but watches every domain currently configured on that app rather than a single Metric; see domain_health.go. KindBackupMissing watches one specific database's or service volume's scheduled backup cadence, picked out by the Backup*-prefixed fields below rather than ResourceID; see backup_missing.go.

const (
	// KindCertExpiring fires while a certificate expires within Threshold
	// days (default DefaultCertExpiringDays). ResourceID "domain:<name>"
	// narrows it to one hostname; empty watches every certificate.
	KindCertExpiring Kind = "cert_expiring"
	// KindCertRenewalStalled fires when a renewal has not moved NotAfter for
	// the stalled threshold, or an ACME failure persists that long.
	KindCertRenewalStalled Kind = "cert_renewal_stalled"
	// KindDomainNotResolving fires when a domain has not resolved for
	// ForDuration (default DefaultDomainNotResolvingWindow). ResourceID
	// "service:<app>" narrows it to one app's domains.
	KindDomainNotResolving Kind = "domain_not_resolving"
)

Domain and certificate rule kinds used by the traffic area. The spec's dns_drift, zone_not_delegated and proxy_route_error kinds are documented in docs/observability.md and not evaluated yet.

const KindSLOBurn Kind = "slo_burn"

KindSLOBurn fires on error-budget burn rate for a request-based SLO over the app's ingress request metrics, using the multiwindow multi-burn-rate method.

type LogArchiveHealth added in v0.2.0

type LogArchiveHealth struct {
	// Scope is the app name, or empty for the global policy.
	Scope         string
	Enabled       bool
	Interval      time.Duration
	CreatedAt     time.Time
	LastSuccessAt time.Time
	LastError     string
}

LogArchiveHealth is one log archive policy's freshness, as the engine sees it.

type LogArchiveSource added in v0.2.0

type LogArchiveSource interface {
	LogArchiveHealth(ctx context.Context) ([]LogArchiveHealth, error)
}

LogArchiveSource lists every log archive policy's health.

type LogRuleConfig added in v0.2.0

type LogRuleConfig struct {
	// Query is a full-text phrase matched by the store's FTS index.
	Query string `json:"query,omitempty"`
	// Regex further narrows the lines by their message (RE2 syntax).
	Regex string `json:"regex,omitempty"`
	// Fields are key=value, key!=value, key~text or key>number filters.
	Fields []string `json:"fields,omitempty"`
	// Level keeps lines at or above it. log_level_rate defaults to error.
	Level string `json:"level,omitempty"`
	// Count is the match count that fires a log_pattern rule.
	Count int `json:"count,omitempty"`
	// PerMinute is the lines per minute that fires a log_level_rate rule.
	PerMinute float64 `json:"per_minute,omitempty"`
	// WindowSeconds is the look-back; zero means five minutes.
	WindowSeconds int `json:"window_seconds,omitempty"`
	// CooldownSeconds suppresses a new firing for this long after the last one.
	CooldownSeconds int `json:"cooldown_seconds,omitempty"`
}

LogRuleConfig is what a log_pattern or log_level_rate rule watches.

func (LogRuleConfig) Validate added in v0.2.0

func (c LogRuleConfig) Validate(kind Kind, p LogRulePolicy) error

Validate rejects a config that cannot be evaluated within policy.

func (LogRuleConfig) Window added in v0.2.0

func (c LogRuleConfig) Window() time.Duration

Window returns the look-back with its default applied.

type LogRulePolicy added in v0.2.0

type LogRulePolicy struct {
	EvalEvery time.Duration
	MaxWindow time.Duration
	MaxRows   int
	Samples   int
}

LogRulePolicy bounds one evaluation: how often a rule runs, how far back it may look and how many rows it may read.

func LogRulePolicyFromEnv added in v0.2.0

func LogRulePolicyFromEnv() LogRulePolicy

LogRulePolicyFromEnv applies APP_LOG_RULE_* overrides to the defaults. A malformed or non-positive value keeps the default.

type LogRuleResult added in v0.2.0

type LogRuleResult struct {
	Matched   int
	Value     float64
	Truncated bool
	Samples   []string
	Notice    string
	Satisfied bool
}

LogRuleResult is one evaluation's outcome.

func MatchLogRule added in v0.2.0

func MatchLogRule(kind Kind, cfg LogRuleConfig, entries []telemetry.LogEntry, truncated bool, now time.Time, policy LogRulePolicy) (LogRuleResult, error)

MatchLogRule applies cfg to entries (oldest first, already limited to the window) and returns the result. It is pure so rules test with seeded lines.

type LogRuleSource added in v0.2.0

type LogRuleSource interface {
	QueryLogs(ctx context.Context, resourceID string, from, to time.Time, query string) ([]telemetry.LogEntry, error)
}

LogRuleSource reads bounded log windows; *telemetry.Federator satisfies it.

type LogsSource

type LogsSource interface {
	QueryLogs(ctx context.Context, resourceID string, from, to time.Time, query string) ([]telemetry.LogEntry, error)
}

LogsSource is the narrow surface Engine needs to attach log lines to a firing crashloop event. *telemetry.Federator satisfies this structurally, the same as MetricsSource in evaluate.go.

type MaintenanceWindow added in v0.2.0

type MaintenanceWindow struct {
	ID        string
	Name      string
	Cron      string
	Duration  time.Duration
	Timezone  string
	Scope     string
	Targets   []string
	Enabled   bool
	CreatedBy string
	CreatedAt time.Time
	UpdatedAt time.Time
}

MaintenanceWindow is a recurring silence: it starts at each cron match (evaluated in Timezone) and lasts Duration.

func (MaintenanceWindow) Covers added in v0.2.0

func (w MaintenanceWindow) Covers(c AlertContext) bool

Covers reports whether the window's scope includes the alert c.

func (MaintenanceWindow) StateAt added in v0.2.0

func (w MaintenanceWindow) StateAt(now time.Time) (WindowState, error)

StateAt evaluates the cron schedule in the window's timezone. Cron fields are matched against wall-clock time, so durations are measured on the wall clock too (a window spanning a DST change keeps its nominal length).

func (MaintenanceWindow) Validate added in v0.2.0

func (w MaintenanceWindow) Validate() error

Validate checks every field of the window.

type MetricsSource

type MetricsSource interface {
	QueryMetrics(ctx context.Context, resourceID, metric string, from, to time.Time) ([]telemetry.Sample, error)
}

MetricsSource is the narrow surface a threshold evaluation needs. *telemetry.Federator satisfies this structurally; a hand-written fake in tests avoids needing a real store for pure evaluation-logic tests.

type MuteHit added in v0.2.0

type MuteHit struct {
	ID     string
	Detail string
	Window bool
}

MuteHit names the silence or maintenance window muting an alert.

func FindMute added in v0.2.0

func FindMute(silences []Silence, windows []MaintenanceWindow, c AlertContext, now time.Time) (MuteHit, bool)

FindMute reports whether any active silence or enabled maintenance window in effect at now mutes the alert c.

type NodeAlertState

type NodeAlertState string

NodeAlertState is one node-scoped alert kind's live status for one specific node, computed on demand by CheckNodeAlertStatus rather than read from a rule's stored, all-nodes-aggregated LastValue.

const (
	NodeAlertOK      NodeAlertState = "ok"
	NodeAlertFiring  NodeAlertState = "firing"
	NodeAlertUnknown NodeAlertState = "unknown"
)

The three states CheckNodeAlertStatus can report per kind. NodeAlertUnknown is distinct from NodeAlertOK on purpose: a node with no recent sample must never be reported as healthy, the same "don't silently confirm what you can't actually see" stance every evaluator in this package already takes on a per-node basis.

type NodeAlertStatus

type NodeAlertStatus struct {
	PatchStatus       NodeAlertState
	NodeDiskSpace     NodeAlertState
	NodeResourceUsage NodeAlertState
}

NodeAlertStatus is one node's live status across the three node-scoped, platform-wide alert kinds.

func CheckNodeAlertStatus

func CheckNodeAlertStatus(ctx context.Context, node store.Node, services NodeServiceSource, metrics MetricsSource,
	patchThreshold, diskThresholdPercent, cpuThresholdPercent, memoryThresholdBytes float64, now time.Time, logger *slog.Logger) NodeAlertStatus

CheckNodeAlertStatus live-evaluates patch_status, node_disk_space, and node_resource_usage for node, right now, rather than waiting for Engine's next tick or reading a rule's stored aggregate LastValue (which only ever holds the worst value seen across every node, not which node it came from). It calls each evaluator's own per-node check helper directly so this can never drift from what a real rule would decide. A threshold of 0 falls back to that kind's own default, the same convention EvaluatePatchStatus/EvaluateNodeDiskSpace/ EvaluateNodeResourceUsage already use.

type NodeCertState added in v0.2.0

type NodeCertState string

NodeCertState summarizes a node's agent certificate.

const (
	NodeCertOK       NodeCertState = "ok"
	NodeCertExpiring NodeCertState = "expiring"
	NodeCertCritical NodeCertState = "critical"
	NodeCertExpired  NodeCertState = "expired"
	NodeCertRevoked  NodeCertState = "revoked"
	NodeCertUnknown  NodeCertState = "unknown"
)

Node certificate states, from healthy to unusable.

func ClassifyNodeCert added in v0.2.0

func ClassifyNodeCert(n store.Node, warning, critical time.Duration, now time.Time) (NodeCertState, time.Duration)

ClassifyNodeCert returns n's certificate state at now and the time left before it expires (negative once expired).

type NodeServiceSource

type NodeServiceSource interface {
	ListDesiredServicesByNode(ctx context.Context, nodeID string) ([]store.DesiredService, error)
}

NodeServiceSource is the narrow node-placement surface EvaluateNodeResourceUsage needs alongside NodeSource: which services are placed on a node, the same lookup internal/api's handleQueryNodeMetrics already uses (queryPlacedServiceSamples) to sum per-container metrics into a node-level total. *store.DB satisfies this structurally.

type NodeSource

type NodeSource interface {
	ListNodes(ctx context.Context) ([]store.Node, error)
}

NodeSource is the narrow node-listing surface EvaluatePatchStatus needs. *store.DB satisfies this structurally, the same as CertSource above.

type NoiseConfig added in v0.2.0

type NoiseConfig struct {
	ConsecutiveFailures int
	FlapThreshold       int
	FlapWindow          time.Duration
	GroupWindow         time.Duration
	RateLimit           int
	RateWindow          time.Duration
	HistoryRetention    time.Duration
}

NoiseConfig holds the control plane defaults for alert noise control. A zero value disables the corresponding feature; per-rule fields on Rule override ConsecutiveFailures and the flap settings.

type NoiseControl added in v0.2.0

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

NoiseControl sits between rule evaluation and notification. It applies consecutive-failure thresholds, flapping detection, silences, maintenance windows, node-down inhibition, grouping and per-channel rate limits, and records every decision in alert history. State is in memory: a control plane restart forgets pending groups, streaks and suppressed fires (rule firing state itself is persisted by the engine).

func NewNoiseControl added in v0.2.0

func NewNoiseControl(cfg NoiseConfig, st NoiseStore, placement PlacementSource, logger *slog.Logger) *NoiseControl

NewNoiseControl builds a NoiseControl. placement may be nil, which disables node-based matching and inhibition.

func (*NoiseControl) ApplyStreak added in v0.2.0

func (n *NoiseControl) ApplyStreak(prev, next Rule, now time.Time) Rule

ApplyStreak enforces the consecutive-failure threshold on one evaluated rule before its state is persisted.

func (*NoiseControl) FlushAll added in v0.2.0

func (n *NoiseControl) FlushAll(ctx context.Context, now time.Time, send SendFunc)

FlushAll sends every buffered group immediately, for shutdown.

func (*NoiseControl) Recheck added in v0.2.0

func (n *NoiseControl) Recheck(ctx context.Context, r Rule, now time.Time, send SendFunc)

Recheck releases a held firing notification once its silence, window or node outage is over while the rule is still firing.

func (*NoiseControl) RecordSkipped added in v0.2.0

func (n *NoiseControl) RecordSkipped(ctx context.Context, r Rule, resolved bool, now time.Time)

RecordSkipped notes an event dropped because its rule or channel is disabled.

func (*NoiseControl) Route added in v0.2.0

func (n *NoiseControl) Route(ctx context.Context, ev Event, now time.Time, send SendFunc)

Route decides what happens to one firing or resolved event.

func (*NoiseControl) Sweep added in v0.2.0

func (n *NoiseControl) Sweep(ctx context.Context, now time.Time, send SendFunc)

Sweep flushes due groups, ends calmed flapping rules and prunes history. Call once per evaluation tick.

type NoiseStore added in v0.2.0

type NoiseStore interface {
	ListActiveSilences(ctx context.Context, now time.Time) ([]Silence, error)
	ListMaintenanceWindows(ctx context.Context) ([]MaintenanceWindow, error)
	RecordHistory(ctx context.Context, e HistoryEntry) error
	PruneHistory(ctx context.Context, cutoff time.Time) (int64, error)
}

NoiseStore is what NoiseControl reads and writes. *DB satisfies it.

type NotificationChannel

type NotificationChannel struct {
	ID        string
	Name      string
	Kind      NotifyKind
	NotifyURL string
	Enabled   bool
	// InteractiveApprovals opts this channel into Approve/Deny buttons on
	// a deploy-approval-requested message (chat_approval.go), Slack and
	// Discord kinds only. Off by default: it needs InteractiveSecret set,
	// and a real Slack app or Discord application configured on the
	// other end, not just an incoming-webhook URL.
	InteractiveApprovals bool
	// InteractiveSecret verifies an inbound button click really came
	// from this channel's own configured app: a Slack signing secret
	// (HMAC-SHA256 over the request) for NotifySlack, or a Discord
	// application's Ed25519 public key (hex) for NotifyDiscord. Empty
	// for every other kind.
	InteractiveSecret string
	// NotifyDeviceLogin opts this channel into a link-only notice when a CLI
	// device login awaits approval. The notice never carries the code.
	NotifyDeviceLogin bool
	// NotifyDeviceLoginExpired opts this channel into a link-only notice when
	// a pending CLI login lapses unapproved.
	NotifyDeviceLoginExpired bool
	CreatedAt                string
	UpdatedAt                string
}

NotificationChannel is a global, connect-once notify destination (migrations/0003_notification_channels.sql's own doc comment), reused across apps' DeployTarget rows by ID instead of a fresh URL per app.

type NotificationDelivery

type NotificationDelivery struct {
	ID        string
	ChannelID string
	Trigger   string
	Success   bool
	Error     string
	CreatedAt string
}

NotificationDelivery is one recorded attempt to send through a NotificationChannel. Every real send path (deploy-outcome dispatch, alert-rule dispatch, and the existing test-send routes) records one of these via recordDelivery below, so a channel's delivery history reflects real send attempts, not just test-button clicks.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, ev Event) error
}

Notifier sends one Event somewhere. Every notify* function below satisfies this via notifyFunc, so the dispatch table in Dispatch stays a plain map, no interface boilerplate per channel.

func NewNotifier

func NewNotifier(client *http.Client, sender email.Sender, pushSender PushSender, r Rule) Notifier

NewNotifier builds the right Notifier for r.NotifyKind. An unknown or empty NotifyKind falls back to NotifyGeneric rather than erroring, so a typo'd notify_kind still notifies someone, diagnosable from the payload shape. sender may be nil, in which case an email-kind rule fails with a clear "not configured" error; pushSender may be nil with the identical failure shape for a webpush-kind rule.

type NotifyKind

type NotifyKind string

NotifyKind selects the notification payload shape (notify.go).

const (
	NotifyGeneric    NotifyKind = "generic"
	NotifySlack      NotifyKind = "slack"
	NotifyDiscord    NotifyKind = "discord"
	NotifyTelegram   NotifyKind = "telegram"
	NotifyEmail      NotifyKind = "email"
	NotifyPushover   NotifyKind = "pushover"
	NotifyPagerDuty  NotifyKind = "pagerduty"
	NotifyTeams      NotifyKind = "teams"
	NotifyResend     NotifyKind = "resend"
	NotifyNtfy       NotifyKind = "ntfy"
	NotifyGotify     NotifyKind = "gotify"
	NotifyMattermost NotifyKind = "mattermost"
	NotifyLark       NotifyKind = "lark"
	NotifyRocketChat NotifyKind = "rocketchat"
	NotifyOpsgenie   NotifyKind = "opsgenie"
	NotifyWebex      NotifyKind = "webex"
	NotifyGoogleChat NotifyKind = "googlechat"
	NotifyWebpush    NotifyKind = "webpush"
)

The eighteen payload shapes NewNotifier knows how to build; an unknown or empty NotifyKind falls back to NotifyGeneric. NotifyEmail is the one exception to "NotifyURL is a webhook URL": see emailNotifier's doc comment in notify.go. NotifyPagerDuty is another: NotifyURL there holds a routing key, not a URL, per notifyPagerDuty's own comment. NotifyResend is a third: it needs both an API key and a destination address, so NotifyURL there packs both as query parameters against a fixed endpoint, the same convention NotifyPushover already uses for its own two credentials; see parseResendCreds in notify.go. NotifyOpsgenie is a fourth: it needs only an API key, packed the same way against its own fixed endpoint; see parseOpsgenieCreds in notify.go. NotifyWebpush is a fifth: NotifyURL is unused entirely, the destination is every browser subscription this control plane holds, not something a single channel row can name; see webpushNotifier in notify.go.

type PlacementSource added in v0.2.0

type PlacementSource interface {
	GetDesiredService(ctx context.Context, name string) (*store.DesiredService, error)
	ListNodes(ctx context.Context) ([]store.Node, error)
}

PlacementSource resolves which node an app runs on and whether it is offline. *store.DB satisfies it.

type PushSender added in v0.2.0

type PushSender interface {
	Send(ctx context.Context, title, body string) error
}

PushSender sends one browser push notification to every subscription registered on this control plane. Narrow, consumer-defined interface mirroring email.Sender exactly; *webpush.Sender satisfies it structurally (internal/alerting has no import on internal/webpush, the same "consumer defines the interface it needs" shape as every other Secrets-flavored interface in this codebase).

type RequestCounts added in v0.2.0

type RequestCounts struct {
	Total float64
	Bad   float64
}

RequestCounts is the good/bad split of an app's requests over a span.

func CountRequests added in v0.2.0

func CountRequests(ctx context.Context, src MetricsSource, app string, from, to time.Time, cfg SLOConfig) (RequestCounts, error)

CountRequests sums an app's request counters over [from, to]. Counters are per-tick deltas, so negative or non-finite values (a counter reset artefact) count as zero and Bad never exceeds Total. A window with partial data is counted over what exists.

type RestartRecorder added in v0.2.0

type RestartRecorder interface {
	RecordContainerRestart(ctx context.Context, serviceName string, at time.Time) error
}

RestartRecorder is the narrow telemetry surface RestartTracker needs to persist each real restart it counts as a queryable metric sample, alongside this tracker's own in-memory crashloop-window bookkeeping. *telemetry.DB satisfies this structurally. Not imported directly (internal/telemetry does not depend on internal/alerting, and this keeps it that way): the same narrow, consumer-defined interface shape EventSource and ServiceLister above already use.

type RestartTracker

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

RestartTracker watches Docker's event stream and records each "start" event as a restart for whichever service owns that container, keyed the same "service:<name>" way every other telemetry identifier in this codebase is.

Counts "start" events, not "die" events: a container dying once is an ordinary crash, what makes it a crashloop is the reconciler repeatedly restarting the same container afterward (containers never carry Docker's own restart policy, the reconciler is the sole authority on bringing a dead container back, so every restart is visible as this process re-issuing Start on the same container ID). The very first start observed for a given container name is never counted as a restart of anything: a fresh deploy produces a brand-new container name (internal/reconcile/application's deterministic per-image naming), so its first start is ordinary startup, not a restart, without needing to special-case "was this a deploy or a crash" any other way.

func NewRestartTracker

func NewRestartTracker() *RestartTracker

NewRestartTracker builds an empty RestartTracker with no persistence: Observe only updates this tracker's own in-memory crashloop-window state, the behavior every test in this package already exercises. Call WithRecorder to also persist each real restart as a queryable metric sample.

func (*RestartTracker) CountSince

func (t *RestartTracker) CountSince(resourceID string, since time.Time) int

CountSince returns how many restarts resourceID has had strictly after since. Safe for concurrent use.

func (*RestartTracker) Observe

func (t *RestartTracker) Observe(resourceID, containerName string, at time.Time)

Observe records one "start" event for containerName, owned by resourceID, at time at. Safe for concurrent use. When a recorder is attached (WithRecorder), a genuine restart (not a container's first observed start) is also persisted as a MetricContainerRestartCount sample; a failed persistence write is logged and otherwise ignored, this tracker's own in-memory count is still updated either way.

func (*RestartTracker) Prune

func (t *RestartTracker) Prune(olderThan time.Time)

Prune drops restart records older than olderThan, so this map doesn't grow without bound over a long-running process. Call periodically (Run does this once per resync), not per-evaluation.

func (*RestartTracker) Run

func (t *RestartTracker) Run(ctx context.Context, source EventSource, services ServiceLister, resync time.Duration, logger *slog.Logger) error

Run consumes source's event stream until ctx is done, resolving each "start" event's container name against a service list refreshed every resync tick (services are created/removed over time; a container belonging to a brand-new service must resolve correctly without a process restart, the same level-triggered "re-derive, don't cache forever" principle every reconcile.Controller already follows).

func (*RestartTracker) WithRecorder added in v0.2.0

func (t *RestartTracker) WithRecorder(recorder RestartRecorder, logger *slog.Logger) *RestartTracker

WithRecorder attaches recorder so every restart Observe counts from here on is also persisted via RecordContainerRestart, not just kept in this tracker's own pruned in-memory window. Returns t for chaining at construction (cmd/levelrail/main.go's own call site style). logger defaults to slog.Default() if nil, and is only ever used to log a failed persistence write, never to block or fail Observe itself: a telemetry write failing must not stop crashloop detection, the same "one broken part must not stop the rest" principle telemetry.Collector.CollectOnce's own doc comment already applies to collection.

type RestoreDrillProblem added in v0.2.0

type RestoreDrillProblem struct {
	Resource string
	Reason   string
	At       time.Time
}

RestoreDrillProblem is one resource whose latest restore drill failed.

type RestoreDrillSource added in v0.2.0

type RestoreDrillSource interface {
	RestoreDrillProblems(ctx context.Context) ([]RestoreDrillProblem, error)
}

RestoreDrillSource lists resources whose most recent restore drill failed.

type Rule

type Rule struct {
	ID         string
	Name       string
	Kind       Kind
	ResourceID string

	// Threshold-kind fields.
	Metric      string
	Comparator  Comparator
	Threshold   float64
	ForDuration time.Duration

	// Crashloop-kind fields. RestartCountThreshold is also reused, with
	// the same "N events triggers firing" meaning, by a
	// KindScheduledTaskFailure rule: there it counts consecutive failed
	// runs (store.ScheduledTask.ConsecutiveFailures) rather than restarts
	// within RestartWindow, which that kind leaves unused.
	RestartCountThreshold int
	RestartWindow         time.Duration

	// ScheduledTaskID is the KindScheduledTaskFailure-only field: which
	// of ResourceID's app's scheduled tasks this rule watches. Empty for
	// every other kind.
	ScheduledTaskID string

	// KindBackupMissing-only fields: which backup schedule this rule
	// watches, mirroring store.BackupHistory's own database-vs-volume
	// identity shape (ResourceKind/DatabaseName vs ServiceName/VolumeName,
	// mutually exclusive). BackupResourceKind is
	// store.BackupResourceKindDatabase or store.BackupResourceKindVolume;
	// BackupDatabaseName is set for the former, BackupServiceName/
	// BackupVolumeName for the latter. Empty for every other kind.
	// ForDuration above is reused here as the grace period past a
	// schedule's expected interval before a missing backup counts as
	// overdue, the same "field means something slightly different per
	// kind" reuse RestartCountThreshold already establishes for
	// KindScheduledTaskFailure.
	BackupResourceKind string
	BackupDatabaseName string
	BackupServiceName  string
	BackupVolumeName   string

	// ChannelID attaches an already-connected NotificationChannel; empty
	// for legacy rules, which use NotifyURL/NotifyKind below directly.
	// SLO is the KindSLOBurn-only request-based SLO; nil for every other kind.
	SLO *SLOConfig
	// LogRule is the KindLogPattern/KindLogLevelRate-only match config; nil for every other kind.
	LogRule *LogRuleConfig

	ChannelID  string
	NotifyURL  string
	NotifyKind NotifyKind

	// Enabled reflects both this rule's own flag and, once resolved by a
	// read (GetRule/ListRules*), its attached channel's: a disabled
	// channel silences the rule too.
	Enabled bool

	// Noise control: Severity and Labels feed silence matchers;
	// ConsecutiveFailures, FlapThreshold and FlapWindow override the
	// control plane defaults when non-zero.
	Severity            string
	Labels              map[string]string
	ConsecutiveFailures int
	FlapThreshold       int
	FlapWindow          time.Duration

	// Evaluation state, read-only from a caller's perspective: only the
	// evaluator (evaluate.go, crashloop.go) writes these, via
	// UpdateState below.
	PendingSince    *time.Time
	Firing          bool
	FiringSince     *time.Time
	LastEvaluatedAt *time.Time
	LastValue       *float64
}

Rule is one alert rule: either a threshold check (Kind == KindThreshold, using Metric/Comparator/Threshold/ForDuration) or a crashloop check (Kind == KindCrashloop, using RestartCountThreshold/RestartWindow), plus its own current evaluation state. See migrations/0001_alert_rules.sql for why the two kinds share one table instead of two.

func EvaluateBackupMissing added in v0.2.0

func EvaluateBackupMissing(ctx context.Context, source BackupSource, r Rule, defaultGracePeriod time.Duration, now time.Time) (Rule, string, error)

EvaluateBackupMissing runs one KindBackupMissing rule and returns its updated evaluation state plus, when firing, a human-readable notice line for Engine to attach to the outgoing Event.

"Missing" is measured against the same cron schedule internal/backup.Scheduler itself reads (store.DesiredDatabase. BackupSchedule or store.ServiceVolumeBackupConfig.BackupSchedule), not a second, independently configured cadence: the interval between two consecutive scheduled fire times from now (via cronexpr.Schedule.Next, called twice) stands in for that schedule's period, since nothing in this codebase persists an "expected next run" outside Scheduler's own in-memory map (that struct's own doc comment). The rule fires once now is more than that period plus its grace period (r.ForDuration when set, reused here per Rule's own doc comment, else defaultGracePeriod) past the last successful attempt.

A target with no schedule currently configured, or with no history at all yet (a schedule just created, nothing had a chance to run), goes quiet rather than firing: there is nothing yet to compare against, the same "nothing to alert on" quiet-state EvaluateScheduledTaskFailure's own doc comment applies to a deleted watched task. A target with attempts but no success anywhere in the lookback window anchors the overdue calculation on its oldest known attempt instead, so a backup that has only ever failed still reads as overdue rather than silently never firing.

func EvaluateBudgetThreshold added in v0.2.0

func EvaluateBudgetThreshold(ctx context.Context, src BudgetSource, r Rule, now time.Time) (Rule, []string, []string, error)

EvaluateBudgetThreshold runs one KindBudgetThreshold rule: it fires while any budget has crossed its warn level, its limit, or is forecast to overrun. It returns one notice per breach and the breach keys, which the engine uses to notify again only when a new threshold is crossed.

func EvaluateCertExpiring added in v0.2.0

func EvaluateCertExpiring(ctx context.Context, certs CertSource, r Rule, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluateCertExpiring runs one KindCertExpiring rule.

func EvaluateCertExpiry

func EvaluateCertExpiry(ctx context.Context, certs CertSource, snapshots CertExpiryObservationStore, r Rule, warningWindow, stalledThreshold time.Duration, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluateCertExpiry runs one KindCertExpiry rule against every certificate currently in certs and returns its updated evaluation state plus, when firing, one human-readable notice line per non-healthy certificate for Engine to attach to the outgoing Event.

The rule fires (satisfied, in advanceState terms) the instant any certificate is "expiring_soon" or "expired", with no ForDuration debounce: unlike a noisy metric sample, expiry status only moves monotonically with real time, so there is nothing to debounce. LastValue is the number of days until the earliest-expiring certificate (negative once expired), across every certificate, not just the non-healthy ones, so an operator can see "how close" even while healthy.

Per non-healthy domain, snapshots records the NotAfter it had when it first entered this expiry episode (EpisodeNotAfter/EpisodeStartedAt). If a later tick finds the same domain still non-healthy with an unchanged NotAfter for at least stalledThreshold, that domain's notice is marked as a renewal that appears stalled rather than a plain warning: Caddy's own renewal should have already succeeded well before the warning window even opened (DefaultCertRenewalStalledThreshold's own doc comment), so a stuck NotAfter this deep into the window is a stronger, more actionable signal than "getting close to expiry."

func EvaluateControlPlaneBackupStale added in v0.2.0

func EvaluateControlPlaneBackupStale(source ControlPlaneBackupSource, r Rule, defaultMaxAge time.Duration, now time.Time) (Rule, string, error)

EvaluateControlPlaneBackupStale runs one KindControlPlaneBackupStale rule. It fires when the newest snapshot is older than the rule's ForDuration (else defaultMaxAge). No snapshot yet stays quiet, like the doctor check.

func EvaluateCrashloop

func EvaluateCrashloop(tracker *RestartTracker, r Rule, now time.Time) Rule

EvaluateCrashloop runs one KindCrashloop rule against tracker and returns its updated evaluation state, the crashloop equivalent of EvaluateThreshold, sharing the same pending/firing debounce logic via advanceState. Passes forDuration=0: a crashloop rule fires the instant its restart count crosses the threshold, with no additional debounce layered on top, because RestartWindow itself already is the debounce (requiring N restarts within a real time window is already "sustained," unlike a single noisy metric sample).

func EvaluateDomainHealth

func EvaluateDomainHealth(ctx context.Context, apps AppDomainSource, checker DomainCheckSource, r Rule, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluateDomainHealth runs one KindDomainHealth rule against every domain currently configured on the app it's scoped to (ResourceID) and returns its updated evaluation state plus, when firing, one human-readable notice line per unhealthy domain, for Engine to attach to the outgoing Event.

Unlike EvaluateCertExpiry/EvaluatePatchStatus/EvaluateNodeDiskSpace (platform-wide, ResourceID only a display label), a domain_health rule's ResourceID is real: it picks out exactly which app's own domains this rule checks, the same app-scoped shape EvaluateThreshold/ EvaluateCrashloop use. An app with no domains configured, or one that has since been deleted, is treated like "no recent data": neither confirms nor denies.

r.ForDuration optionally debounces a single unhealthy check the same way EvaluateThreshold's own ForDuration does, since a real DNS lookup can blip; zero (the default) fires the instant any domain is unhealthy, matching every other non-threshold kind's own default.

func EvaluateLogArchiveStale added in v0.2.0

func EvaluateLogArchiveStale(ctx context.Context, source LogArchiveSource, r Rule, now time.Time) (Rule, string, error)

EvaluateLogArchiveStale runs one KindLogArchiveStale rule. A policy is unhealthy when its last run failed or it has not succeeded within the rule's ForDuration (else three intervals, at least two hours).

func EvaluateNodeCertExpiring added in v0.2.0

func EvaluateNodeCertExpiring(ctx context.Context, nodes NodeSource, r Rule, warning, critical time.Duration, now time.Time) (Rule, []string, error)

EvaluateNodeCertExpiring runs one KindNodeCertExpiring rule: it fires while any node's agent certificate is inside the warning window (the rule's ForDuration, else warning) or already expired. Revoked nodes are left out: that was an operator's decision, not a failure. LastValue is the fewest days left across the flagged nodes.

func EvaluateNodeDiskSpace

func EvaluateNodeDiskSpace(ctx context.Context, nodes NodeSource, metrics MetricsSource, r Rule, thresholdPercent float64, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluateNodeDiskSpace runs one KindNodeDiskSpace rule against every node in nodes and returns its updated evaluation state plus, when firing, one human-readable notice line per node whose latest used-disk percentage meets or exceeds thresholdPercent, for Engine to attach to the outgoing Event.

Firing follows EvaluatePatchStatus's shape, not EvaluateThreshold's: the instant any node is over threshold, with no ForDuration debounce. LastValue is the highest used-disk percentage seen across every node with a recent sample, not just the ones over threshold, matching EvaluatePatchStatus's own LastValue convention.

A node missing either a disk_used_bytes or disk_total_bytes sample inside diskSpaceLookback (collector hasn't run yet), or reporting a total of zero or less, is skipped, neither confirming nor denying the condition. A node whose metrics query fails is logged and skipped too, the same "one broken resource must not block the rest" stance EvaluatePatchStatus already takes.

func EvaluateNodeOffline added in v0.2.0

func EvaluateNodeOffline(ctx context.Context, nodes NodeSource, r Rule, now time.Time) (Rule, []string, error)

EvaluateNodeOffline runs one KindNodeOffline rule: it fires while any node's status is offline and resolves once every node is back. Like the other platform-wide node kinds it has no debounce, since the heartbeat sweep already applies its own grace before marking a node offline. LastValue is the count of offline nodes.

func EvaluateNodeResourceUsage

func EvaluateNodeResourceUsage(ctx context.Context, nodes NodeSource, services NodeServiceSource, metrics MetricsSource, r Rule, cpuThresholdPercent, memoryThresholdBytes float64, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluateNodeResourceUsage runs one KindNodeResourceUsage rule against every node in nodes and returns its updated evaluation state plus, when firing, one human-readable notice line per node whose summed CPU and/or memory usage meets or exceeds its threshold, for Engine to attach to the outgoing Event.

There is no real host-level CPU/memory collector in this codebase (unlike disk, telemetry has no /proc-based host reading): what exists is cpu_percent/memory_usage_bytes sampled per container (telemetry.Collector) and already summed across a node's placed services by internal/api's own node-metrics endpoint (nodeSummableMetrics). This evaluator reuses that exact same signal rather than inventing a new collector, taking each placed service's latest sample (not a time-series sum) since alerting only needs "right now," not a chart.

CPU and memory are evaluated together as one rule, not two, because they are the same "is this node under load" question from an operator's perspective, but they use genuinely different-shaped thresholds: CPU is compared as a percent (cpuThresholdPercent, the metric's own natural unit), memory as an absolute byte count (memoryThresholdBytes), since no honest node-capacity percentage exists for memory (see DefaultNodeMemoryThresholdBytes). LastValue only ever holds the highest summed CPU percent seen across nodes, matching EvaluateNodeDiskSpace's own "one representative number" convention; memory readings, having no comparable unit to take a single max across with CPU, are surfaced only in the per-node notice text.

Firing follows EvaluateNodeDiskSpace's shape: the instant either signal is over its threshold on any node, with no ForDuration debounce. A node with nothing placed on it, or whose placed services have no recent sample for either metric, is skipped, neither confirming nor denying the condition. A service whose own metrics query fails is logged and skipped, the same "one broken resource must not block the rest" stance EvaluateNodeDiskSpace already takes on a node.

func EvaluatePatchStatus

func EvaluatePatchStatus(ctx context.Context, nodes NodeSource, metrics MetricsSource, r Rule, threshold float64, now time.Time, logger *slog.Logger) (Rule, []string, error)

EvaluatePatchStatus runs one KindPatchStatus rule against every node in nodes and returns its updated evaluation state plus, when firing, one human-readable notice line per node whose latest security-patch count meets or exceeds threshold, for Engine to attach to the outgoing Event.

Firing follows EvaluateCertExpiry's shape, not EvaluateThreshold's: the instant any node is over threshold, with no ForDuration debounce. HostPatchCollector already only samples once an hour, so there is nothing left for a per-tick debounce to smooth out. LastValue is the highest security-patch count seen across every node with a recent sample, not just the ones over threshold, so an operator can see "how close" the fleet is even while every node is healthy, matching EvaluateCertExpiry's own LastValue convention.

A node with no sample inside patchStatusLookback (no supported package manager, or the collector hasn't run yet) is skipped, neither confirming nor denying the condition, the same "no recent data" stance EvaluateThreshold takes. A node whose own metrics query fails is logged and skipped too, rather than failing the whole rule: the same "one broken resource must not block the rest" stance ListCertificates takes on a single malformed certificate.

func EvaluateRestoreDrillFailed added in v0.2.0

func EvaluateRestoreDrillFailed(ctx context.Context, source RestoreDrillSource, r Rule, now time.Time) (Rule, string, error)

EvaluateRestoreDrillFailed runs one KindRestoreDrillFailed rule. It fires while any resource's latest restore drill is failed and resolves once a later drill passes.

func EvaluateSLOBurn added in v0.2.0

func EvaluateSLOBurn(ctx context.Context, src MetricsSource, r Rule, p SLOPolicy, now time.Time) (Rule, string, error)

EvaluateSLOBurn runs one KindSLOBurn rule. It returns the rule's next state and, while firing, the notice for the notification. A rule with no SLO config never fires.

func EvaluateScheduledTaskFailure

func EvaluateScheduledTaskFailure(ctx context.Context, tasks ScheduledTaskSource, r Rule, now time.Time) (Rule, string, error)

EvaluateScheduledTaskFailure runs one KindScheduledTaskFailure rule against r.ScheduledTaskID's current ConsecutiveFailures count and returns its updated evaluation state plus, when firing, a human-readable notice line for Engine to attach to the outgoing Event.

Polls the task's own persisted counter rather than tracking anything itself, the same "read current state, no local history" shape EvaluateCertExpiry already uses: the counter is maintained by store.RecordScheduledTaskRun at the moment a run actually happens, decoupled from how often Engine.Tick itself runs.

Fires the instant ConsecutiveFailures crosses RestartCountThreshold, with no ForDuration debounce: requiring N consecutive failures is already the debounce, the same reasoning EvaluateCrashloop's own doc comment gives for RestartWindow.

func EvaluateScheduledTaskHealth added in v0.2.0

func EvaluateScheduledTaskHealth(ctx context.Context, tasks ScheduledTaskSource, r Rule, now time.Time) (Rule, string, error)

EvaluateScheduledTaskHealth evaluates a KindScheduledTaskMissed or KindScheduledTaskRunningLong rule from the job's persisted counters, so firing does not depend on how often the engine ticks.

func EvaluateThreshold

func EvaluateThreshold(ctx context.Context, source MetricsSource, r Rule, now time.Time) (Rule, error)

EvaluateThreshold runs one KindThreshold rule against source and returns its updated evaluation state, to be persisted via DB.UpdateState by the caller (engine.go). Pure with respect to persistence: this function never writes to the database itself, so its state-machine logic (the part actually worth testing precisely) is testable without a real DB.

Semantics: the rule's condition must hold continuously, checked on every evaluation tick, for at least ForDuration before it graduates from "pending" to "firing" (avoiding a single noisy sample flapping a brand-new alert into existence). Once firing, it stays firing until a tick observes the condition no longer holding, at which point it resets to not-pending/not-firing immediately: there's no separate "resolving" debounce, a single good sample is enough to consider it recovered, asymmetric on purpose since false-positive resolution (briefly saying "recovered" when it isn't) is a much smaller problem than false-positive firing (paging someone for a blip).

func EvaluateVersionSkew added in v0.2.0

func EvaluateVersionSkew(ctx context.Context, settings VersionSkewSettingsSource, fetchers upgrade.Fetchers, cache *upgrade.Cache, r Rule, currentVersion string, now time.Time) (Rule, string, error)

EvaluateVersionSkew runs one KindVersionSkew rule: it fires while currentVersion is behind the configured channel's latest release. cache is checked before calling fetchers, so a rule enabled alongside Engine's own short evaluation interval never hits GitHub more often than upgrade.DefaultCacheTTL, the same cadence GET /api/v1/updates and updatecheck.Scheduler already hold themselves to.

type RuleStore

type RuleStore interface {
	ListEnabledRules(ctx context.Context) ([]Rule, error)
	UpdateState(ctx context.Context, id string, pendingSince, firingSince *time.Time, firing bool, evaluatedAt time.Time, value *float64) error
	RecordNotificationDelivery(ctx context.Context, d NotificationDelivery) error
	UpsertCertExpiryObservation(ctx context.Context, o CertExpiryObservation) error
	ListCertExpiryObservations(ctx context.Context, ruleID string) ([]CertExpiryObservation, error)
}

RuleStore is the narrow store surface Engine needs: list what to evaluate, persist the result. *DB satisfies this structurally. The two CertExpiry methods are what a kind=cert_expiry rule uses to remember its per-domain state across ticks (EvaluateCertExpiry, cert_expiry.go); every other rule kind never calls them.

type SLOAutoRollbackStore added in v0.2.0

type SLOAutoRollbackStore interface {
	AutoRollbackStore
	SaveDeployApproval(ctx context.Context, a store.DeployApproval) error
}

SLOAutoRollbackStore extends AutoRollbackStore (crashloop.go) with the save-approval surface store.AutoRollbackSLOBurnPauseForHuman mode needs. *store.DB satisfies this structurally.

type SLOBurnHistoryRecorder added in v0.2.0

type SLOBurnHistoryRecorder interface {
	RecordHistory(ctx context.Context, e HistoryEntry) error
}

SLOBurnHistoryRecorder is the narrow alerting.DB surface store.AutoRollbackSLOBurnDryRun mode needs to log a "would have rolled back" event, without threading NoiseControl's full history pipeline through this call. *DB satisfies this structurally.

type SLOConfig added in v0.2.0

type SLOConfig struct {
	Objective string  `json:"objective"`
	Target    float64 `json:"target"`
	LatencyMs float64 `json:"latency_ms,omitempty"`
}

SLOConfig is the request-based SLO a KindSLOBurn rule watches. Target is a percentage of good requests (99.9). For SLOLatency a request is good when it finishes within LatencyMs, rounded down to the nearest histogram bound.

func (SLOConfig) ErrorBudget added in v0.2.0

func (c SLOConfig) ErrorBudget() float64

ErrorBudget is the allowed fraction of bad requests, 0.001 for a 99.9 target.

func (SLOConfig) Validate added in v0.2.0

func (c SLOConfig) Validate() error

Validate rejects a config that cannot yield a meaningful burn rate.

type SLOPolicy added in v0.2.0

type SLOPolicy struct {
	Tiers       []BurnTier
	Window      time.Duration
	MinRequests float64
	EvalEvery   time.Duration
}

SLOPolicy is the engine-wide burn-rate configuration.

func DefaultSLOPolicy added in v0.2.0

func DefaultSLOPolicy() SLOPolicy

DefaultSLOPolicy returns the SRE workbook defaults: 14.4x over 1h and 5m and 6x over 6h and 30m page, 3x over 1d and 2h and 1x over 3d and 6h open a ticket.

func SLOPolicyFromEnv added in v0.2.0

func SLOPolicyFromEnv() SLOPolicy

SLOPolicyFromEnv applies APP_SLO_* overrides to the defaults. A malformed or non-positive value keeps the default.

type SLOStatus added in v0.2.0

type SLOStatus struct {
	Config          SLOConfig        `json:"config"`
	HasTraffic      bool             `json:"has_traffic"`
	BudgetRemaining float64          `json:"budget_remaining"`
	BudgetRequests  float64          `json:"budget_window_requests"`
	Windows         []BurnWindowStat `json:"windows"`
	Tiers           []TierStatus     `json:"tiers"`
	Firing          bool             `json:"firing"`
	Page            bool             `json:"page"`
	MaxBurn         float64          `json:"max_burn"`
}

SLOStatus is the full evaluation of an SLO at one instant.

func ComputeSLOStatus added in v0.2.0

func ComputeSLOStatus(ctx context.Context, src MetricsSource, app string, cfg SLOConfig, p SLOPolicy, now time.Time) (SLOStatus, error)

ComputeSLOStatus queries every distinct window once and evaluates all tiers. Partial windows (an app newer than the window) are judged on the traffic that exists, and a tier whose long window saw fewer than MinRequests cannot fire.

func (SLOStatus) Notice added in v0.2.0

func (s SLOStatus) Notice() string

Notice is the one-line explanation of the firing tier that burns fastest.

type ScheduledTaskSource

type ScheduledTaskSource interface {
	GetScheduledTask(ctx context.Context, id string) (store.ScheduledTask, error)
}

ScheduledTaskSource is the narrow store surface a scheduled-task-failure evaluation needs. *store.DB satisfies this structurally.

type SendFunc added in v0.2.0

type SendFunc func(ctx context.Context, ev Event) error

SendFunc delivers one event to its channel.

type ServiceLister

type ServiceLister interface {
	ListDesiredServices(ctx context.Context) ([]store.DesiredService, error)
}

ServiceLister is the narrow store surface RestartTracker needs to resolve a container name to the service that owns it. *store.DB satisfies this structurally.

type Silence added in v0.2.0

type Silence struct {
	ID        string
	Matchers  SilenceMatcher
	StartsAt  time.Time
	EndsAt    time.Time
	CreatedBy string
	Reason    string
	CreatedAt time.Time
	ExpiredAt *time.Time
}

Silence suppresses notifications for matching alerts between StartsAt and EndsAt.

func (Silence) EffectiveEnd added in v0.2.0

func (s Silence) EffectiveEnd() time.Time

EffectiveEnd is when the silence stopped or will stop applying.

func (Silence) Status added in v0.2.0

func (s Silence) Status(now time.Time) string

Status reports pending, active or expired at now.

func (Silence) Validate added in v0.2.0

func (s Silence) Validate() error

Validate checks the silence's matchers and time range.

type SilenceMatcher added in v0.2.0

type SilenceMatcher struct {
	RuleIDs    []string          `json:"rule_ids,omitempty"`
	Kinds      []string          `json:"kinds,omitempty"`
	Apps       []string          `json:"apps,omitempty"`
	Nodes      []string          `json:"nodes,omitempty"`
	Labels     map[string]string `json:"labels,omitempty"`
	Severities []string          `json:"severities,omitempty"`
}

SilenceMatcher selects alerts. Every non-empty field must match (AND); within a list field any entry may match (OR).

func (SilenceMatcher) IsEmpty added in v0.2.0

func (m SilenceMatcher) IsEmpty() bool

IsEmpty reports whether the matcher has no criteria at all.

func (SilenceMatcher) Matches added in v0.2.0

func (m SilenceMatcher) Matches(c AlertContext) bool

Matches reports whether c satisfies every criterion in m.

func (SilenceMatcher) Validate added in v0.2.0

func (m SilenceMatcher) Validate() error

Validate rejects a matcher that would match everything or names an unknown severity.

type TierStatus added in v0.2.0

type TierStatus struct {
	BurnTier
	LongBurn  float64 `json:"long_burn"`
	ShortBurn float64 `json:"short_burn"`
	Effective float64 `json:"effective_burn"`
	Firing    bool    `json:"firing"`
}

TierStatus is one tier's evaluation. Effective is the lower of the long and short burn rates, the value compared against Factor.

type VersionSkewSettingsSource added in v0.2.0

type VersionSkewSettingsSource interface {
	GetUpdateSettings(ctx context.Context) (store.UpdateSettings, error)
}

VersionSkewSettingsSource is the narrow store surface EvaluateVersionSkew needs: the configured update channel (migrations/0258_update_settings.sql). *store.DB satisfies this structurally.

type WindowState added in v0.2.0

type WindowState struct {
	Active    bool
	Start     time.Time
	End       time.Time
	NextStart time.Time
}

WindowState is a window's status at one instant.

Jump to

Keyboard shortcuts

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