alerts

package
v0.5.9 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package alerts evaluates operator-defined rules against osquery log traffic and node state, and dispatches notifications through configured channels (email, webhooks, …).

Design constraints (hot-path first):

  • The ingest path never reads the database or Redis to decide whether a log line matches. Rules are served from an immutable RuleSet snapshot loaded at boot / refresh; a config change swaps the snapshot atomically (copy-on-write), so matching is lock-free.
  • Regexes are compiled once per snapshot. Matching over a decoded batch is O(rules) with no allocations beyond the hit slice.
  • Cooldown/dedupe state lives in Redis with a TTL equal to the rule's cooldown window, and is consulted by the dispatch worker — never by the ingest goroutine.

Row conventions follow pkg/logsinks: EnvironmentID=0 means the rule applies globally to every environment; per-env rows override. Rules are stored per source type so the ingest hook only walks rules registered for the log type it just decoded.

Index

Constants

View Source
const (
	ChannelWebhook = "webhook"
	ChannelEmail   = "email"
)

Channel types.

View Source
const (
	FieldString  = "string"
	FieldInteger = "integer"
	FieldBoolean = "boolean"
	FieldSecret  = "secret"
)

Field types for channel forms.

View Source
const (
	// SourceResultLog watches osquery scheduled-query result logs.
	SourceResultLog = "result_log"
	// SourceStatusLog watches osquery daemon status logs.
	SourceStatusLog = "status_log"
	// SourceQueryLog watches on-demand distributed query results.
	SourceQueryLog = "query_log"
	// SourceNodeInactive fires when a node crosses the inactive
	// threshold. Evaluated by a periodic sweep, not the ingest path.
	SourceNodeInactive = "node_inactive"
	// SourceNodeRecovered fires when a previously inactive node is
	// seen again. Evaluated by the same sweep.
	SourceNodeRecovered = "node_recovered"
)

Sources a rule can watch. The ingest hook only evaluates rules whose Source matches the data it just decoded, so unrelated rules cost nothing on any given path.

View Source
const (
	MatchTypeSubstring = "substring"
	MatchTypeRegex     = "regex"
)

Match types. Substring is the operator default — regex is opt-in because an author-supplied pattern is a DoS vector otherwise.

View Source
const DefaultCooldown = 15 * time.Minute

DefaultCooldown is used when a rule specifies CooldownMinutes == 0. A small non-zero default keeps a flood of identical matches from spamming channels when the operator has not thought about cooldowns.

View Source
const MaxNodeUUIDLen = 64

MaxNodeUUIDLen bounds NodeUUID to its column width.

View Source
const MaxPatternLen = 512

MaxPatternLen caps an operator-supplied regex/substring length. Long patterns are a trivial DoS lever on a hot path.

View Source
const MaxRulesPerEnv = 50

MaxRulesPerEnv caps rule count per environment so a fleet of very matchy rules cannot make ingest O(huge). Enforced at create/update in the manager.

View Source
const NoEnvironmentID uint = 0

NoEnvironmentID is the sentinel for global (non-env-scoped) rows. Mirrors settings.NoEnvironmentID / logsinks.NoEnvironmentID.

View Source
const SignatureHeader = "X-Osctrl-Signature"

SignatureHeader carries the HMAC of the request body.

View Source
const TestSendTimeout = 15 * time.Second

TestSendTimeout bounds a channel test so an unresponsive relay cannot hold the API request open. Senders carry their own per-request timeouts; this is the backstop for the ones that do not (SMTP).

Variables

