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
98 lines
4.7 KiB
Rust
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,
|
|
},
|
|
}
|