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:
co-authored by
Claude Sonnet 5
parent
85fb145be7
commit
e397f9fbdb
+40
-29
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user