View Source
var (
	// ErrRuleNotFound is returned by manager Get/Update/Delete.
	ErrRuleNotFound = errors.New("alert rule not found")
	// ErrChannelNotFound is returned by channel manager methods.
	ErrChannelNotFound = errors.New("alert channel not found")
	// ErrRuleExists is returned on duplicate (name, environment).
	ErrRuleExists = errors.New("alert rule already exists")
	// ErrChannelExists is returned on duplicate (name, environment).
	ErrChannelExists = errors.New("alert channel already exists")
	// ErrInvalidSource is returned when Source is not a known source.
	ErrInvalidSource = errors.New("invalid alert source")
	// ErrInvalidRule wraps every ValidateRule failure.
	ErrInvalidRule = errors.New("invalid alert rule")

	// ErrTooManyRules guards MaxRulesPerEnv.
	ErrTooManyRules = fmt.Errorf("too many alert rules for one environment (max %d)", MaxRulesPerEnv)
)
View Source
var ChannelRegistry = map[string]ChannelSpec{
	ChannelWebhook: {
		Type:         ChannelWebhook,
		Description:  "HTTP POST to a URL with the alert as JSON",
		HasSecret:    true,
		SecretFields: []string{"secret"},
		Fields: []FieldSpec{
			{Name: "url", Label: "URL", Type: FieldString, Required: true, Placeholder: "https://example.com/hook", Help: "HTTPS URL receiving the alert as a JSON POST."},
			{Name: "secret", Label: "HMAC secret", Type: FieldSecret, Help: "When set, the payload is signed with X-Osctrl-Signature (HMAC-SHA256, hex)."},
			{Name: "timeoutSeconds", Label: "Timeout (seconds)", Type: FieldInteger, Default: 10, Help: "Per-request timeout, including retries."},
			{Name: "insecureSkipVerify", Label: "Skip TLS verify", Type: FieldBoolean, Default: false, Help: "Do not verify the server certificate. Avoid in production."},
			{Name: "allowPrivateTargets", Label: "Allow private/loopback target", Type: FieldBoolean, Default: false, Help: "Required to reach localhost or an RFC1918 address. Off by default so a webhook cannot be pointed at internal services; enable only for a relay you run yourself."},
		},
		Decode: decodeTyped[WebhookConfig](),
		Build: func(cfg any) (ChannelSender, error) {
			return buildWebhook(*cfg.(*WebhookConfig))
		},
	},
	ChannelEmail: {
		Type:         ChannelEmail,
		Description:  "Email to a list of recipients via SMTP",
		HasSecret:    true,
		SecretFields: []string{"password"},
		Fields: []FieldSpec{
			{Name: "host", Label: "SMTP host", Type: FieldString, Required: true, Placeholder: "smtp.example.com", Help: "SMTP relay host."},
			{Name: "port", Label: "SMTP port", Type: FieldInteger, Default: 587, Help: "587 = STARTTLS, 465 = implicit TLS, 25 = plaintext."},
			{Name: "username", Label: "Username", Type: FieldString},
			{Name: "password", Label: "Password", Type: FieldSecret},
			{Name: "from", Label: "From address", Type: FieldString, Required: true, Placeholder: "osctrl@example.com", Help: "Envelope sender for the notification."},
			{Name: "to", Label: "Recipients", Type: FieldString, Required: true, Placeholder: "soc@example.com, oncall@example.com", Help: "Comma-separated recipient addresses."},
			{Name: "starttls", Label: "Use STARTTLS", Type: FieldBoolean, Default: true, Help: "Upgrade to TLS after connect (port 587). Ignored on port 465 (implicit TLS)."},
		},
		Decode: decodeTyped[EmailConfig](),
		Build: func(cfg any) (ChannelSender, error) {
			return buildEmail(*cfg.(*EmailConfig))
		},
	},
}

ChannelRegistry maps each channel type to its spec. Adding a channel type means: implement the sender, add the config struct here, add a ChannelSpec entry.

View Source
var ErrChannelDisabled = errors.New("alert channel disabled")

ErrChannelDisabled marks a channel the operator turned off. The dispatcher skips these without treating them as failures.

View Source
var ErrInvalidChannelConfig = fmt.Errorf("invalid channel configuration")

ErrInvalidChannelConfig is returned when the config JSON fails to decode against the registered type.

View Source
var ErrInvalidChannelType = fmt.Errorf("invalid channel type")

ErrInvalidChannelType is returned for unregistered channel types.

Functions

func CompileRule

func CompileRule(rule AlertRule) (compiledRule, error)

CompileRule validates and compiles one rule into its matcher form. Returns an error when the regex is invalid or the pattern is too long — callers (manager Create/Update) must reject such rules before they reach the store.

func DecodeChannelIDs

func DecodeChannelIDs(raw string) ([]uint, error)

DecodeChannelIDs parses the JSON channel-ID array. Empty string / null decodes to nil.

func DecodeChannelIDsOrEmpty

func DecodeChannelIDsOrEmpty(raw string) []uint

