Merge ticket 051: allow deleting a declared metric
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
+85
-1
@@ -205,7 +205,7 @@ pub async fn run(mut rx: mpsc::Receiver<EngineMessage>, settings: SettingsHandle
|
||||
let _ = respond_to.send(result);
|
||||
}
|
||||
EngineMessage::RemoveMetricDeclaration { name, respond_to } => {
|
||||
let existed = index.remove(&IndexKey::MetricDeclaration { name }).is_some();
|
||||
let existed = remove_metric_declaration(&mut index, &name);
|
||||
let _ = respond_to.send(existed);
|
||||
}
|
||||
EngineMessage::Query { query, respond_to } => {
|
||||
@@ -516,6 +516,18 @@ fn clear_metric_data(index: &mut BTreeMap<IndexKey, DataValue>, metric_name: &st
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes `name`'s declaration and, per ticket 051, purges its bucket history along with it
|
||||
/// (reusing `clear_metric_data`, same as `update_metric`'s re-backfill does for the old
|
||||
/// declaration) rather than leaving it orphaned — an orphaned bucket would let a stale dashboard
|
||||
/// panel or PromQL query keep silently returning old data by name instead of failing with a clear
|
||||
/// "unknown metric" error (see the declared-or-has-data check in `evaluate_promql`). Returns
|
||||
/// whether a declaration existed, which the caller maps to 404 vs success at the API layer.
|
||||
fn remove_metric_declaration(index: &mut BTreeMap<IndexKey, DataValue>, name: &str) -> bool {
|
||||
let existed = index.remove(&IndexKey::MetricDeclaration { name: name.to_string() }).is_some();
|
||||
clear_metric_data(index, name);
|
||||
existed
|
||||
}
|
||||
|
||||
/// Returns `None` on a parse failure, which the caller treats as "this match doesn't apply".
|
||||
fn parse_metric_primitive(raw: &str, kind: MetricPrimitiveKind) -> Option<MetricPrimitive> {
|
||||
match kind {
|
||||
@@ -1639,6 +1651,20 @@ fn evaluate_promql(
|
||||
start_ms: Option<i64>,
|
||||
end_ms: Option<i64>,
|
||||
) -> Result<Vec<PromqlSeriesDto>, String> {
|
||||
// Ticket 051: a metric with neither a declaration nor any recorded bucket data is genuinely
|
||||
// unknown (as opposed to a declared-but-empty-so-far metric, or an undeclared scraped
|
||||
// Prometheus metric ingested via `--metrics-url`'s `IngestMetricSample`, which never gets a
|
||||
// `MetricDeclaration` but still has bucket data) — most commonly because a dashboard panel
|
||||
// still references a metric that has since been deleted. Failing here with a clear error beats
|
||||
// a panic or a silently-empty result.
|
||||
if !index.contains_key(&IndexKey::MetricDeclaration { name: parsed.metric_name.clone() })
|
||||
&& bucket_values(index, &parsed.metric_name, None).next().is_none()
|
||||
{
|
||||
return Err(format!(
|
||||
"unknown metric \"{}\": no declaration or recorded data for it (was it deleted?)",
|
||||
parsed.metric_name
|
||||
));
|
||||
}
|
||||
if matches!(parsed.agg, Some(AggOp::Sum) | Some(AggOp::Avg))
|
||||
&& !parsed.inner_rate
|
||||
&& declared_value_kind(index, &parsed.metric_name) == Some(MetricPrimitiveKind::String)
|
||||
@@ -2260,6 +2286,64 @@ mod tests {
|
||||
assert!(metric_declarations_range(&index).next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_metric_declaration_reports_whether_it_existed() {
|
||||
let mut index = BTreeMap::new();
|
||||
declare_requests_metric(&mut index);
|
||||
assert!(remove_metric_declaration(&mut index, "requests"));
|
||||
// Removing an already-gone (or never-declared) metric is a clean `false`, not a panic —
|
||||
// this is what the API layer's 404 branch relies on.
|
||||
assert!(!remove_metric_declaration(&mut index, "requests"));
|
||||
assert!(!remove_metric_declaration(&mut index, "never-declared"));
|
||||
}
|
||||
|
||||
/// Ticket 051: deleting a metric purges its bucket history too, so a stale PromQL query
|
||||
/// (e.g. a dashboard panel that still names the deleted metric) fails with a readable
|
||||
/// "unknown metric" error instead of silently returning orphaned data.
|
||||
#[test]
|
||||
fn remove_metric_declaration_purges_bucket_history_so_promql_reports_unknown_metric() {
|
||||
let mut index = BTreeMap::new();
|
||||
declare_requests_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("requests").unwrap();
|
||||
assert!(evaluate_promql(&index, &parsed, None, None).unwrap().len() == 1);
|
||||
|
||||
assert!(remove_metric_declaration(&mut index, "requests"));
|
||||
assert!(bucket_values(&index, "requests", None).next().is_none());
|
||||
|
||||
let err = evaluate_promql(&index, &parsed, None, None).unwrap_err();
|
||||
assert!(err.contains("unknown metric"), "unexpected error message: {err}");
|
||||
}
|
||||
|
||||
/// A metric that was ingested via `--metrics-url` scraping (`IngestMetricSample`) never gets a
|
||||
/// `MetricDeclaration` at all, so the "unknown metric" check must key off having *either* a
|
||||
/// declaration *or* recorded data — not declaration alone — or every scraped metric would be
|
||||
/// rejected as unknown.
|
||||
#[test]
|
||||
fn undeclared_metric_with_bucket_data_is_still_queryable_via_promql() {
|
||||
let mut index = BTreeMap::new();
|
||||
push_bucket_value(
|
||||
&mut index,
|
||||
"scraped_metric",
|
||||
BTreeMap::new(),
|
||||
0,
|
||||
Timestamp::from_millis(0),
|
||||
MetricPrimitive::Number(Number::Float(1.0)),
|
||||
);
|
||||
let parsed = promql::parse_query("scraped_metric").unwrap();
|
||||
let series = evaluate_promql(&index, &parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn promql_rejects_metric_with_no_declaration_and_no_data() {
|
||||
let index = BTreeMap::new();
|
||||
let parsed = promql::parse_query("never_declared").unwrap();
|
||||
let err = evaluate_promql(&index, &parsed, None, None).unwrap_err();
|
||||
assert!(err.contains("unknown metric"), "unexpected error message: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_metric_rejects_when_no_declaration_exists_yet() {
|
||||
let mut index = BTreeMap::new();
|
||||
|
||||
Reference in New Issue
Block a user