outcome

package
v0.7.0 Latest Latest
Warning

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

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

Documentation

Overview

Package outcome records, in an append-only JSONL ledger, whether an investigated incident later resolved and which answer was used for it — the "did it actually work?" signal the learning loop reads. The ledger keeps an in-memory index of still-open incidents, rebuilt by replaying the file on startup so a resolve survives a restart / leader failover.

Index

Constants

View Source
const (
	// GitOpsFingerprintPrefix marks a fingerprint derived from a GitOps failure's
	// resource-ref + condition reason.
	GitOpsFingerprintPrefix = "gitops:"
	// ReinvestigateFingerprintPrefix marks a fingerprint derived for a reinvestigate poll.
	ReinvestigateFingerprintPrefix = "reinvestigate:"
)

Fingerprint prefixes for incidents RunLore assigns a synthetic id to because the triggering source carries no external alert fingerprint (an Alertmanager/PagerDuty incident id). They are chosen so a derived id can never collide with a real fingerprint, and they double as the "no ground-truth resolve channel" marker that keeps such recall opens out of recall-decay (see Event.Resolvable / applyOpenLocked).

View Source
const DefaultMaxEvents = 50000

DefaultMaxEvents is the generous default compaction bound used by New (and by the serve wiring when outcome.max_events is unset). Chosen high enough that a healthy ledger never compacts in normal operation; compaction is a growth backstop, not a routine trim.

Variables

This section is empty.

Functions

func DeriveFingerprint added in v0.7.0

func DeriveFingerprint(prefix, key string) string

DeriveFingerprint returns a stable, deterministic fingerprint for an incident that carries no external alert id, formed as prefix + a short hex sha256 of key (e.g. the trigger key, or resource-ref+reason). Determinism is the point: the same recurring incident derives the SAME fingerprint every time, so its opens roll up into Occurrences/Episodes as recurrences — while the prefix keeps it from ever colliding with a real Alertmanager/PagerDuty fingerprint.

func Derived added in v0.7.0

func Derived(fp string) bool

Derived reports whether fp was assigned by DeriveFingerprint (a synthetic id for a source with no external fingerprint). Such incidents have no resolve channel — no resolved-alert webhook can ever match them — so their recall opens are recorded for recurrence but never counted toward recall decay (see Event.Resolvable).

Types

type Aggregate

type Aggregate struct {
	Recalls       int
	Resolved      int
	LastConfirmed time.Time
}

Aggregate is a per-entry roll-up of recall episodes: how often the entry was recalled, how often the incident then resolved, and when it last resolved.

type Episode

type Episode struct {
	Kind, Entry, Title, Resource string
	DupFingerprint               string // curator dedup fingerprint; stable join key for the curated-PR resolution check
	OpenedAt, ResolvedAt         time.Time
	Duration                     time.Duration
	Resolved                     bool
}

Episode is a matched open→resolve pair (or, from Episodes(), an unresolved open when Resolved is false).

type Event

type Event struct {
	Event          string `json:"event"`                     // "open" | "resolve"
	Fingerprint    string `json:"fingerprint"`               // Alertmanager fingerprint (stable firing↔resolved)
	DupFingerprint string `json:"dup_fingerprint,omitempty"` // curator dedup fingerprint (resource+cause); the curated-PR resolution join key

	Kind     string    `json:"kind,omitempty"`  // open: "recall" | "fresh"
	Entry    string    `json:"entry,omitempty"` // open+recall: the recalled entry path
	Title    string    `json:"title,omitempty"`
	Resource string    `json:"resource,omitempty"`
	At       time.Time `json:"at"`

	// Recurrence fields (written by the delivery path). Kept omitempty so the
	// append-only file stays backward/forward compatible with older readers.
	TriggerKey string `json:"trigger_key,omitempty"` // groups recurrences of the same alert; keys the byTrigger index
	CuratedURL string `json:"curated_url,omitempty"` // KB link surfaced as "previous: <link>" on recurrence
	Verdict    string `json:"verdict,omitempty"`     // curator's machine verdict on the investigation

	// Resolvable is set on an open when we know whether a ground-truth resolve signal
	// can ever arrive for it: true for sources with a resolve channel (Alertmanager,
	// PagerDuty), false for sources that never emit one (GitOps, reinvestigate, or
	// Alertmanager with send_resolved off). A pointer for a three-state distinction:
	// nil ⇒ the field is absent, i.e. a LEGACY open written before this field existed —
	// those came from Alertmanager/PagerDuty and are treated as resolvable. Only a
	// non-resolvable recall open is excluded from recall decay (see applyOpenLocked).
	Resolvable *bool `json:"resolvable,omitempty"`

	// Checkpoint is set only on a compaction record (Event=="checkpoint"); nil on every
	// open/resolve. It carries the folded aggregate state of the events a compaction
	// dropped. Kept omitempty so normal lines are unaffected, and a distinct event kind
	// so OLD binaries — whose switch has no "checkpoint" case — ignore it gracefully
	// (they lose the pre-horizon aggregates, but do not choke).
	Checkpoint *checkpointData `json:"checkpoint,omitempty"`
}

Event is one ledger line: an investigation opened, or an incident resolved.