DecodeChannelIDsOrEmpty parses the channel-ID array and returns an empty slice on any error — a convenience for API clients that only need display values.

func EncodeChannelIDs

func EncodeChannelIDs(ids []uint) string

EncodeChannelIDs serializes channel IDs for storage.

func MergeChannelSecrets

func MergeChannelSecrets(typ, prevCfgJSON, newCfgJSON string) (string, error)

MergeChannelSecrets replaces "***" placeholder values in newCfg with the corresponding values from prevCfg, so an API update that did not touch a secret preserves the stored one instead of writing the placeholder.

func RedactedChannelConfig

func RedactedChannelConfig(typ, cfgJSON string) string

RedactedChannelConfig returns the Config JSON with secret fields replaced by "***" for the channel Type. Non-secret types return the raw config unchanged. Best-effort: undecodable configs pass through.

func RegisterWorkerMetrics

func RegisterWorkerMetrics(reg prometheus.Registerer, w *Worker)

RegisterWorkerMetrics registers a collector for the worker's counters. Call once per process with the live worker; the collector reads the same atomics the hot path bumps, so there is no locking between scrape and dispatch.

func SupportedChannelTypes

func SupportedChannelTypes() []string

SupportedChannelTypes lists the registry keys, sorted.

func TestSend

func TestSend(typ, cfgJSON string) error

TestSend delivers one synthetic notification through a channel config without storing it, so an operator can verify a channel from the form before saving. Config errors and delivery errors are both returned as they are — the operator needs to see which relay refused what.

func ValidateChannelConfig

func ValidateChannelConfig(typ, cfgJSON string) error

ValidateChannelConfig decodes the JSON config against the registry.

func ValidateChannelType

func ValidateChannelType(typ string) bool

ValidateChannelType reports whether the type is registered.

func ValidateRule

func ValidateRule(rule AlertRule) error

ValidateRule checks the operator-facing fields of a rule before it is stored. Mirrors logsinks.ValidateSink. Every failure wraps ErrInvalidRule: these are all operator-input errors, so the API answers 400 with the reason instead of an opaque 500.

Types

type AlertChannel

type AlertChannel struct {
	gorm.Model
	Name          string `gorm:"uniqueIndex:idx_alert_channels_unique"`
	EnvironmentID uint   `gorm:"uniqueIndex:idx_alert_channels_unique"`
	// Type is the Registry key: "email", "webhook", …
	Type string `gorm:"index;size:32"`
	// Config is a JSON blob whose shape depends on Type.
	Config  string `gorm:"type:text"`
	Enabled bool
	Info    string
}

AlertChannel is one notification destination. Type + Config follow the logsinks pattern: Config is a JSON blob validated against the channel Registry (Stage 3); the typed form schema ships with the Registry so the SPA renders it generically.

func (AlertChannel) TableName

func (AlertChannel) TableName() string

TableName overrides the default table name.

type AlertHistory

type AlertHistory struct {
	ID        uint      `gorm:"primarykey" json:"id"`
	CreatedAt time.Time `json:"created_at"`
	// RuleID / ChannelID are denormalized as uint rather than FKs so
	// deleting a rule or channel does not cascade-delete history.
	RuleID      uint   `gorm:"index" json:"rule_id"`
	RuleName    string `gorm:"size:128" json:"rule_name"`
	ChannelID   uint   `gorm:"index" json:"channel_id"`
	ChannelName string `gorm:"size:128" json:"channel_name"`
	Environment string `gorm:"size:64;index" json:"environment"`
	NodeUUID    string `gorm:"size:64;index" json:"node_uuid"`
	// Entity is what matched: a hostname, a query name, or "node" for
	// inactive/recovered rules. Part of the Redis dedupe key.
	Entity string `gorm:"size:256" json:"entity"`
	// Detail is the rendered match context (bounded at match time).
	Detail string `gorm:"type:text" json:"detail"`
}

AlertHistory records every notification the system sent, for the "recent alerts" view and for auditing. Written only by the dispatch worker (off the hot path); reads happen through the management API.

func (AlertHistory) TableName

func (AlertHistory) TableName() string

TableName overrides the default table name.

type AlertRule

