Add gauge/counter semantics to metric declarations (ticket 097)

Metrics previously only declared a data type (String/Number/...), so
rate() could be applied to a point-in-time gauge reading and sum()/avg()
could be applied directly to a cumulative counter reading with no guard
against either. Adds a MetricSemantics enum (Gauge, default; Counter)
threaded through DeclareMetricRequest, the engine's MetricDeclaration
data value, profile persistence, the API/MCP declaration routes, and the
Metrics panel's declaration form, then uses it in evaluate_promql to
reject rate() on a declared gauge and bare sum()/avg() on a declared
counter (suggesting rate() first). The guard only applies to metrics
with a captured value_param — bare occurrence-count metrics are always
safe under both operations, so existing dashboards are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
Hugh Kaznowski
2026-09-07 12:30:06 +01:00
co-authored by Claude Sonnet 5
parent c3108677ce
commit 4a0fe92ce8
12 changed files with 299 additions and 55 deletions
@@ -1,50 +0,0 @@
# 097 — Metrics have no gauge/counter distinction, so `rate()`/`sum()`/`avg()` can be applied without a semantics guard
## Summary
`MetricPrimitiveKind` only distinguishes data *type*
(`String`/`Number`/`Timestamp`/`Duration`/`Boolean`), not metric
*semantics* (counter vs. gauge). PromQL's `gauge()` op is a bespoke
query-time "take the last point" function rather than something tied
to how a metric was declared, and nothing stops calling `rate()` on a
metric that's conceptually a gauge, or `sum()`/`avg()` on a
monotonically-increasing counter without rating it first.
## Where
- `src/engine/data_value.rs:70-76` — `MetricPrimitiveKind`:
`String, Number, Timestamp, Duration, Boolean`. No `Counter`/`Gauge`
variant.
- `src/engine/metric_primitive.rs:8-14` — `MetricPrimitive` mirrors
the same type-only distinction (`String, Number(Number), Timestamp,
Duration(i64), Boolean`).
- `src/engine/promql.rs:9-16` — `AggOp` includes `Gauge` as one of six
query-time aggregation ops (`Sum, Avg, Count, Rate, Histogram,
Gauge`), evaluated in `src/engine/snapshot/mod.rs:993-1001` by
taking only `points.last()` per series — a query-side convenience,
not a property of the declared metric.
- The only type guard on aggregation today is
`src/engine/snapshot/mod.rs:888-896`, which rejects `sum`/`avg` on
`MetricPrimitiveKind::String` — there's no equivalent guard for
applying `rate()` to a non-monotonic value or `sum()`/`avg()` to an
un-rated counter.
## Scope
- Add a semantics field to metric declarations (e.g. extend
`MetricPrimitiveKind` with `Counter`/`Gauge` variants, or add a
separate `MetricSemantics` enum alongside the existing type field —
decide which based on whether type and semantics should ever vary
independently for the same declaration).
- Thread that through `DeclareMetricRequest`/`MetricDeclarationRecord`/
`MetricDeclarationDto` (the same three-places pattern noted in this
repo's `CLAUDE.md` for declaration features: `engine.rs` message +
handler, `settings.rs`/`profile.rs` persistence, `api.rs` route,
plus a webapp UI affordance in `Params.tsx`/`Metrics`'s declaration
form).
- Use it to validate PromQL queries at evaluation time (e.g. warn or
reject `rate()` on a declared gauge, or `sum()`/`avg()` directly on a
declared counter without an inner `rate()`), and consider whether
the current ad hoc `gauge()` PromQL wrapper becomes redundant once
the declaration itself carries this information (a bare selector on
a declared gauge could just work the way `gauge()` does today).