From 024a53fce44af067fb06b1dcbc598722bd46ddc4 Mon Sep 17 00:00:00 2001 From: Mariam Date: Thu, 15 Oct 2026 17:34:41 +0100 Subject: [PATCH 1/2] feat(metrics): add clear_all to stop reporting unchanged series Signed-off-by: Mariam --- foundations-metrics/src/clear.rs | 410 +++++++++++++++++++++++ foundations-metrics/src/collect.rs | 4 +- foundations-metrics/src/info.rs | 13 + foundations-metrics/src/lib.rs | 2 + foundations-metrics/tests/clear_all.rs | 133 ++++++++ foundations/src/telemetry/metrics/mod.rs | 6 + foundations/tests/metrics_clear_all.rs | 66 ++++ 7 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 foundations-metrics/src/clear.rs create mode 100644 foundations-metrics/tests/clear_all.rs create mode 100644 foundations/tests/metrics_clear_all.rs diff --git a/foundations-metrics/src/clear.rs b/foundations-metrics/src/clear.rs new file mode 100644 index 00000000..5b580bf3 --- /dev/null +++ b/foundations-metrics/src/clear.rs @@ -0,0 +1,410 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + +use foundations_metrics_registry::proto::{LabelPair, Metric}; +use foundations_metrics_registry::{EncodeMetric, MetricFamily, iter}; +use parking_lot::Mutex; + +use crate::info::info_metric_names; + +/// Series that haven't changed since [`clear_all`] was last called. +static CLEARED: OnceLock>> = OnceLock::new(); + +/// Stops reporting every series until its value changes. +/// +/// After this call, collection skips every series whose value is the same as +/// at the time of the call. Once a series' value changes, it's reported again +/// with its current value and stays reported from then on. Series and metrics +/// created after the call are reported as usual, and info metrics are always +/// reported. +/// +/// This is meant for a process that is winding down, such as the old instance +/// during a graceful restart. Most of its series stop changing, and exporting +/// them again only adds load to the monitoring system. +/// +/// Values aren't reset, so metric handles held elsewhere keep working. Until a +/// series changes, a copy of its value is kept to compare against. Calling this +/// again starts over from the values at that time. +/// +/// # Examples +/// +/// ``` +/// use foundations_metrics::{ +/// CollectionOptions, Counter, EncodeMetric, NamedMetric, RegistrationMetadata, +/// ServiceNameFormat, clear_all, collect, register, +/// }; +/// +/// let requests = Counter::::default(); +/// requests.inc_by(553); +/// +/// register( +/// Box::new(NamedMetric::new("requests_total", "Requests.", requests.clone())) +/// as Box, +/// RegistrationMetadata::default(), +/// ); +/// +/// let options = CollectionOptions { +/// include_optional: false, +/// service_name: None, +/// service_name_format: ServiceNameFormat::MetricPrefix, +/// }; +/// +/// clear_all(); +/// assert!(collect(options).is_empty()); +/// +/// requests.inc(); +/// assert_eq!(collect(options).len(), 1); +/// ``` +pub fn clear_all() { + let always_reported = info_metric_names(); + let snapshot = Snapshot::take( + iter().map(|registered| encode_settled(registered.metric())), + &always_reported, + ); + + *CLEARED.get_or_init(Default::default).lock() = Some(snapshot); +} + +/// Removes the rows of the metric at registry position `index` that haven't +/// changed since [`clear_all`] was called. +pub(crate) fn hide_unchanged(index: usize, families: &mut Vec) { + let Some(cleared) = CLEARED.get() else { + return; + }; + + if let Some(snapshot) = cleared.lock().as_mut() { + snapshot.hide_unchanged(index, families); + } +} + +/// Encodes `metric` twice and returns the second result. +/// +/// Encoding can reset state, like a `RangeGauge`'s min and max, so the second +/// result is what later collections see while nothing changes. +fn encode_settled(metric: &dyn EncodeMetric) -> Vec { + metric.encode(); + metric.encode() +} + +/// The rows each registered metric had when [`clear_all`] was called, by +/// registry position. Rows are forgotten once they change. +struct Snapshot { + metrics: Vec, +} + +/// One metric's unchanged rows by family name, then by labels. The labels are +/// moved out of each stored row into its key. +type MetricSnapshot = HashMap, Metric>>; + +impl Snapshot { + fn take( + encoded: impl IntoIterator>, + always_reported: &[&str], + ) -> Self { + let metrics = encoded + .into_iter() + .map(|families| { + let mut snapshot = MetricSnapshot::new(); + + for family in families { + let Some(name) = family.name else { + continue; + }; + + if family.metric.is_empty() + || always_reported.iter().any(|reported| *reported == name) + { + continue; + } + + let rows = snapshot.entry(name).or_default(); + + for mut row in family.metric { + let labels = std::mem::take(&mut row.label); + rows.insert(labels, row); + } + } + + snapshot + }) + .collect(); + + Self { metrics } + } + + fn hide_unchanged(&mut self, index: usize, families: &mut Vec) { + let Some(snapshot) = self.metrics.get_mut(index) else { + return; + }; + + if snapshot.is_empty() { + return; + } + + families.retain_mut(|family| { + let Some(name) = family.name.as_deref() else { + return true; + }; + + let Some(rows) = snapshot.get_mut(name) else { + return true; + }; + + let had_rows = !family.metric.is_empty(); + + family.metric.retain(|row| { + let changed = match rows.get(row.label.as_slice()) { + Some(old) => !same_value(row, old), + None => return true, + }; + + if changed { + rows.remove(row.label.as_slice()); + } + + changed + }); + + // Drop families emptied here, but leave already empty ones alone. + !had_rows || !family.metric.is_empty() + }); + + snapshot.retain(|_, rows| !rows.is_empty()); + } +} + +/// Whether `row` still has the value stored in `old`. Labels aren't compared, +/// because they're the snapshot key. NaN never compares equal, so a series +/// holding NaN is always reported. +fn same_value(row: &Metric, old: &Metric) -> bool { + // Destructured so that a new field can't be left out by accident. + let Metric { + label: _, + gauge, + counter, + summary, + untyped, + histogram, + timestamp_ms, + } = row; + + *gauge == old.gauge + && *counter == old.counter + && *summary == old.summary + && *untyped == old.untyped + && *histogram == old.histogram + && *timestamp_ms == old.timestamp_ms +} + +#[cfg(test)] +mod tests { + use serde::Serialize; + + use super::*; + use crate::{Counter, Family, Gauge, Histogram, NamedMetric, RangeGauge}; + + #[derive(Clone, Eq, Hash, PartialEq, Serialize)] + struct Labels { + kind: &'static str, + } + + fn labels(kind: &'static str) -> Labels { + Labels { kind } + } + + /// Collects `metric` as if it were registered at position `index`, and + /// formats each reported row as `name{labels} value`. + fn reported(snapshot: &mut Snapshot, index: usize, metric: &dyn EncodeMetric) -> Vec { + let mut families = metric.encode(); + snapshot.hide_unchanged(index, &mut families); + + let mut rows: Vec = families + .iter() + .flat_map(|family| family.metric.iter().map(move |row| format_row(family, row))) + .collect(); + + rows.sort(); + rows + } + + fn format_row(family: &MetricFamily, row: &Metric) -> String { + let name = family.name.as_deref().unwrap_or_default(); + let value = row + .counter + .as_ref() + .and_then(|counter| counter.value) + .or_else(|| row.gauge.as_ref().and_then(|gauge| gauge.value)) + .or_else(|| { + row.histogram + .as_ref() + .and_then(|histogram| histogram.sample_count) + .map(|count| count as f64) + }) + .expect("row has a value"); + + let labels: Vec = row + .label + .iter() + .map(|label| { + format!( + "{}=\"{}\"", + label.name.as_deref().unwrap_or_default(), + label.value.as_deref().unwrap_or_default() + ) + }) + .collect(); + + if labels.is_empty() { + format!("{name} {value}") + } else { + format!("{name}{{{}}} {value}", labels.join(",")) + } + } + + #[test] + fn hides_series_until_they_change() { + let requests = Family::::default(); + requests.get_or_create(&labels("ok")).inc_by(553); + requests.get_or_create(&labels("error")).inc_by(12); + + let metric = NamedMetric::new("requests_total", "Requests.", requests.clone()); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + assert!(reported(&mut snapshot, 0, &metric).is_empty()); + + requests.get_or_create(&labels("ok")).inc(); + assert_eq!( + reported(&mut snapshot, 0, &metric), + ["requests_total{kind=\"ok\"} 554"] + ); + + // A series stays reported once it has changed. + assert_eq!( + reported(&mut snapshot, 0, &metric), + ["requests_total{kind=\"ok\"} 554"] + ); + } + + #[test] + fn reports_the_current_value_of_a_gauge() { + let connections = Family::::default(); + connections.get_or_create(&labels("https")).set(50); + + let metric = NamedMetric::new("connections", "Open connections.", connections.clone()); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + assert!(reported(&mut snapshot, 0, &metric).is_empty()); + + connections.get_or_create(&labels("https")).dec(); + assert_eq!( + reported(&mut snapshot, 0, &metric), + ["connections{kind=\"https\"} 49"] + ); + } + + #[test] + fn saved_handles_keep_reporting() { + let requests = Family::::default(); + let ok = requests.get_or_create_owned(&labels("ok")); + ok.inc_by(553); + + let metric = NamedMetric::new("requests_total", "Requests.", requests); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + ok.inc_by(100); + assert_eq!( + reported(&mut snapshot, 0, &metric), + ["requests_total{kind=\"ok\"} 653"] + ); + } + + #[test] + fn hides_unlabeled_metrics_and_reports_new_series() { + let reports = Counter::::default(); + reports.inc_by(7); + let unlabeled = NamedMetric::new("reports_total", "Reports.", reports.clone()); + + let requests = Family::::default(); + requests.get_or_create(&labels("ok")).inc(); + let labeled = NamedMetric::new("requests_total", "Requests.", requests.clone()); + + let mut snapshot = + Snapshot::take([encode_settled(&unlabeled), encode_settled(&labeled)], &[]); + + assert!(reported(&mut snapshot, 0, &unlabeled).is_empty()); + assert!(reported(&mut snapshot, 1, &labeled).is_empty()); + + reports.inc(); + assert_eq!(reported(&mut snapshot, 0, &unlabeled), ["reports_total 8"]); + + // A label set created after the snapshot is reported before it changes. + drop(requests.get_or_create(&labels("new"))); + assert_eq!( + reported(&mut snapshot, 1, &labeled), + ["requests_total{kind=\"new\"} 0"] + ); + + // So is a metric registered after the snapshot. + let later = NamedMetric::new( + "later_total", + "Registered later.", + Counter::::default(), + ); + assert_eq!(reported(&mut snapshot, 2, &later), ["later_total 0"]); + } + + #[test] + fn always_reports_exempt_families() { + let build_info = Gauge::::default(); + build_info.set(1); + + let metric = NamedMetric::new("build_info", "Build information.", build_info); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &["build_info"]); + + assert_eq!(reported(&mut snapshot, 0, &metric), ["build_info 1"]); + } + + #[test] + fn idle_range_gauge_stays_hidden() { + let inflight = RangeGauge::default(); + inflight.inc_by(3); + inflight.dec_by(2); + + let metric = NamedMetric::new("inflight", "In-flight requests.", inflight.clone()); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + // Its min and max differed from its value when the snapshot was taken. + assert!(reported(&mut snapshot, 0, &metric).is_empty()); + + inflight.inc(); + assert_eq!( + reported(&mut snapshot, 0, &metric), + ["inflight 2", "inflight_max 2"] + ); + } + + #[test] + fn reports_a_histogram_once_it_records_a_value() { + let latency = Histogram::new([0.1, 1.0]); + latency.observe(0.5); + + let metric = NamedMetric::new("latency_seconds", "Latency.", latency.clone()); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + assert!(reported(&mut snapshot, 0, &metric).is_empty()); + + latency.observe(0.05); + assert_eq!(reported(&mut snapshot, 0, &metric), ["latency_seconds 2"]); + } + + #[test] + fn drops_families_left_without_rows() { + let metric = NamedMetric::new("reports_total", "Reports.", Counter::::default()); + let mut snapshot = Snapshot::take([encode_settled(&metric)], &[]); + + let mut families = metric.encode(); + snapshot.hide_unchanged(0, &mut families); + + assert!(families.is_empty()); + } +} diff --git a/foundations-metrics/src/collect.rs b/foundations-metrics/src/collect.rs index 72995b81..f3c34db2 100644 --- a/foundations-metrics/src/collect.rs +++ b/foundations-metrics/src/collect.rs @@ -1,6 +1,7 @@ use foundations_metrics_registry::{iter, proto::LabelPair}; use crate::MetricFamily; +use crate::clear::hide_unchanged; use crate::diagnostics::report_collect_error; use crate::validation::{ NAME_REQUIREMENT, ValidationContext, is_valid_name, sanitize_metric_family, @@ -44,7 +45,7 @@ pub fn collect(options: CollectionOptions) -> Vec { let mut collected = Vec::new(); - for registered in iter() { + for (index, registered) in iter().enumerate() { let metadata = registered.metadata(); if metadata.optional && !options.include_optional { @@ -52,6 +53,7 @@ pub fn collect(options: CollectionOptions) -> Vec { } let mut families = registered.metric().encode(); + hide_unchanged(index, &mut families); if let Some(service_name) = options.service_name { apply_service_name( diff --git a/foundations-metrics/src/info.rs b/foundations-metrics/src/info.rs index 84ef02d8..0066aade 100644 --- a/foundations-metrics/src/info.rs +++ b/foundations-metrics/src/info.rs @@ -98,6 +98,19 @@ where } } +/// Returns the names of the info metrics reported so far. +pub(crate) fn info_metric_names() -> Vec<&'static str> { + let Some(info_metrics) = INFO_METRICS.get() else { + return Vec::new(); + }; + + info_metrics + .read() + .values() + .map(|entry| entry.name) + .collect() +} + /// Encodes every reported info metric as a gauge family valued `1`. struct InfoCollector; diff --git a/foundations-metrics/src/lib.rs b/foundations-metrics/src/lib.rs index e13ad3c0..8716d39b 100644 --- a/foundations-metrics/src/lib.rs +++ b/foundations-metrics/src/lib.rs @@ -12,6 +12,7 @@ //! crate. See the crate README for migration instructions. #![warn(missing_docs)] +mod clear; mod collect; mod diagnostics; mod encoding; @@ -22,6 +23,7 @@ mod registered; mod validation; mod value; +pub use clear::clear_all; pub use collect::{CollectionOptions, ServiceNameFormat, collect}; pub use diagnostics::{CollectErrorHookAlreadySet, set_collect_error_hook}; pub use encoding::{ diff --git a/foundations-metrics/tests/clear_all.rs b/foundations-metrics/tests/clear_all.rs new file mode 100644 index 00000000..d03d10e6 --- /dev/null +++ b/foundations-metrics/tests/clear_all.rs @@ -0,0 +1,133 @@ +//! [`clear_all`] changes process-wide collection state, so it's tested in its +//! own binary. + +use foundations_metrics::proto::Metric; +use foundations_metrics::{ + CollectionOptions, Counter, EncodeMetric, Family, Gauge, InfoMetric, NamedMetric, + RegistrationMetadata, ServiceNameFormat, clear_all, collect, register, report_info, +}; +use serde::Serialize; + +#[derive(Clone, Eq, Hash, PartialEq, Serialize)] +struct Labels { + status: u16, +} + +#[derive(Serialize)] +struct BuildInfo { + version: &'static str, +} + +impl InfoMetric for BuildInfo { + const NAME: &'static str = "build_info"; + const HELP: &'static str = "Build information."; +} + +fn register_named(metric: NamedMetric) +where + NamedMetric: EncodeMetric, +{ + register( + Box::new(metric) as Box, + RegistrationMetadata::default(), + ); +} + +/// Formats each collected row as `name{labels} value`, sorted. +fn collected() -> Vec { + let families = collect(CollectionOptions { + include_optional: false, + service_name: None, + service_name_format: ServiceNameFormat::MetricPrefix, + }); + + let mut rows: Vec = families + .iter() + .flat_map(|family| { + let name = family.name.as_deref().unwrap_or_default(); + family.metric.iter().map(move |row| format_row(name, row)) + }) + .collect(); + + rows.sort(); + rows +} + +fn format_row(name: &str, row: &Metric) -> String { + let value = row + .counter + .as_ref() + .and_then(|counter| counter.value) + .or_else(|| row.gauge.as_ref().and_then(|gauge| gauge.value)) + .expect("row has a value"); + + let labels: Vec = row + .label + .iter() + .map(|label| { + format!( + "{}=\"{}\"", + label.name.as_deref().unwrap_or_default(), + label.value.as_deref().unwrap_or_default() + ) + }) + .collect(); + + if labels.is_empty() { + format!("{name} {value}") + } else { + format!("{name}{{{}}} {value}", labels.join(",")) + } +} + +#[test] +fn reports_only_series_that_change_after_clear_all() { + let requests = Family::::default(); + requests.get_or_create(&Labels { status: 200 }).inc_by(553); + requests.get_or_create(&Labels { status: 404 }).inc_by(12); + register_named(NamedMetric::new( + "requests_total", + "Requests.", + requests.clone(), + )); + + let connections: Gauge = Gauge::default(); + connections.set(50); + register_named(NamedMetric::new( + "connections", + "Open connections.", + connections.clone(), + )); + + report_info(BuildInfo { version: "1.2.3" }); + + clear_all(); + + // Info metrics are always reported. + assert_eq!(collected(), ["build_info{version=\"1.2.3\"} 1"]); + + requests.get_or_create(&Labels { status: 200 }).inc(); + connections.dec(); + + assert_eq!( + collected(), + [ + "build_info{version=\"1.2.3\"} 1", + "connections 49", + "requests_total{status=\"200\"} 554", + ] + ); + + let later: Counter = Counter::default(); + register_named(NamedMetric::new("later_total", "Registered later.", later)); + + assert_eq!( + collected(), + [ + "build_info{version=\"1.2.3\"} 1", + "connections 49", + "later_total 0", + "requests_total{status=\"200\"} 554", + ] + ); +} diff --git a/foundations/src/telemetry/metrics/mod.rs b/foundations/src/telemetry/metrics/mod.rs index 786ddb05..aea3b3f9 100644 --- a/foundations/src/telemetry/metrics/mod.rs +++ b/foundations/src/telemetry/metrics/mod.rs @@ -12,6 +12,10 @@ not(feature = "foundations-metrics-backend"), doc = "- Use [`collect`] method to obtain metrics report programmatically." )] +#![cfg_attr( + feature = "foundations-metrics-backend", + doc = "- Use [`clear_all`] to stop reporting unchanged series, e.g. when draining." +)] //! - Use [telemetry server] to expose a metrics endpoint. //! //! [Prometheus]: https://prometheus.io/ @@ -70,6 +74,8 @@ mod backend { EncodeMetric, EncodeMetricValue, IntoMetrics, LabelError, MetricFamily, NamedMetric, RegistrationMetadata, proto, register, to_label_pairs, }; + + pub use foundations_metrics::clear_all; } #[cfg(not(feature = "foundations-metrics-backend"))] mod backend { diff --git a/foundations/tests/metrics_clear_all.rs b/foundations/tests/metrics_clear_all.rs new file mode 100644 index 00000000..3feb5e9c --- /dev/null +++ b/foundations/tests/metrics_clear_all.rs @@ -0,0 +1,66 @@ +//! End-to-end test for `clear_all` through the Foundations facade, in its own +//! binary because it changes process-wide collection state. +#![cfg(feature = "foundations-metrics-backend")] + +use foundations::telemetry::metrics::{Counter, Gauge, clear_all, metrics}; +use foundations::telemetry::settings::{MetricsSettings, ServiceNameFormat}; + +mod common; +use common::collect_text; + +#[metrics] +mod draining { + /// Requests by status. + pub fn requests_total(status: u16) -> Counter; + + /// Reports sent. + pub fn reports_total() -> Counter; + + /// Open connections. + pub fn connections() -> Gauge; +} + +#[test] +fn clear_all_reports_only_series_that_change() { + draining::requests_total(200).inc_by(553); + draining::requests_total(404).inc_by(12); + draining::reports_total().inc_by(7); + draining::connections().set(50); + + // Resolved once and reused, as the `metrics` macro docs recommend. + let ok_requests = draining::requests_total(200); + + let settings = MetricsSettings { + service_name_format: ServiceNameFormat::MetricPrefix, + report_optional: false, + }; + + clear_all(); + + let text = collect_text(&settings); + assert!( + !text.contains("undefined_draining_"), + "collected output was: {text}" + ); + + ok_requests.inc(); + draining::connections().dec(); + + let text = collect_text(&settings); + assert!( + text.contains("undefined_draining_requests_total{status=\"200\"} 554"), + "collected output was: {text}" + ); + assert!( + text.contains("undefined_draining_connections 49"), + "collected output was: {text}" + ); + assert!( + !text.contains("status=\"404\""), + "collected output was: {text}" + ); + assert!( + !text.contains("undefined_draining_reports_total"), + "collected output was: {text}" + ); +} From fab48cf3bb77cf3956800a95ea2a5d877330dfb9 Mon Sep 17 00:00:00 2001 From: Mariam Date: Thu, 22 Oct 2026 12:25:10 +0100 Subject: [PATCH 2/2] fix: address reviewer comments Signed-off-by: Mariam --- foundations-metrics/Cargo.toml | 1 + foundations-metrics/src/clear.rs | 30 ++++++++++++++---------------- foundations-metrics/src/collect.rs | 5 +++-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/foundations-metrics/Cargo.toml b/foundations-metrics/Cargo.toml index 06360e90..6694fbc8 100644 --- a/foundations-metrics/Cargo.toml +++ b/foundations-metrics/Cargo.toml @@ -36,6 +36,7 @@ ryu = { workspace = true } parking_lot = { workspace = true } prost = { workspace = true } prost-types = { workspace = true } +crossbeam-utils = { workspace = true } [lints] workspace = true diff --git a/foundations-metrics/src/clear.rs b/foundations-metrics/src/clear.rs index 5b580bf3..ced73d37 100644 --- a/foundations-metrics/src/clear.rs +++ b/foundations-metrics/src/clear.rs @@ -1,14 +1,16 @@ use std::collections::HashMap; -use std::sync::OnceLock; +use crossbeam_utils::CachePadded; use foundations_metrics_registry::proto::{LabelPair, Metric}; use foundations_metrics_registry::{EncodeMetric, MetricFamily, iter}; -use parking_lot::Mutex; +use parking_lot::{Mutex, MutexGuard}; use crate::info::info_metric_names; /// Series that haven't changed since [`clear_all`] was last called. -static CLEARED: OnceLock>> = OnceLock::new(); +static CLEARED: CachePadded> = CachePadded::new(Mutex::new(Snapshot { + metrics: Vec::new(), +})); /// Stops reporting every series until its value changes. /// @@ -62,19 +64,13 @@ pub fn clear_all() { &always_reported, ); - *CLEARED.get_or_init(Default::default).lock() = Some(snapshot); + *CLEARED.lock() = snapshot; } -/// Removes the rows of the metric at registry position `index` that haven't -/// changed since [`clear_all`] was called. -pub(crate) fn hide_unchanged(index: usize, families: &mut Vec) { - let Some(cleared) = CLEARED.get() else { - return; - }; - - if let Some(snapshot) = cleared.lock().as_mut() { - snapshot.hide_unchanged(index, families); - } +/// Locks the rows that haven't changed since [`clear_all`] was called, so a +/// collection can filter every metric under one lock. +pub(crate) fn cleared() -> MutexGuard<'static, Snapshot> { + CLEARED.lock() } /// Encodes `metric` twice and returns the second result. @@ -88,7 +84,7 @@ fn encode_settled(metric: &dyn EncodeMetric) -> Vec { /// The rows each registered metric had when [`clear_all`] was called, by /// registry position. Rows are forgotten once they change. -struct Snapshot { +pub(crate) struct Snapshot { metrics: Vec, } @@ -132,7 +128,9 @@ impl Snapshot { Self { metrics } } - fn hide_unchanged(&mut self, index: usize, families: &mut Vec) { + /// Removes the rows of the metric at registry position `index` that + /// haven't changed since [`clear_all`] was called. + pub(crate) fn hide_unchanged(&mut self, index: usize, families: &mut Vec) { let Some(snapshot) = self.metrics.get_mut(index) else { return; }; diff --git a/foundations-metrics/src/collect.rs b/foundations-metrics/src/collect.rs index f3c34db2..950a420d 100644 --- a/foundations-metrics/src/collect.rs +++ b/foundations-metrics/src/collect.rs @@ -1,7 +1,7 @@ use foundations_metrics_registry::{iter, proto::LabelPair}; use crate::MetricFamily; -use crate::clear::hide_unchanged; +use crate::clear::cleared; use crate::diagnostics::report_collect_error; use crate::validation::{ NAME_REQUIREMENT, ValidationContext, is_valid_name, sanitize_metric_family, @@ -44,6 +44,7 @@ pub fn collect(options: CollectionOptions) -> Vec { } let mut collected = Vec::new(); + let mut cleared = cleared(); for (index, registered) in iter().enumerate() { let metadata = registered.metadata(); @@ -53,7 +54,7 @@ pub fn collect(options: CollectionOptions) -> Vec { } let mut families = registered.metric().encode(); - hide_unchanged(index, &mut families); + cleared.hide_unchanged(index, &mut families); if let Some(service_name) = options.service_name { apply_service_name(