type AlertRule struct {
	gorm.Model
	Name          string `gorm:"uniqueIndex:idx_alert_rules_unique"`
	EnvironmentID uint   `gorm:"uniqueIndex:idx_alert_rules_unique"`
	// Source is one of the Source* constants.
	Source string `gorm:"index;size:32"`
	// NodeUUID optionally scopes the rule to a single node. When set:
	// log-matching rules only match entries whose hostIdentifier equals
	// it, and node-state rules only fire for that node. Empty = all
	// nodes in scope (the original behavior).
	NodeUUID string `gorm:"size:64;index"`
	// MatchType is MatchTypeSubstring or MatchTypeRegex.
	MatchType string `gorm:"size:16"`
	// MatchField scopes the match to one column ("message" for status
	// logs, a column name for result/query logs). Empty = any field.
	MatchField string `gorm:"size:128"`
	// MatchValue is the substring or regex pattern.
	MatchValue string `gorm:"type:text"`
	// StatusSeverity filters status-log rules: "error", "warning" or
	// "any". Only consulted when Source == SourceStatusLog.
	StatusSeverity string `gorm:"size:16;default:any"`
	// CooldownMinutes suppresses repeat notifications for the same
	// rule+entity within this window. 0 = alert on every match (subject
	// to the global default cooldown).
	CooldownMinutes int `gorm:"default:0"`
	// ChannelIDs is a JSON-encoded array of alert_channel IDs. Empty
	// array = no channels yet; the rule matches but nothing is sent.
	ChannelIDs string `gorm:"type:text"`
	Enabled    bool
	Info       string
}

AlertRule is one operator-defined alert. Name + EnvironmentID are unique together (gorm composite index below), mirroring saved queries and log sinks.

func (AlertRule) TableName

func (AlertRule) TableName() string

TableName overrides the default table name.

type ChannelSender

type ChannelSender interface {
	Send(h Hit) error
	// Name identifies the channel in logs and history.
	Name() string
}

ChannelSender delivers one notification for one hit. Implementations must be safe for concurrent use from the worker pool.

type ChannelSpec

type ChannelSpec struct {
	Type         string
	Description  string
	HasSecret    bool
	SecretFields []string
	Fields       []FieldSpec
	// Decode unmarshals a raw JSON Config into the typed struct the
	// sender expects.
	Decode func(json.RawMessage) (any, error)
	// Build instantiates a ChannelSender from a decoded config.
	Build func(any) (ChannelSender, error)
}

ChannelSpec describes one supported channel type.

type DispatchSink

type DispatchSink interface {
	Dispatch(ctx context.Context, h Hit) error
}

DispatchSink processes one claimed hit. Implemented by the channel fan-out in Stage 3; tests substitute their own.

type Dispatcher

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

Dispatcher fans hits out to configured channels.

func NewDispatcher

func NewDispatcher(mgr *Manager) *Dispatcher

NewDispatcher builds a channel dispatcher over the alert manager.

func (*Dispatcher) Dispatch

func (d *Dispatcher) Dispatch(ctx context.Context, h Hit) error

Dispatch delivers the hit to every referenced channel. Channels that are disabled, mis-typed, or missing are skipped without counting as failures — an operator disabling a channel is a choice, not an outage. Real delivery errors are per-channel: one dead webhook does not block the email. Returns an error only when at least one delivery was attempted and every one of them failed (so the worker can retry the claim). Zero usable channels is a no-op success.

func (*Dispatcher) RefreshChannel

func (d *Dispatcher) RefreshChannel(id uint)

RefreshChannel drops the cached sender for one channel so the next dispatch rebuilds it from the current row.

func (*Dispatcher) Reset

func (d *Dispatcher) Reset()

Reset drops all cached senders (full reload).

type EmailConfig

type EmailConfig struct {
	Host     string `json:"host"`
	Port     int    `json:"port"`
	Username string `json:"username"`
	Password string `json:"password"`
	From     string `json:"from"`
	To       string `json:"to"`
	StartTLS bool   `json:"starttls"`
}

EmailConfig is the typed JSON config for email channels.

type FieldSpec

type FieldSpec struct {
	Name        string
	Label       string
	Type        string // string | integer | boolean | secret
	Required    bool
	Placeholder string
	Help        string
	Default     any
}

FieldSpec reuses the logsinks field schema for SPA form rendering.

