Add Duration-type metric param: elapsed time since previous same-label match (ticket 023)

MetricPrimitiveKind::Duration/MetricPrimitive::Duration(i64 ms) is a
new primitive kind. On a metric's Value-role param (ticket 022), kind
Duration is special-cased in apply_metric_to_log: instead of parsing
the captured text, the value is derived as elapsed time since the
previous counter event with the same label set, using the ingestion
timestamp. That "previous event" lookup is tracked via a new
IndexKey::LastMetricEvent/DataValue::LastEventTimestamp bookkeeping
entry rather than Bucket.values itself, because a label set's first
occurrence has nothing to diff against and pushes no Bucket entry at
all (per spec: omit, don't emit a bogus zero) — but it still needs to
be recorded so the *next* occurrence has something to diff against.

Duration values compose with the existing bucket/PromQL surface
unchanged (coerce_to_f64, filter comparisons, histogram()/gauge()).
On any other param (Label-role metric param, or a standalone
ParamDeclaration), Duration kind instead parses captured text as a
literal i64 millisecond count, same as Number. Profile round-trip
(ProfileMetricPrimitiveKind) and the Metrics.tsx declaration form are
updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 15:24:28 +01:00
co-authored by Claude Sonnet 5
parent 0c57ed2953
commit 8faf12f819
7 changed files with 165 additions and 6 deletions
+7
View File
@@ -10,6 +10,10 @@ pub enum MetricPrimitive {
String(String),
Number(Number),
Timestamp(Timestamp),
/// Milliseconds elapsed (ticket 023). Distinct from `Number` so a
/// duration reads unambiguously in JSON/label form, even though it's
/// stored the same way (a plain `i64`).
Duration(i64),
}
impl PartialEq for MetricPrimitive {
@@ -32,12 +36,14 @@ impl Ord for MetricPrimitive {
MetricPrimitive::String(_) => 0,
MetricPrimitive::Number(_) => 1,
MetricPrimitive::Timestamp(_) => 2,
MetricPrimitive::Duration(_) => 3,
}
}
match (self, other) {
(MetricPrimitive::String(a), MetricPrimitive::String(b)) => a.cmp(b),
(MetricPrimitive::Number(a), MetricPrimitive::Number(b)) => a.cmp(b),
(MetricPrimitive::Timestamp(a), MetricPrimitive::Timestamp(b)) => a.cmp(b),
(MetricPrimitive::Duration(a), MetricPrimitive::Duration(b)) => a.cmp(b),
(a, b) => rank(a).cmp(&rank(b)),
}
}
@@ -54,6 +60,7 @@ impl fmt::Display for MetricPrimitive {
MetricPrimitive::Number(Number::Int(n)) => write!(f, "{n}"),
MetricPrimitive::Number(Number::Float(n)) => write!(f, "{n}"),
MetricPrimitive::Timestamp(ts) => write!(f, "{}", ts.as_millis()),
MetricPrimitive::Duration(ms) => write!(f, "{ms}"),
}
}
}