type Ledger

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

Ledger is an append-only outcome log with an in-memory open-index and a cached per-entry recall aggregate (the OpenCounts roll-up). The aggregate is built once by replaying the file at New and then maintained INCREMENTALLY under mu on every Open/Resolve, so OpenCounts is O(1) and never re-reads the file — it lived on the recall hot path, which previously replayed the whole JSONL per incident lookup.

func New

func New(path string) (*Ledger, error)

New opens (replaying) the ledger at path with the default compaction bound (DefaultMaxEvents). An empty path returns a disabled, no-op ledger (the feature is off).

func NewWithMaxEvents added in v0.7.0

func NewWithMaxEvents(path string, maxEvents int) (*Ledger, error)

NewWithMaxEvents opens (replaying) the ledger at path, compacting the JSONL on load when it exceeds maxEvents (0 disables compaction). An empty path returns a disabled, no-op ledger.

func (*Ledger) Enabled added in v0.5.0

func (l *Ledger) Enabled() bool

Enabled reports whether the ledger will actually persist events (a non-empty ledger_path was configured); nil-safe. Exported for wiring sites: a disabled ledger's methods silently no-op, so handing it to a consumer that assumes its writes land would misrepresent persistence. Cheaper than Status(), which re-reads the whole file.

func (*Ledger) Episodes

func (l *Ledger) Episodes() ([]Episode, error)

Episodes replays the full ledger and turns every open into an Episode, pairing each resolve with the most-recent (LIFO) unresolved open for the same fingerprint — so recurrence is preserved (N opens + 1 resolve ⇒ N episodes, 1 resolved). Pairing is order-independent: a resolve that arrives BEFORE its open (a transient incident that cleared mid-investigation, so the resolve webhook landed before the open was recorded) is buffered and paired with the next open for that fingerprint. Episodes are returned in open order; all kinds are included. A disabled/empty ledger yields nil.

func (*Ledger) Occurrences added in v0.5.0

func (l *Ledger) Occurrences(triggerKey string) (int, time.Time, string)

Occurrences reports how many investigations have been recorded for a TriggerKey, when the most recent one happened, and its KB link — the recurrence facts the notifier renders. Zero values for a disabled ledger, an empty key, or a never-seen key.

func (*Ledger) Open

func (l *Ledger) Open(e Event) error

Open records that an investigation completed for an incident (fingerprint).

func (*Ledger) OpenCounts

func (l *Ledger) OpenCounts() (map[string]Aggregate, error)

OpenCounts rolls recall episodes up per catalog entry (fresh investigations carry no entry). It is the input to recall decay: resolve-rate ≈ (Resolved+k)/(Recalls+k), and runs on the recall hot path once per incident lookup. It returns the cached aggregate — built once at New and maintained incrementally on every Open/Resolve — so it is O(entries) and never re-reads the file; the value equals a fresh full replay of the ledger for any event sequence below the maxPendingResolvesPerFingerprint cap — above the cap (pathological orphan-resolve load), dropped excess resolves mean the cache and an unbounded Episodes() replay may diverge. A disabled/empty ledger yields an empty (non-nil) map. The returned map is a fresh copy the caller may freely mutate.

Behaviour note: because the read moved to New/Reload, OpenCounts no longer performs file I/O and so can no longer return a read error — any error reading the ledger surfaces at construction (New) or re-sync (Reload) time, not per call. The error return is retained for signature stability and is always nil here.

func (*Ledger) Reload added in v0.3.0

func (l *Ledger) Reload() error

Reload re-replays the ledger file under the write lock and rebuilds the cached aggregate, open-index, and pairing state from scratch. The cache is otherwise maintained incrementally and so only reflects writes made through THIS process: in a multi-replica HA deployment sharing one ledger file, a replica that loses then re-acquires leadership would keep serving its pre-handover cache, missing every open/resolve another replica appended while it was a follower — stale aggregates and thus wrong recall-decay. Call Reload when this process (re-)acquires leadership so it re-syncs with those external writes. It takes the write lock (no torn read against a concurrent OpenCounts). A disabled/nil ledger is a no-op.

func (*Ledger) Resolve

func (l *Ledger) Resolve(fp string, at time.Time) (Episode, bool, error)

Resolve records that an incident's alert cleared. When it matches an open investigation it returns the Episode (with duration + kind) and ok=true.

func (*Ledger) Status

func (l *Ledger) Status() Status

Status reports whether the ledger is configured and whether its file is actually present/non-empty where this process runs. A disabled ledger (path=="") reports Configured=false.

type Status

type Status struct {
	Path       string // the configured ledger path ("" when disabled)
	Configured bool   // a non-empty ledger_path was set
	Present    bool   // the file exists (true even when empty)
	Events     int    // number of replayable events (0 for absent/empty)
}

Status is a cheap snapshot of the ledger's on-disk reality, used to tell apart "feature off" (Configured=false) from "configured but the file the curate pod can see is absent/empty" (Configured=true, Present=false or Events==0) — the silent-no-op the `lore curate` startup warning surfaces. It re-reads the file (no cached open-index) so it reflects what a fresh process actually sees.

Jump to

Keyboard shortcuts

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