type Hit

type Hit struct {
	RuleID        uint
	RuleName      string
	EnvironmentID uint
	// Environment is the env name captured at match time, used for the
	// history row and rendered payloads.
	Environment     string
	NodeUUID        string
	Entity          string
	Detail          string
	CooldownMinutes int
	Channels        []uint
}

Hit is one rule × one entity match, ready for the dispatch worker.

func TestHit

func TestHit() Hit

TestHit is the payload a channel test delivers. It is shaped like a real hit so the operator sees exactly the format their relay will receive, and is labelled so nobody mistakes it for a live alert.

type InactiveWatcher

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

InactiveWatcher runs the sweep and emits hits to the dispatch worker.

func NewInactiveWatcher

func NewInactiveWatcher(source NodeSource, store *Store, worker *Worker, client *redis.Client) *InactiveWatcher

NewInactiveWatcher builds the watcher. worker may be nil (feature off mid-restart) — sweeps then no-op. A nil client builds a watcher with no persistence: every sweep re-evaluates transitions from scratch, so inactive nodes re-alert each pass (acceptable only for tests; production always passes the Redis client).

func (*InactiveWatcher) Run

func (w *InactiveWatcher) Run(stop <-chan struct{}, interval time.Duration)

Run blocks running the sweep on the interval until stop closes.

func (*InactiveWatcher) Sweep

func (w *InactiveWatcher) Sweep(ctx context.Context)

Sweep runs one detection pass: mark newly-inactive nodes, unmark recovered ones, and enqueue hits for the transitions.

type IngestMatcher

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

IngestMatcher adapts a rule snapshot + worker to the ingest hook. A nil *IngestMatcher is a valid disabled matcher (all methods no-op), so cmd/tls can store the nil pointer without nil-interface traps.

func NewIngestMatcher

func NewIngestMatcher(store *Store, worker *Worker) *IngestMatcher

NewIngestMatcher wires a matcher over the given snapshot store and dispatch worker. Both must be non-nil.

func (*IngestMatcher) MatchQueryResult

func (m *IngestMatcher) MatchQueryResult(envID uint, environment, queryName string, result json.RawMessage, status int, message string)

MatchQueryResult implements logging.AlertMatcher.

func (*IngestMatcher) MatchResultLogs

func (m *IngestMatcher) MatchResultLogs(envID uint, environment string, logs []types.LogResultData)

MatchResultLogs implements logging.AlertMatcher.

func (*IngestMatcher) MatchStatusLogs

func (m *IngestMatcher) MatchStatusLogs(envID uint, environment string, logs []types.LogStatusData)

MatchStatusLogs implements logging.AlertMatcher.

type Manager

type Manager struct {
	DB *gorm.DB
}

Manager manages the alert_rules / alert_channels / alert_history tables.

func NewManager

func NewManager(backend *gorm.DB) *Manager

NewManager initializes the manager and auto-migrates the three tables. AutoMigrate is production-impacting (it creates the tables on first boot) but additive: existing deployments gain three empty tables.

func (*Manager) CreateChannel

func (m *Manager) CreateChannel(ch AlertChannel) (AlertChannel, error)

CreateChannel inserts a new notification channel. Type and config are validated against the channel registry.

func (*Manager) CreateRule

func (m *Manager) CreateRule(rule AlertRule) (AlertRule, error)

CreateRule validates and inserts a new rule. The rule-count cap per environment is enforced here.

func (*Manager) DeleteChannel

func (m *Manager) DeleteChannel(id uint) error

DeleteChannel removes a channel by ID. Rules referencing it keep the dangling ID in ChannelIDs; dispatch treats unknown channels as disabled and skips them.

func (*Manager) DeleteRule

func (m *Manager) DeleteRule(id uint) error

DeleteRule removes a rule by ID. History rows are not cascaded — they keep the RuleName snapshot for the audit trail.

func (*Manager) GetChannel

func (m *Manager) GetChannel(id uint) (AlertChannel, error)

GetChannel retrieves one channel by ID.

func (*Manager) GetRule

func (m *Manager) GetRule(id uint) (AlertRule, error)

GetRule retrieves one rule by ID.

func (*Manager) ListChannels

func (m *Manager) ListChannels(envID *uint) ([]AlertChannel, error)

