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
This commit is contained in:
Hugh Kaznowski
2026-09-05 22:41:14 +01:00
co-authored by Claude Sonnet 5
parent 85fb145be7
commit e397f9fbdb
7 changed files with 755 additions and 594 deletions
+40 -29
View File
@@ -280,7 +280,8 @@ async fn declare_metric(
let dto = MetricDeclarationDto {
name: request.name,
message_match_regex: request.message_match_regex,
params: request.params,
label_params: request.label_params,
value_param: request.value_param,
matched_labels: request.matched_labels,
unmatched_labels: request.unmatched_labels,
};
@@ -313,7 +314,8 @@ async fn update_metric(
let dto = MetricDeclarationDto {
name: request.name,
message_match_regex: request.message_match_regex,
params: request.params,
label_params: request.label_params,
value_param: request.value_param,
matched_labels: request.matched_labels,
unmatched_labels: request.unmatched_labels,
};
@@ -906,9 +908,29 @@ mod tests {
assert!(body["error"].as_str().unwrap().contains("invalid regex"));
}
/// Declares a standalone param via `POST /api/params` — most metric-declaration tests need one
/// or more already-declared params to reference by name (ticket 037), rather than embedding a
/// regex/capture-group/kind inline in the metric request.
async fn declare_test_param(app: &Router, name: &str, pattern: &str, capture_group: &str, kind: &str) {
let (status, _body) = send_json(
app,
"POST",
"/api/params",
json!({
"name": name,
"message_match_regex": pattern,
"capture_group": capture_group,
"kind": kind,
}),
)
.await;
assert_eq!(status, StatusCode::CREATED);
}
#[tokio::test]
async fn declares_metric_and_shows_up_in_list() {
let (app, _engine, _settings, _dir) = test_app();
declare_test_param(&app, "path", r"request path=(?P<path>\S+)", "path", "string").await;
let (status, body) = send_json(
&app,
"POST",
@@ -916,9 +938,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "path", "capture_group": "path", "kind": "string" }
]
"label_params": ["path"]
}),
)
.await;
@@ -933,7 +953,7 @@ mod tests {
}
#[tokio::test]
async fn rejects_metric_with_unknown_capture_group() {
async fn rejects_metric_referencing_undeclared_param() {
let (app, _engine, _settings, _dir) = test_app();
let (status, body) = send_json(
&app,
@@ -942,9 +962,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "duration", "capture_group": "duration", "kind": "number" }
]
"label_params": ["duration"]
}),
)
.await;
@@ -955,6 +973,8 @@ mod tests {
#[tokio::test]
async fn updates_metric_declaration_in_place_rather_than_duplicating() {
let (app, _engine, _settings, _dir) = test_app();
declare_test_param(&app, "path", r"request path=(?P<path>\S+)", "path", "string").await;
declare_test_param(&app, "duration", r"duration=(?P<duration>\d+)", "duration", "number").await;
send_json(
&app,
"POST",
@@ -962,9 +982,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "path", "capture_group": "path", "kind": "string" }
]
"label_params": ["path"]
}),
)
.await;
@@ -976,23 +994,20 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+) duration=(?P<duration>\d+)",
"params": [
{ "param_name": "path", "capture_group": "path", "kind": "string" },
{ "param_name": "duration", "capture_group": "duration", "kind": "number" }
]
"label_params": ["path", "duration"]
}),
)
.await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body["name"], "requests");
assert_eq!(body["params"].as_array().unwrap().len(), 2);
assert_eq!(body["label_params"].as_array().unwrap().len(), 2);
// Still exactly one declaration named "requests", now with the updated regex/params — not a second declaration alongside the original.
let (status, body) = get_json(&app, "/api/metrics").await;
assert_eq!(status, StatusCode::OK);
let metrics = body.as_array().unwrap();
assert_eq!(metrics.len(), 1);
assert_eq!(metrics[0]["params"].as_array().unwrap().len(), 2);
assert_eq!(metrics[0]["label_params"].as_array().unwrap().len(), 2);
}
#[tokio::test]
@@ -1005,7 +1020,7 @@ mod tests {
json!({
"name": "does-not-exist",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": []
"label_params": []
}),
)
.await;
@@ -1023,7 +1038,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": []
"label_params": []
}),
)
.await;
@@ -1035,9 +1050,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "duration", "capture_group": "duration", "kind": "number" }
]
"label_params": ["duration"]
}),
)
.await;
@@ -1049,6 +1062,7 @@ mod tests {
async fn queries_bucket_values_with_label_filter_and_range() {
let (app, engine, _settings, _dir) = test_app();
declare_test_param(&app, "path", r"request path=(?P<path>\S+)", "path", "string").await;
send_json(
&app,
"POST",
@@ -1056,9 +1070,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "path", "capture_group": "path", "kind": "string" }
]
"label_params": ["path"]
}),
)
.await;
@@ -1103,6 +1115,7 @@ mod tests {
async fn promql_query_returns_series_for_seeded_fixture() {
let (app, engine, _settings, _dir) = test_app();
declare_test_param(&app, "path", r"request path=(?P<path>\S+)", "path", "string").await;
send_json(
&app,
"POST",
@@ -1110,9 +1123,7 @@ mod tests {
json!({
"name": "requests",
"message_match_regex": r"request path=(?P<path>\S+)",
"params": [
{ "param_name": "path", "capture_group": "path", "kind": "string" }
]
"label_params": ["path"]
}),
)
.await;