fix(promql): drop metadata labels from range function results

Prometheus drops __name__, __type__ and __unit__ from the result of every
range function except last_over_time and first_over_time; the evaluator
passed the metric through unchanged.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
This commit is contained in:
Dennis Zhuang
2026-10-17 22:30:39 +08:00
parent 3ff496da1e
commit 2bf0bf4247
3 changed files with 75 additions and 4 deletions
+13 -2
View File
@@ -70,8 +70,12 @@ pub struct RangeEvaluator {
series: Option<SeriesState>,
fields: Vec<FieldSamples>,
row: Vec<Option<f64>>,
drops_metric_name: bool,
}
/// Prometheus `schema.IsMetadataLabel`: the labels removed with the metric name.
const METADATA_LABELS: [&str; 3] = ["__name__", "__type__", "__unit__"];
struct SeriesState {
primary_key: Buffer,
metric: MetricName,
@@ -119,7 +123,10 @@ impl RangeEvaluator {
}
}
}
Self::with_kernel(input, grid, window, function.kernel(window.offset_ms))
let mut evaluator =
Self::with_kernel(input, grid, window, function.kernel(window.offset_ms))?;
evaluator.drops_metric_name = function.drops_metric_name();
Ok(evaluator)
}
pub(crate) fn with_kernel(
@@ -156,6 +163,7 @@ impl RangeEvaluator {
.map(|_| FieldSamples::default())
.collect(),
row: Vec::with_capacity(input.fields.len()),
drops_metric_name: false,
})
}
@@ -182,12 +190,15 @@ impl RangeEvaluator {
return Ok(None);
}
self.evaluate(None)?;
let state = self.series.take().expect("checked by check_chunk");
let mut state = self.series.take().expect("checked by check_chunk");
for field in &mut self.fields {
field.timestamps.clear();
field.values.clear();
field.head = 0;
}
if self.drops_metric_name {
state.metric.remove_all(&METADATA_LABELS);
}
self.output.finish(state.metric)
}
+6
View File
@@ -53,6 +53,12 @@ pub enum RangeFunction {
}
impl RangeFunction {
/// Whether the result drops the metric name, as Prometheus does for every range function
/// except `last_over_time` and `first_over_time`.
pub(crate) fn drops_metric_name(self) -> bool {
!matches!(self, Self::LastOverTime)
}
/// Returns the kernel computing this function over windows read with `offset_ms`.
pub(crate) fn kernel(self, offset_ms: i64) -> WindowKernel {
match self {
+56 -2
View File
@@ -99,6 +99,15 @@ fn probe_evaluator(fields: usize, grid: Arc<EvalGrid>, window: RangeWindow) -> R
}
/// Builds a chunk whose timestamp and value arrays are slices at a nonzero offset.
/// The metric of a result of `function` over the series of `metric(host)`.
fn output_metric(function: RangeFunction, host: u64) -> MetricName {
let mut metric = metric(host);
if function.drops_metric_name() {
metric.remove("__name__");
}
metric
}
fn chunk(rows: &[Row], fields: usize, host: u64, is_last: bool) -> RawSeriesChunk {
let padding = 3;
let timestamps: Vec<_> = (0..padding)
@@ -408,7 +417,7 @@ fn functions_match_range_udfs() {
for piece in random_chunks(&mut rng, &rows, fields, host) {
result = evaluator.push(&piece).unwrap();
}
assert_series_eq(result.as_ref(), &expected, &metric(host));
assert_series_eq(result.as_ref(), &expected, &output_metric(function, host));
}
}
}
@@ -690,7 +699,12 @@ fn range_functions_match_prometheus() {
.push(&chunk(&rows[split..], 1, 0, true))
.unwrap()
.or(result);
assert_series_eq_any_nan(result.as_ref(), &expected, &metric(0), line);
assert_series_eq_any_nan(
result.as_ref(),
&expected,
&output_metric(function, 0),
line,
);
checked += expected_points.len();
}
assert!(checked > 1_000, "{checked} points checked");
@@ -714,3 +728,43 @@ fn rejects_double_exponential_smoothing_factors_outside_the_unit_interval() {
));
}
}
/// Range functions drop the metric name and the other metadata labels, except
/// `last_over_time`, as Prometheus does.
#[test]
fn range_functions_drop_metadata_labels() {
let labels = |names: &[&str]| -> Vec<Label> {
names
.iter()
.map(|name| Label {
name: Arc::from(*name),
value: Arc::from("v"),
})
.collect()
};
let input = MetricName::try_new(
Some(Arc::from("m")),
labels(&["__type__", "__unit__", "host"]),
)
.unwrap();
let window = RangeWindow {
range_ms: 10_000,
offset_ms: 0,
};
let rows: Vec<Row> = vec![(1_000, vec![Some(1.0)]), (2_000, vec![Some(2.0)])];
for (function, expected) in [
(
RangeFunction::Rate,
MetricName::try_new(None, labels(&["host"])).unwrap(),
),
(RangeFunction::LastOverTime, input.clone()),
] {
let mut chunk = chunk(&rows, 1, 0, true);
chunk.metric = Some(input.clone());
let mut evaluator =
RangeEvaluator::try_new(&raw_schema(1), grid(2_000, 1_000, 1), window, function)
.unwrap();
let series = evaluator.push(&chunk).unwrap().unwrap();
assert_eq!(series.metric, expected, "{function:?}");
}
}