ListChannels returns all channels, optionally env-scoped.

func (*Manager) ListRules

func (m *Manager) ListRules(envID *uint) ([]AlertRule, error)

ListRules returns all rules, optionally scoped to one environment. envID nil = all environments; NoEnvironmentID = global rows only.

func (*Manager) LoadSnapshot

func (m *Manager) LoadSnapshot(store *Store) error

LoadSnapshot reads all enabled rules, compiles them, and publishes a fresh RuleSet to the store. A rule that fails to compile (bad regex introduced by direct DB edit) is skipped with the error surfaced; it never blocks the rest of the ruleset.

func (*Manager) PruneHistory

func (m *Manager) PruneHistory(olderThan interface{}) (int64, error)

PruneHistory removes rows older than the retention window. Called by a periodic sweep, never on the hot path. Returns the number of rows deleted so the sweep can log the effect.

func (*Manager) PruneHistoryWithRetention

func (m *Manager) PruneHistoryWithRetention(retentionDays int64, now time.Time) (int64, error)

PruneHistoryWithRetention computes the cutoff from a retention window in days and prunes. Kept separate from the raw PruneHistory so the sweep can log "retention=Nd deleted=N" in one place.

func (*Manager) RecentHistory

func (m *Manager) RecentHistory(limit int) ([]AlertHistory, error)

RecentHistory returns the newest history rows, capped.

func (*Manager) RecordHistory

func (m *Manager) RecordHistory(h AlertHistory) error

RecordHistory appends one dispatched-alert row (dispatch worker only). Detail is bounded here as defense in depth — the matcher already truncates at hit time.

func (*Manager) UpdateChannel

func (m *Manager) UpdateChannel(id uint, ch AlertChannel) (AlertChannel, error)

UpdateChannel replaces the mutable fields of a channel. Type and config are re-validated.

func (*Manager) UpdateRule

func (m *Manager) UpdateRule(id uint, rule AlertRule) (AlertRule, error)

UpdateRule replaces the mutable fields of an existing rule.

type NodeSnapshot

type NodeSnapshot struct {
	UUID string
	// EnvironmentID / Environment identify the node's environment for
	// rule scoping (ruleApplies) and hit attribution respectively.
	EnvironmentID uint
	Environment   string
	Hostname      string
	Active        bool
	LastSeen      time.Time
}

NodeSnapshot is one node's liveness state as seen by the sweep.

type NodeSource

type NodeSource interface {
	// InactiveNodes returns every node whose last_seen is older than
	// the given threshold, and the threshold itself is resolved per
	// environment by the implementation.
	InactiveNodes(ctx context.Context) ([]NodeSnapshot, error)
	// ActiveNodes returns every currently-active node (used to detect
	// recoveries of nodes previously marked inactive).
	ActiveNodes(ctx context.Context) ([]NodeSnapshot, error)
}

NodeSource produces node snapshots for the sweep. Implemented over the nodes manager in osctrl-tls; tests substitute their own.

type RuleSet

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

RuleSet is an immutable snapshot of all enabled rules, grouped by source so each ingest path only walks its own rules. The zero value (no rules) is valid and match-free.

func (*RuleSet) MatchQueryResult

func (rs *RuleSet) MatchQueryResult(envID uint, environment, queryName string, result json.RawMessage, status int, message string) []Hit

MatchQueryResult evaluates one on-demand query result (the ProcessLogQueryResult tap) against rules scoped to envID. queryName scopes the hit to the query; rows is the decoded result payload.

func (*RuleSet) MatchResultLogs

func (rs *RuleSet) MatchResultLogs(envID uint, environment string, logs []types.LogResultData) []Hit

MatchResultLogs evaluates result-log entries against rules scoped to envID. The columns of each entry (or its snapshot rows) are matched field-scoped or across all fields depending on the rule.

func (*RuleSet) MatchStatusLogs

func (rs *RuleSet) MatchStatusLogs(envID uint, environment string, logs []types.LogStatusData) []Hit

MatchStatusLogs evaluates status-log entries against rules scoped to envID. Entries below the rule's severity floor are skipped before any pattern work happens.

type State

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

State is the Redis-backed dedupe gate.

func NewState

func NewState(client *redis.Client) *State

