Make elapsed-since-last an explicit param extractor, not a Duration-kind special case (ticket 038)
Previously a Value-role param with kind: Duration silently meant "elapsed time since the previous same-labeled event" instead of parsed text — the same MetricPrimitiveKind::Duration variant meant two unrelated things depending on which list (label_params vs value_param) referenced it. Add ParamDeclaration::elapsed_since_last, a third extraction source alongside capture_group/json_field_path: when set, the param is computed (via a new resolve_param_value, used uniformly for label_params and value_param) rather than text-extracted, and declare_param validates it's paired with kind: Duration. apply_metric_to_log no longer branches on (role, kind) — it dispatches purely on what each resolved param declares. Migrate existing profiles: migrate_duration_value_params converts any Duration-kind param referenced as a metric's value_param into the new extractor, and resolve_or_create_legacy_param (ticket 037's pre-existing migration) now carries the same meaning forward for pre-037 inline Value-role Duration matchers. Params.tsx offers "Elapsed since previous event" as its own Extraction option (hides the now-irrelevant message-match-regex/capture-group fields and forces kind to duration); Metrics.tsx labels such params accordingly wherever they're listed as label/value param choices. 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
375aa38ef8
commit
5e30e02a78
+128
-1
@@ -69,6 +69,11 @@ pub struct ParamDeclarationRecord {
|
||||
#[serde(default)]
|
||||
pub json_field_path: Option<String>,
|
||||
pub kind: ProfileMetricPrimitiveKind,
|
||||
/// See `crate::data_value::ParamDeclaration::elapsed_since_last`; `#[serde(default)]` so
|
||||
/// profiles saved before ticket 038 (where a `Duration`-kind value param implicitly meant this)
|
||||
/// still load — `migrate_duration_value_params` below converts those.
|
||||
#[serde(default)]
|
||||
pub elapsed_since_last: bool,
|
||||
}
|
||||
|
||||
impl From<&DeclareParamRequest> for ParamDeclarationRecord {
|
||||
@@ -79,6 +84,7 @@ impl From<&DeclareParamRequest> for ParamDeclarationRecord {
|
||||
capture_group: request.capture_group.clone(),
|
||||
json_field_path: request.json_field_path.clone(),
|
||||
kind: request.kind.into(),
|
||||
elapsed_since_last: request.elapsed_since_last,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +97,7 @@ impl From<&ParamDeclarationRecord> for DeclareParamRequest {
|
||||
capture_group: record.capture_group.clone(),
|
||||
json_field_path: record.json_field_path.clone(),
|
||||
kind: record.kind.into(),
|
||||
elapsed_since_last: record.elapsed_since_last,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,11 +243,44 @@ fn resolve_or_create_legacy_param(
|
||||
capture_group: matcher.capture_group.clone(),
|
||||
json_field_path: matcher.json_field_path.clone(),
|
||||
kind: matcher.kind,
|
||||
// Pre-037, a Value-role Duration matcher implicitly meant "elapsed time since the
|
||||
// previous same-labeled event" (ticket 038's now-explicit `elapsed_since_last`) — carry
|
||||
// that meaning forward rather than dropping it into a meaningless literal-parse matcher.
|
||||
elapsed_since_last: matcher.role == LegacyParamRole::Value
|
||||
&& matcher.kind == ProfileMetricPrimitiveKind::Duration,
|
||||
});
|
||||
}
|
||||
implicit_name
|
||||
}
|
||||
|
||||
/// Migrates every already-declared `Duration`-kind param that's referenced as some metric's
|
||||
/// `value_param` into the explicit `elapsed_since_last` extractor (ticket 038). Before ticket 038, a
|
||||
/// `Duration`-kind value param implicitly meant "elapsed time since the previous same-labeled
|
||||
/// event" (see `crate::engine::apply_metric_to_log`'s pre-038 history) — this migration makes that
|
||||
/// meaning explicit so `Duration` kind no longer has a role-dependent special case. Only params
|
||||
/// actually referenced as *some* metric's value param are converted: a `Duration`-kind param used
|
||||
/// purely as a label param already meant a literal parsed millisecond count, and keeps meaning that.
|
||||
/// Runs after `migrate_legacy_metric_params` (which populates `value_param` from pre-037
|
||||
/// `legacy_params` and already sets `elapsed_since_last` correctly for those via
|
||||
/// `resolve_or_create_legacy_param`), so this only has new work to do for profiles saved between
|
||||
/// ticket 037 and ticket 038 landing, where `value_param` already pointed at a bare `Duration`-kind
|
||||
/// param straight from the unified shape. Returns whether anything changed.
|
||||
fn migrate_duration_value_params(profile: &mut Profile) -> bool {
|
||||
let value_param_names: std::collections::HashSet<&str> =
|
||||
profile.metrics.iter().filter_map(|metric| metric.value_param.as_deref()).collect();
|
||||
let mut migrated = false;
|
||||
for param in &mut profile.params {
|
||||
if !param.elapsed_since_last
|
||||
&& param.kind == ProfileMetricPrimitiveKind::Duration
|
||||
&& value_param_names.contains(param.name.as_str())
|
||||
{
|
||||
param.elapsed_since_last = true;
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
migrated
|
||||
}
|
||||
|
||||
/// A chart type a dashboard panel can render; kept in sync with the frontend's `'bar' | 'line' | 'pie'` union (see `webapp/src/lib/api.ts`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -485,9 +525,17 @@ impl ProfileStore {
|
||||
// load; persisting the result isn't required for correctness (migration re-runs
|
||||
// identically next time), but it's done here anyway so the on-disk file only carries the
|
||||
// legacy `"params"` shape once, on the very next save.
|
||||
//
|
||||
// Ticket 038: follows on immediately after — once `migrate_legacy_metric_params` has
|
||||
// resolved every metric's `value_param`, convert any `Duration`-kind param still referenced
|
||||
// that way into the explicit `elapsed_since_last` extractor, so a profile saved either before
|
||||
// 037 (inline matchers) or between 037 and 038 (bare `value_param` reference) ends up
|
||||
// behaving identically under the new, explicit shape.
|
||||
let mut any_migrated = false;
|
||||
for profile in &mut store.profiles {
|
||||
if migrate_legacy_metric_params(profile) {
|
||||
let legacy_migrated = migrate_legacy_metric_params(profile);
|
||||
let duration_migrated = migrate_duration_value_params(profile);
|
||||
if legacy_migrated || duration_migrated {
|
||||
any_migrated = true;
|
||||
}
|
||||
}
|
||||
@@ -766,6 +814,7 @@ mod tests {
|
||||
capture_group: capture_group.to_string(),
|
||||
json_field_path: None,
|
||||
kind: MetricPrimitiveKind::String,
|
||||
elapsed_since_last: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,6 +1332,84 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
/// Acceptance criterion (ticket 038): a pre-037 legacy profile whose inline `Value`-role matcher
|
||||
/// used `kind: "duration"` (the old "elapsed time since previous event" special case) migrates
|
||||
/// into a standalone param with the new explicit `elapsed_since_last: true` extractor, not just a
|
||||
/// bare `Duration`-kind param that would now (post-038) mean a literal parsed millisecond count.
|
||||
#[test]
|
||||
fn migrates_legacy_value_role_duration_matcher_into_elapsed_since_last_param() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store_path = dir.path().join("profiles.json");
|
||||
write_legacy_profiles_json(
|
||||
&store_path,
|
||||
r#"[
|
||||
{
|
||||
"name": "legacy",
|
||||
"params": [],
|
||||
"metrics": [
|
||||
{
|
||||
"name": "login_gap",
|
||||
"message_match_regex": "user=(?P<user>\\S+) logged in",
|
||||
"params": [
|
||||
{ "param_name": "user", "capture_group": "user", "kind": "string", "role": "label" },
|
||||
{ "param_name": "gap", "capture_group": "user", "kind": "duration", "role": "value" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"dashboards": []
|
||||
}
|
||||
]"#,
|
||||
);
|
||||
|
||||
let store = ProfileStore::load(&store_path);
|
||||
let profile = store.get("legacy").unwrap();
|
||||
|
||||
let gap_param = profile.params.iter().find(|p| p.name == "login_gap.gap").unwrap();
|
||||
assert!(matches!(gap_param.kind, ProfileMetricPrimitiveKind::Duration));
|
||||
assert!(gap_param.elapsed_since_last);
|
||||
assert_eq!(profile.metrics[0].value_param, Some("login_gap.gap".to_string()));
|
||||
}
|
||||
|
||||
/// Acceptance criterion (ticket 038): a profile saved after ticket 037 landed but before ticket
|
||||
/// 038 — so it already carries the unified `label_params`/`value_param` shape rather than legacy
|
||||
/// inline matchers, with a bare `Duration`-kind param referenced as a metric's `value_param` — is
|
||||
/// migrated the same way: the param gains `elapsed_since_last: true` so its behavior is unchanged
|
||||
/// (elapsed time since the previous same-labeled event) under the new explicit extractor.
|
||||
#[test]
|
||||
fn migrates_post_037_bare_duration_value_param_into_elapsed_since_last() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store_path = dir.path().join("profiles.json");
|
||||
write_legacy_profiles_json(
|
||||
&store_path,
|
||||
r#"[
|
||||
{
|
||||
"name": "legacy",
|
||||
"params": [
|
||||
{ "name": "user", "message_match_regex": "user=(?P<user>\\S+) logged in", "capture_group": "user", "kind": "string" },
|
||||
{ "name": "gap", "message_match_regex": "user=(?P<user>\\S+) logged in", "capture_group": "user", "kind": "duration" }
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"name": "login_gap",
|
||||
"message_match_regex": "user=(?P<user>\\S+) logged in",
|
||||
"label_params": ["user"],
|
||||
"value_param": "gap"
|
||||
}
|
||||
],
|
||||
"dashboards": []
|
||||
}
|
||||
]"#,
|
||||
);
|
||||
|
||||
let store = ProfileStore::load(&store_path);
|
||||
let profile = store.get("legacy").unwrap();
|
||||
|
||||
let gap_param = profile.params.iter().find(|p| p.name == "gap").unwrap();
|
||||
assert!(gap_param.elapsed_since_last);
|
||||
// A Duration-kind param not referenced as any value_param is left untouched (still a literal parse).
|
||||
assert!(!profile.params.iter().find(|p| p.name == "user").unwrap().elapsed_since_last);
|
||||
}
|
||||
|
||||
/// When a bare-named param already declared on the profile matches a legacy matcher's shape
|
||||
/// exactly (same regex/capture-group/kind), migration reuses it by name instead of creating a
|
||||
/// redundant `<metric>.<param>` declaration.
|
||||
|
||||
Reference in New Issue
Block a user