Documentation
¶
Overview ¶
Package storage is the persistence layer used by the agent (pattern catalog, shadow log, service registry) and by the incident service (incident history). One Provider is constructed at boot from `storage:` in config.yaml and passed to every consumer that needs to read or write durable state.
The interface is split into two concerns:
- Blob: opaque byte slices keyed by short name. Used by the agent catalog and shadow log, both of which already serialize themselves to JSON. Backends translate the name into a file path / Redis key / row.
- Incident: first-class CRUD-ish operations because the UI lists, filters, and acks incidents.
Backends today:
- file (pkg/storage/file.go) — production
- memory (pkg/storage/memory.go) — tests only
- redis (pkg/storage/redis.go) — config stub, returns ErrUnsupported
- database (pkg/storage/database.go) — config stub
Index ¶
Constants ¶
const MaxIncidentsDefault = 1000
MaxIncidentsDefault is the default rolling cap for the file backend. Older records are dropped on SaveIncident when the cap is exceeded.
Variables ¶
var ErrNotFound = errors.New("storage: not found")
ErrNotFound is returned by Get* methods when the key/id is missing. File and memory backends translate os.ErrNotExist into ErrNotFound so callers can rely on errors.Is(err, storage.ErrNotFound).
var ErrUnsupported = errors.New("storage: backend not implemented")
ErrUnsupported is returned by the redis/database stub backends.
Functions ¶
This section is empty.
Types ¶
type AnalysisRecord ¶ added in v1.4.3
type AnalysisRecord struct {
ID string `json:"id"`
IncidentID string `json:"incident_id"`
RequestedAt time.Time `json:"requested_at"`
RequestedBy string `json:"requested_by,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
Model string `json:"model,omitempty"`
// ToolCalls is the full audit trail of read-only tool invocations
// the agent issued during the run, in execution order.
ToolCalls []AnalysisToolCall `json:"tool_calls,omitempty"`
// Finding is the parsed structured output from the model. Nil when
// the run failed before the model produced JSON.
Finding *core.AIFinding `json:"finding,omitempty"`
// RawResponse is the model's final assistant message. Kept for
// audit / debugging when ParseFinding fails.
RawResponse string `json:"raw_response,omitempty"`
// Status is one of: "ok", "error", "rate_limited".
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
AnalysisRecord is the durable shape of one analyze-mode run. The admin /analyze endpoint creates one per request; the UI lists them per incident.
type AnalysisToolCall ¶ added in v1.4.3
type AnalysisToolCall struct {
Name string `json:"name"`
Args json.RawMessage `json:"args,omitempty"`
Output json.RawMessage `json:"output,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
Error string `json:"error,omitempty"`
}
AnalysisToolCall captures one tool round-trip for the audit log.
type Config ¶
type Config struct {
Type string // file | redis | database (default: file)
File FileOptions
Redis RedisOptions
Database DatabaseOptions
}
Config mirrors the root-level `storage:` block in config.yaml. It is kept here (rather than in pkg/config) so tests in this package don't pull in viper.
type DatabaseOptions ¶
type DatabaseOptions struct {
Driver string // postgres | mysql | sqlite
DSN string
MaxIncidents int
}
DatabaseOptions mirrors the database sub-block of `storage:`. The database backend is config-only today — instantiating it returns ErrUnsupported. The struct exists so the config layer can validate its shape.
type FileOptions ¶
type FileOptions struct {
DataDir string // default "data"
MaxIncidents int // default MaxIncidentsDefault
}
FileOptions configures the file backend. Empty fields fall back to sensible defaults.
type IncidentRecord ¶
type IncidentRecord struct {
ID string `json:"id"`
TeamID string `json:"team_id,omitempty"`
Title string `json:"title,omitempty"`
Source string `json:"source,omitempty"` // "webhook" | "sns" | "sqs" | ...
Service string `json:"service,omitempty"` // best-effort from payload
Resolved bool `json:"resolved"`
// ChannelsEnabled is the snapshot of channels that were configured
// when the alert fired. ChannelsNotified is the subset that
// actually succeeded. The two diverge whenever a channel fails:
// keep both so the UI can show "Slack failed" without losing the
// fact that Slack was supposed to be tried.
ChannelsEnabled []string `json:"channels_enabled,omitempty"`
ChannelsNotified []string `json:"channels_notified,omitempty"`
OnCallTriggered bool `json:"oncall_triggered,omitempty"`
OnCallError string `json:"oncall_error,omitempty"`
// NotifyStatus reflects the outcome of the alert fan-out:
// "pending" — record persisted, fan-out not yet attempted
// "sent" — every enabled channel returned success
// "partial" — at least one channel succeeded, at least one failed
// "failed" — no channel succeeded
NotifyStatus string `json:"notify_status,omitempty"`
NotifyError string `json:"notify_error,omitempty"`
CreatedAt time.Time `json:"created_at"`
AckedAt *time.Time `json:"acked_at,omitempty"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
Content map[string]interface{} `json:"content,omitempty"`
// AssignedTeamID and AssignedMemberIDs record an operator's
// assignment for this incident. Routing logic (Phase 2) will read
// these to pick channels per assignee; the storage layer only
// holds the references. Empty means unassigned.
AssignedTeamID string `json:"assigned_team_id,omitempty"`
AssignedMemberIDs []string `json:"assigned_member_ids,omitempty"`
}
IncidentRecord is the durable shape of an incident. It mirrors the runtime models.Incident plus the audit fields the UI needs (when it happened, who got notified, was it acked, raw payload for debugging).
type Provider ¶
type Provider interface {
// ReadBlob returns the contents previously written under name.
// Missing blobs MUST return (nil, nil) — not ErrNotFound — so the
// agent's "fresh start" path stays a single line.
ReadBlob(name string) ([]byte, error)
// WriteBlob atomically replaces the blob stored under name.
WriteBlob(name string, data []byte) error
// SaveIncident appends a new incident to the store. Subsequent
// SaveIncident calls with the same ID overwrite the existing record
// (used by the ack path). Implementations are responsible for
// trimming the history to a sane upper bound — the file backend
// caps at MaxIncidents.
SaveIncident(rec *IncidentRecord) error
// UpdateIncidentAck stamps an existing incident as acknowledged.
// Returns ErrNotFound when the id is unknown.
UpdateIncidentAck(id string, ackedAt time.Time) error
// GetIncident returns one incident or ErrNotFound.
GetIncident(id string) (*IncidentRecord, error)
// ListIncidents returns the most recent incidents, newest first.
// limit <= 0 returns the full window.
ListIncidents(limit int) ([]*IncidentRecord, error)
// SaveAnalysis stores (or replaces by ID) one analyze-mode run.
SaveAnalysis(rec *AnalysisRecord) error
// GetAnalysis returns one analysis or ErrNotFound.
GetAnalysis(id string) (*AnalysisRecord, error)
// ListAnalysesByIncident returns all analyses for one incident,
// newest first. limit <= 0 returns the full window.
ListAnalysesByIncident(incidentID string, limit int) ([]*AnalysisRecord, error)
// ListAnalyses returns analyses across all incidents, newest first.
// limit <= 0 returns the full window.
ListAnalyses(limit int) ([]*AnalysisRecord, error)
// DeleteAnalysis removes one analysis. Returns ErrNotFound when the
// id is unknown.
DeleteAnalysis(id string) error
// Close releases any underlying resources (file handles, redis
// connections, db pools). Calling Close on a closed provider is a
// no-op.
Close() error
}
Provider is the storage interface used by the agent and incident service.
func NewDatabase ¶
func NewDatabase(_ DatabaseOptions) (Provider, error)
NewDatabase is a placeholder that returns ErrUnsupported.
func NewFile ¶
func NewFile(opts FileOptions) (Provider, error)
NewFile returns a Provider backed by the local filesystem.
func NewMemory ¶
func NewMemory() Provider
NewMemory returns a Provider that keeps all state in memory. Intended for tests; never select via the config factory.
func NewRedis ¶
func NewRedis(_ RedisOptions) (Provider, error)
NewRedis is a placeholder that returns ErrUnsupported. The redis backend will be implemented alongside its first dependency.
type RedisOptions ¶
type RedisOptions struct {
Host string
Port int
Password string
DB int
InsecureSkipVerify bool
KeyPrefix string
MaxIncidents int
}
RedisOptions mirrors the redis sub-block of `storage:`. The redis backend is config-only today — instantiating it returns ErrUnsupported. The struct exists so the config layer can validate its shape.