Files
ottyl/src/data_value.rs
T
Hugh KaznowskiandClaude Sonnet 5 e397f9fbdb Unify param declarations: metrics reference params by name (ticket 037)
Collapse the Params-panel ParamDeclaration and the metric-embedded
RegexToMetricPrimitiveMatcher into a single ParamDeclaration type
(name, regex/capture-group-or-json-field-path, kind). MetricDeclaration
now references params by name via label_params: Vec<String> and
value_param: Option<String> instead of embedding its own extractor per
param, so a param declared once in the Params panel can be reused as a
label or value by any number of metrics. Declaring a metric that
references an undeclared param name is now rejected at declare-time.

Backend:
- src/data_value.rs: unified ParamDeclaration struct; MetricDeclaration
  data value now carries label_params/value_param instead of a Vec of
  inline matchers.
- src/engine.rs: DeclareParamRequest is now an alias for
  ParamDeclaration; DeclareMetricRequest carries label_params/
  value_param; validate_metric_request checks referenced param names
  against the declared-param store; apply_metric_to_log resolves
  label_params/value_param against a per-ingest resolved-param map
  instead of reading an inline matcher list.
- src/profile.rs: MetricDeclarationRecord carries label_params/
  value_param plus a private, serialize-skipped legacy_params field
  that only ever gets populated by deserializing a pre-037 profile
  file's old "params" key. ProfileStore::load migrates any such legacy
  metric into the unified shape (reusing an existing same-shaped bare
  param by name, or creating one implicitly named
  "<metric_name>.<param_name>"), then persists the migrated store so
  the on-disk file only carries the legacy shape once. Migration is
  idempotent (deterministic naming) and covered by dedicated tests.
- src/api.rs / src/dto.rs: MetricDeclarationDto and the declare/update
  metric response bodies updated to the new shape.

Frontend:
- webapp/src/lib/api.ts: MetricDeclaration type now has label_params/
  value_param; MetricParamMatcher/ParamRole types removed.
- webapp/src/components/Metrics.tsx: the declaration form now fetches
  declared params and lets you check off label params / pick a value
  param from that list, instead of re-entering a regex/capture-group/
  kind per metric param.

Leaves ticket 038 (duration as a derived param kind) and 039 (param
query resolving declared params) untouched, as scoped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
2026-09-05 22:41:14 +01:00

98 lines
4.7 KiB
Rust

use crate::metric_primitive::MetricPrimitive;
use crate::timestamp::Timestamp;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// A named, standalone parameter: how to pull a value out of a matching log line, and what kind it
/// parses as. Ticket 037 unified this with the metric-declaration-embedded matcher that used to
/// exist alongside it (`RegexToMetricPrimitiveMatcher`) — a metric now references params by name
/// (`MetricDeclaration::label_params`/`value_param`) instead of redeclaring its own extractor per
/// param, so a param declared once (e.g. via the Params panel) can be reused as a label by one
/// metric and as the value by another without re-entering its regex/capture-group/kind.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParamDeclaration {
pub name: String,
pub message_match_regex: String,
pub capture_group: String,
/// Alternative extraction source to `capture_group`: a dot-separated JSON field path (e.g.
/// `foo.bar`), read from lines that parse as a JSON object instead of via the regex capture
/// group. When present and non-empty, this takes precedence over `capture_group`.
/// `#[serde(default)]` so profile files/requests saved before this field existed still load
/// as plain regex-capture-group params.
#[serde(default)]
pub json_field_path: Option<String>,
pub kind: MetricPrimitiveKind,
}
/// Reads a value out of a JSON object by dot-separated field path (e.g. `"foo.bar"`), for
/// [`ParamDeclaration::json_field_path`]. Returns `None` when `text` doesn't parse as a JSON
/// object, the path doesn't resolve to a value, or the resolved value isn't a scalar
/// (string/number/bool) — there's no sensible flat text form of a nested object/array to feed into
/// `parse_metric_primitive`.
pub fn extract_json_field(text: &str, field_path: &str) -> Option<String> {
let root: serde_json::Value = serde_json::from_str(text).ok()?;
let mut current = root.as_object()?;
let mut segments = field_path.split('.').peekable();
loop {
let segment = segments.next()?;
let next_value = current.get(segment)?;
if segments.peek().is_none() {
return match next_value {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::Bool(b) => Some(b.to_string()),
_ => None,
};
}
current = next_value.as_object()?;
}
}
/// `rename_all = "lowercase"` matches other `kind`-style enums, so JSON bodies use "string"/"number"/"timestamp".
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MetricPrimitiveKind {
String,
Number,
Timestamp,
/// On a metric's Value param this is derived as elapsed time since the previous event (not parsed from text); otherwise it's a literal i64 millisecond count.
Duration,
/// A standalone param declared with this kind doesn't require (or use) a capture group: its value is
/// simply whether `message_match_regex` matches the line, case-insensitively or per the regex's own
/// flags — see `crate::engine::derive_log_params`. Unlike other kinds it's present as `true`/`false`
/// on every line rather than omitted when unmatched.
Boolean,
}
/// Value type paired with [`crate::index_key::IndexKey`] in the engine's primary index.
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize)]
pub enum DataValue {
/// Paired with `IndexKey::IngestedLog`.
LogLiteral { text: String },
/// Paired with `IndexKey::ParamDeclaration`.
ParamDeclaration(ParamDeclaration),
/// Paired with `IndexKey::MetricDeclaration`. `label_params`/`value_param` reference standalone
/// `ParamDeclaration`s by name (resolved against the engine's param store in
/// `crate::engine::apply_metric_to_log`) rather than embedding their own extractor.
MetricDeclaration {
message_match_regex: String,
label_params: Vec<String>,
/// At most one value param per metric; its primitive becomes the event's bucket value instead of a label.
value_param: Option<String>,
/// See `crate::engine::DeclareMetricRequest::matched_labels`.
matched_labels: Option<BTreeMap<String, String>>,
/// See `crate::engine::DeclareMetricRequest::unmatched_labels`.
unmatched_labels: Option<BTreeMap<String, String>>,
},
/// Paired with `IndexKey::Bucket`.
BucketValue {
values: Vec<(u64, Timestamp, MetricPrimitive)>,
},
/// Paired with `IndexKey::LastMetricEvent`.
LastEventTimestamp {
timestamp: Timestamp,
},
}