Files
nmon/internal/alert/source.go
Vibe Coder b529423324 Add custom alert rules built from metrics and inventory fields
The predefined rules are too constraining: an ISP cannot say "CPU over
80% on servers for 15 minutes" or "uplink without a description", and
every new need meant editing config.yaml. Admins now build their own
rules; the shipped ones stay as read-only built-in macros with their
names unchanged, so v1.1 policies and alert history keep working.

Custom rules live in a new alert_rules table (migration 13) and are
builder-only. A metric rule names a catalogue metric (every pollable
profile metric plus push-ingested names) with a transform (value, rate,
percent_of), window, function, aggregate, comparison and threshold; the
pure internal/alertrule package compiles it to MetricsQL from fixed
templates, so no user text reaches a query. An inventory rule ANDs one to
five conditions over whitelisted device or port fields and is evaluated
in Go over store rows. The evaluator takes its rule set from a function
each cycle, so edits apply live, and both kinds use the existing state
machine, policies, suppression and notifications. Comparisons now cover
> >= < <= == != everywhere. utilisation is not offered: port speed is not
a stored series. config.yaml rules still work but are deprecated.

API (additive): rule list gains id/kind/editable/spec/expr, plus get,
create, update, delete, catalogue, test (viewer, read-only) and copy of
a built-in (cpu_high, ups_on_battery, printer_supply_empty) to a custom
rule. Deleting or renaming a rule that a policy names is a 409. The UI
gets an Alert rules tab with a builder dialog, a test button and the
generated query, and the policy dialog lists custom rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
2026-10-24 00:17:48 +00:00

50 lines
1.4 KiB
Go

package alert
import (
"context"
"time"
)
// Reading is one entity's measured value, as a Source reports it.
type Reading struct {
Device, Iface, Component string
Value float64
}
// Source supplies a rule's readings from somewhere other than the metric store. The app implements it for
// inventory rules, which are evaluated in Go over device and interface rows. Like a query result it must
// include entities that do not breach (value below the threshold), because a missing entity is "no data", not
// "resolved".
type Source interface {
Read(ctx context.Context, now time.Time) ([]Reading, error)
}
// ProbeReading is a reading and whether it breaches the rule's own threshold.
type ProbeReading struct {
Reading
Breach bool
}
// Probe runs one rule once, without touching alert state or any policy, and reports what it sees. The API's
// rule test uses it to show an admin what a draft rule would do.
func Probe(ctx context.Context, q Querier, storeName string, r Rule, now time.Time) ([]ProbeReading, error) {
c, err := compile(r)
if err != nil {
return nil, err
}
var rds []Reading
if r.Source != nil {
rds, err = r.Source.Read(ctx, now)
} else {
rds, err = readMetric(ctx, q, storeName, c, now)
}
if err != nil {
return nil, err
}
out := make([]ProbeReading, 0, len(rds))
for _, rd := range rds {
out = append(out, ProbeReading{Reading: rd, Breach: c.breach(rd.Value, r.Threshold)})
}
return out, nil
}