Add bulk clear-params action that re-extracts from retained logs (ticket 052)

DELETE /api/params clears the entire standalone-param set — manual and
auto-derived alike — then immediately re-runs auto_declare_params against
every IndexKey::IngestedLog entry still held in the engine's index (oldest
to newest), so params come back fresh from currently-retained log text
without requiring new lines to be ingested. This is a deliberate full
reset, unlike reload_engine_declarations's profile-switch cleanup, which
preserves auto-derived params since those are engine-discovered facts
rather than profile state; here the immediate re-extraction step makes
clearing them safe.

Settings::clear_param_declarations empties the active profile's persisted
params so they don't reappear on restart. The webapp's Params section gets
a single "Clear all" button (with a confirm dialog and a status line
reporting removed/re-derived/logs_considered) rather than a per-param
delete loop; logs_considered surfaces that lines already evicted by
max_logs can't regain their params.

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-06 09:39:22 +01:00
co-authored by Claude Sonnet 5
parent d17cfe9b83
commit 4e37e2a7ee
6 changed files with 293 additions and 82 deletions
+131
View File
@@ -36,6 +36,19 @@ pub struct MetricSample {
/// Also the request body for `POST /api/params` — `Deserialize` is derived directly here rather than adding a separate DTO, since the shape already matches `ParamDeclaration`.
pub type DeclareParamRequest = ParamDeclaration;
/// Response to `EngineMessage::ClearParams` (ticket 052) — also the JSON body for `DELETE /api/params`.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ClearParamsResult {
/// How many param declarations (manual + auto-derived) were removed.
pub removed: usize,
/// How many params `auto_declare_params` re-derived immediately afterward from retained log text.
pub reextracted: usize,
/// How many `IndexKey::IngestedLog` entries were still retained (not yet evicted by `max_logs`)
/// and so were re-scanned — surfaced so a caller can tell the user re-extraction only covers
/// logs still held in memory, not lines evicted before the clear.
pub logs_considered: usize,
}
/// A metric references its params by name (resolved against the engine's standalone
/// `ParamDeclaration` store in `apply_metric_to_log`) rather than embedding its own
/// regex/capture-group/kind per param — see ticket 037. Also the request body for
@@ -90,6 +103,16 @@ pub enum EngineMessage {
name: String,
respond_to: oneshot::Sender<bool>,
},
/// Ticket 052: wipes the entire standalone-param set (manual *and* auto-derived — unlike
/// `reload_engine_declarations`'s profile-switch cleanup, this is an explicit user-driven reset,
/// not a "keep engine-discovered facts across profiles" cleanup), then immediately re-runs
/// `auto_declare_params` over every `IndexKey::IngestedLog` entry still retained in the index,
/// oldest to newest, so declaration-order semantics match a fresh ingest. Lines already evicted
/// by `max_logs` can't regain their params — `ClearParamsResult::logs_considered` reports how
/// many retained lines were re-scanned, so a caller can surface that bound to the user.
ClearParams {
respond_to: oneshot::Sender<ClearParamsResult>,
},
DeclareMetric {
request: DeclareMetricRequest,
respond_to: oneshot::Sender<Result<(), String>>,
@@ -195,6 +218,11 @@ pub async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle
let existed = index.remove(&IndexKey::ParamDeclaration { name }).is_some();
let _ = respond_to.send(existed);
}
EngineMessage::ClearParams { respond_to } => {
let removed = clear_all_param_declarations(&mut index);
let (reextracted, logs_considered) = reextract_params_from_retained_logs(&mut index);
let _ = respond_to.send(ClearParamsResult { removed, reextracted, logs_considered });
}
EngineMessage::DeclareMetric { request, respond_to } => {
let result = declare_metric(&mut index, request);
let _ = respond_to.send(result);
@@ -326,6 +354,42 @@ fn auto_declare_params(index: &mut BTreeMap<IndexKey, DataValue>, text: &str) {
}
}
/// Ticket 052: removes every `IndexKey::ParamDeclaration` entry — manual and auto-derived alike.
/// Returns how many were removed. Unlike `RemoveParamDeclaration`'s single-name removal (used by
/// `reload_engine_declarations` during a profile switch, which deliberately keeps auto-derived
/// params since they're engine-discovered facts rather than profile state), this is a full reset:
/// the caller (`EngineMessage::ClearParams`) immediately re-runs `auto_declare_params` afterward, so
/// wiping the auto-derived ones too just means they come back fresh instead of stale.
fn clear_all_param_declarations(index: &mut BTreeMap<IndexKey, DataValue>) -> usize {
let names: Vec<String> =
param_declarations_range(index).map(|(name, ..)| name.to_string()).collect();
let removed = names.len();
for name in names {
index.remove(&IndexKey::ParamDeclaration { name });
}
removed
}
/// Ticket 052: re-runs `auto_declare_params` against every `IndexKey::IngestedLog` entry still held
/// in the index, oldest to newest (matching normal ingest order, so declaration-order semantics —
/// e.g. which key "wins" a naming collision — come out the same as if the lines were freshly
/// ingested). Only ever called right after `clear_all_param_declarations`, so every param produced
/// here is fresh, not a re-declaration.
///
/// Returns `(reextracted, logs_considered)`: how many params came back, and how many retained log
/// lines were re-scanned. Lines already dropped by `evict_excess_logs` before this ran aren't in the
/// index anymore and so can't contribute — `logs_considered` is how the caller surfaces that bound
/// to the user, rather than the re-extraction silently looking complete when it isn't.
fn reextract_params_from_retained_logs(index: &mut BTreeMap<IndexKey, DataValue>) -> (usize, usize) {
let texts: Vec<String> = logs_range(index).map(|(_, _, text)| text.to_string()).collect();
let logs_considered = texts.len();
for text in &texts {
auto_declare_params(index, text);
}
let reextracted = param_declarations_range(index).count();
(reextracted, logs_considered)
}
/// Validates the metric's own regex plus that every name in `label_params`/`value_param` refers to
/// an already-declared standalone param (not just the first bad one, so a rejection reports every
/// problem at once). Shared by `declare_metric` and `update_metric` so both reject a bad
@@ -1928,6 +1992,73 @@ mod tests {
assert_eq!(approx_bytes_total, 3);
}
fn insert_ingested_log(index: &mut BTreeMap<IndexKey, DataValue>, sequence: u64, text: &str) {
index.insert(
IndexKey::IngestedLog { sequence, timestamp: Timestamp::from_millis(sequence as i64) },
DataValue::LogLiteral { text: text.to_string() },
);
}
#[test]
fn clear_all_param_declarations_removes_manual_and_auto_derived() {
let mut index = BTreeMap::new();
auto_declare_params(&mut index, "x1=-4");
declare_param(&mut index, request("level", r"level=(?P<level>\w+)", "level")).unwrap();
assert_eq!(param_declarations_range(&index).count(), 2);
let removed = clear_all_param_declarations(&mut index);
assert_eq!(removed, 2);
assert_eq!(param_declarations_range(&index).count(), 0);
}
#[test]
fn reextract_params_from_retained_logs_rebuilds_from_ingested_text_without_new_ingest() {
let mut index = BTreeMap::new();
insert_ingested_log(&mut index, 0, r#"level=info msg="started""#);
insert_ingested_log(&mut index, 1, "x1=-4 y1=34");
auto_declare_params(&mut index, r#"level=info msg="started""#);
auto_declare_params(&mut index, "x1=-4 y1=34");
assert_eq!(param_declarations_range(&index).count(), 4);
clear_all_param_declarations(&mut index);
assert_eq!(param_declarations_range(&index).count(), 0);
// No new lines ingested — re-extraction works purely off the `IngestedLog` entries already
// retained in the index (ticket 052's core acceptance criterion).
let (reextracted, logs_considered) = reextract_params_from_retained_logs(&mut index);
assert_eq!(logs_considered, 2);
assert_eq!(reextracted, 4);
let declarations: HashMap<&str, _> =
param_declarations_range(&index).map(|d| (d.0, d)).collect();
assert!(declarations.contains_key("level"));
assert!(declarations.contains_key("msg"));
assert!(declarations.contains_key("x1"));
assert!(declarations.contains_key("y1"));
assert!(declarations.values().all(|d| d.6), "re-extracted params must be flagged auto_derived");
}
#[test]
fn reextract_params_from_retained_logs_only_covers_lines_still_in_the_index() {
// Simulates a line evicted (by `max_logs`) before the clear+re-extract runs: its key/value
// pair is simply gone from the index, same as `evict_excess_logs` leaves things, so
// `auto_declare_params` never sees it again.
let mut index = BTreeMap::new();
insert_ingested_log(&mut index, 1, "x1=-4");
auto_declare_params(&mut index, "evicted_only_key=1");
auto_declare_params(&mut index, "x1=-4");
assert_eq!(param_declarations_range(&index).count(), 2);
clear_all_param_declarations(&mut index);
let (reextracted, logs_considered) = reextract_params_from_retained_logs(&mut index);
assert_eq!(logs_considered, 1, "only the retained log line should be re-scanned");
assert_eq!(reextracted, 1, "the evicted line's key can't come back");
let declarations: Vec<_> = param_declarations_range(&index).collect();
assert_eq!(declarations[0].0, "x1");
}
#[test]
fn declarations_stay_within_their_range() {
let mut index = BTreeMap::new();