NewState builds a cooldown state on top of an existing Redis client (the same client the backend cache uses).

func (*State) Claim

func (s *State) Claim(ctx context.Context, h Hit) (bool, error)

Claim attempts to reserve the right to notify for a hit. Returns true when the caller should dispatch. On a Redis error the claim fails open (true): availability of alerting beats perfect dedupe, and a Redis outage is not a reason to silently swallow matches.

func (*State) Release

func (s *State) Release(ctx context.Context, h Hit)

Release drops the claim early (used when dispatch fails so the next hit can retry without waiting out the window). Best-effort.

type Store

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

Store publishes rule snapshots. Safe for concurrent use; readers pull via Snapshot() which returns the current immutable set.

func NewStore

func NewStore() *Store

NewStore creates an empty rule store.

func (*Store) Publish

func (s *Store) Publish(rs *RuleSet)

Publish atomically replaces the snapshot. Building the new RuleSet happens before this call, so the swap itself is a single word store.

func (*Store) Snapshot

func (s *Store) Snapshot() *RuleSet

Snapshot returns the current immutable rule set. The returned pointer must be treated as read-only.

type WebhookConfig

type WebhookConfig struct {
	URL                string `json:"url"`
	Secret             string `json:"secret"`
	TimeoutSeconds     int    `json:"timeoutSeconds"`
	InsecureSkipVerify bool   `json:"insecureSkipVerify"`
	// AllowPrivateTargets permits http(s):// targets on loopback or
	// RFC1918 / link-local ranges. Intended for the dev stack's
	// sink-catchall container; production deployments should leave it
	// off so a compromised operator account cannot probe internals.
	AllowPrivateTargets bool `json:"allowPrivateTargets"`
}

WebhookConfig is the typed JSON config for webhook channels.

type Worker

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

Worker is the dispatch pipeline behind the matcher.

func NewSyncWorker

func NewSyncWorker(store *Store, state claimGate, manager *Manager, sink DispatchSink) *Worker

NewSyncWorker builds a worker that dispatches inline (tests only).

func NewWorker

func NewWorker(store *Store, state claimGate, manager *Manager, sink DispatchSink, queueSize, workers int) *Worker

NewWorker builds and starts the dispatch worker pool. store is the rule snapshot (for future per-rule lookups); state may be nil (claims pass through); sink may be nil (hits are only claimed + recorded). Set selfRecording when the sink writes its own history rows.

func (*Worker) Close

func (w *Worker) Close()

Close drains the queue and stops the workers.

func (*Worker) Enqueue

func (w *Worker) Enqueue(hits []Hit)

Enqueue offers hits to the dispatch pipeline. Never blocks: a full queue drops the hit and bumps the counter. Nil worker is a no-op (alerts disabled), which keeps the ingest hook allocation-free when the feature is off.

func (*Worker) MetricsSnapshot added in v0.5.9

func (w *Worker) MetricsSnapshot() WorkerSnapshot

MetricsSnapshot reads the counters. Safe on a nil worker (alerts disabled).

func (*Worker) QueueDepth

func (w *Worker) QueueDepth() int

QueueDepth returns the current queue length.

type WorkerMetrics

type WorkerMetrics struct {
	// Matched counts hits accepted into the queue.
	Matched atomic.Uint64
	// Dropped counts hits rejected because the queue was full.
	Dropped atomic.Uint64
	// Dispatched counts hits processed by the sink.
	Dispatched atomic.Uint64
	// Collapsed counts hits suppressed by the cooldown gate.
	Collapsed atomic.Uint64
	// Failed counts sink errors.
	Failed atomic.Uint64
}

WorkerMetrics collects the counters exposed to Prometheus. Kept as plain atomics so the hot path never touches the registry; cmd/tls polls them from its metrics endpoint.

type WorkerSnapshot added in v0.5.9

type WorkerSnapshot struct {
	QueueDepth    int
	QueueCapacity int
	Matched       uint64
	Dispatched    uint64
	Collapsed     uint64
	Dropped       uint64
	Failed        uint64
}

WorkerSnapshot is a point-in-time read of the worker counters, for callers outside this package (the health heartbeat). Plain values, not atomics, so the caller cannot mutate live counters.

Jump to

Keyboard shortcuts

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