From 02433cbe809d72e066b1bd3c3461e4349a201c67 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 10 Jun 2025 18:56:26 +0100 Subject: [PATCH] fix: [#1569] Prometheus txt export format. Only one HELP and TYPE header per metric Current format: ``` # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0087"} 4 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (FD66)",client_software_version=""} 1 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (SP)",client_software_version="3605"} 631 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (TIX0325)",client_software_version=""} 14 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0202"} 6754 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (XF)",client_software_version="9400"} 1 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0090"} 7 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Transmission",client_software_version="2.32"} 1 # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (61-b39e)",client_software_version=""} 1 ``` Expected format: ``` # HELP udp_tracker_server_connection_id_errors_total Total number of requests with connection ID errors # TYPE udp_tracker_server_connection_id_errors_total counter udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0087"} 4 udp_tracker_server_connection_id_errors_total{client_software_name="Other (FD66)",client_software_version=""} 1 udp_tracker_server_connection_id_errors_total{client_software_name="Other (SP)",client_software_version="3605"} 631 udp_tracker_server_connection_id_errors_total{client_software_name="Other (TIX0325)",client_software_version=""} 14 udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0202"} 6754 udp_tracker_server_connection_id_errors_total{client_software_name="Other (XF)",client_software_version="9400"} 1 udp_tracker_server_connection_id_errors_total{client_software_name="Other (BC)",client_software_version="0090"} 7 udp_tracker_server_connection_id_errors_total{client_software_name="Transmission",client_software_version="2.32"} 1 udp_tracker_server_connection_id_errors_total{client_software_name="Other (61-b39e)",client_software_version=""} 1 ``` A line break after each metric has also been added to improve readability. --- packages/metrics/src/label/set.rs | 8 ++ packages/metrics/src/lib.rs | 6 +- packages/metrics/src/metric/mod.rs | 102 ++++++---------------- packages/metrics/src/metric_collection.rs | 31 ++++--- packages/metrics/src/sample.rs | 6 +- packages/metrics/src/sample_collection.rs | 6 +- 6 files changed, 64 insertions(+), 95 deletions(-) diff --git a/packages/metrics/src/label/set.rs b/packages/metrics/src/label/set.rs index 1c2c3e27e..cab457f42 100644 --- a/packages/metrics/src/label/set.rs +++ b/packages/metrics/src/label/set.rs @@ -16,6 +16,10 @@ impl LabelSet { pub fn upsert(&mut self, key: LabelName, value: LabelValue) { self.items.insert(key, value); } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } } impl Display for LabelSet { @@ -157,6 +161,10 @@ impl<'de> Deserialize<'de> for LabelSet { impl PrometheusSerializable for LabelSet { fn to_prometheus(&self) -> String { + if self.is_empty() { + return String::new(); + } + let items = self.items.iter().fold(String::new(), |mut output, label_pair| { if !output.is_empty() { output.push(','); diff --git a/packages/metrics/src/lib.rs b/packages/metrics/src/lib.rs index 95d70bf6c..997cd3c8c 100644 --- a/packages/metrics/src/lib.rs +++ b/packages/metrics/src/lib.rs @@ -12,12 +12,12 @@ pub const METRICS_TARGET: &str = "METRICS"; #[cfg(test)] mod tests { - /// It removes leading and trailing whitespace from each line, and empty lines. + /// It removes leading and trailing whitespace from each line. pub fn format_prometheus_output(output: &str) -> String { output .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) + .map(str::trim_start) + .map(str::trim_end) .collect::>() .join("\n") } diff --git a/packages/metrics/src/metric/mod.rs b/packages/metrics/src/metric/mod.rs index 6f254023f..df743c519 100644 --- a/packages/metrics/src/metric/mod.rs +++ b/packages/metrics/src/metric/mod.rs @@ -103,19 +103,6 @@ impl Metric { } } -/// `PrometheusMetricSample` is a wrapper around types that provides methods to -/// convert the metric and its measurement into a Prometheus-compatible format. -/// -/// In Prometheus, a metric is a time series that consists of a name, a set of -/// labels, and a value. The sample value needs data from the `Metric` and -/// `Measurement` structs, as well as the `LabelSet` that defines the labels for -/// the metric. -struct PrometheusMetricSample<'a, T> { - metric: &'a Metric, - measurement: &'a Measurement, - label_set: &'a LabelSet, -} - enum PrometheusType { Counter, Gauge, @@ -130,91 +117,58 @@ impl PrometheusSerializable for PrometheusType { } } -impl PrometheusMetricSample<'_, T> { - fn to_prometheus(&self, prometheus_type: &PrometheusType) -> String { - format!( - // Format: - // # HELP - // # TYPE - // {label_set} - "{}{}{}", - self.help_line(), - self.type_line(prometheus_type), - self.metric_line() - ) - } - - fn help_line(&self) -> String { - if let Some(description) = &self.metric.opt_description { - format!( - // Format: # HELP - "# HELP {} {}\n", - self.metric.name().to_prometheus(), - description.to_prometheus() - ) +impl Metric { + #[must_use] + fn prometheus_help_line(&self) -> String { + if let Some(description) = &self.opt_description { + format!("# HELP {} {}", self.name.to_prometheus(), description.to_prometheus()) } else { String::new() } } - fn type_line(&self, kind: &PrometheusType) -> String { - format!("# TYPE {} {}\n", self.metric.name().to_prometheus(), kind.to_prometheus()) + #[must_use] + fn prometheus_type_line(&self, prometheus_type: &PrometheusType) -> String { + format!("# TYPE {} {}", self.name.to_prometheus(), prometheus_type.to_prometheus()) } - fn metric_line(&self) -> String { + #[must_use] + fn prometheus_sample_line(&self, label_set: &LabelSet, measurement: &Measurement) -> String { format!( - // Format: {label_set} "{}{} {}", - self.metric.name.to_prometheus(), - self.label_set.to_prometheus(), - self.measurement.value().to_prometheus() + self.name.to_prometheus(), + label_set.to_prometheus(), + measurement.to_prometheus() ) } -} -impl<'a> PrometheusMetricSample<'a, Counter> { - pub fn new(metric: &'a Metric, measurement: &'a Measurement, label_set: &'a LabelSet) -> Self { - Self { - metric, - measurement, - label_set, - } + #[must_use] + fn prometheus_samples(&self) -> String { + self.sample_collection + .iter() + .map(|(label_set, measurement)| self.prometheus_sample_line(label_set, measurement)) + .collect::>() + .join("\n") } -} -impl<'a> PrometheusMetricSample<'a, Gauge> { - pub fn new(metric: &'a Metric, measurement: &'a Measurement, label_set: &'a LabelSet) -> Self { - Self { - metric, - measurement, - label_set, - } + fn to_prometheus(&self, prometheus_type: &PrometheusType) -> String { + let help_line = self.prometheus_help_line(); + let type_line = self.prometheus_type_line(prometheus_type); + let samples = self.prometheus_samples(); + + format!("{help_line}\n{type_line}\n{samples}") } } impl PrometheusSerializable for Metric { fn to_prometheus(&self) -> String { - let samples: Vec = self - .sample_collection - .iter() - .map(|(label_set, measurement)| { - PrometheusMetricSample::::new(self, measurement, label_set).to_prometheus(&PrometheusType::Counter) - }) - .collect(); - samples.join("\n") + self.to_prometheus(&PrometheusType::Counter) } } impl PrometheusSerializable for Metric { fn to_prometheus(&self) -> String { - let samples: Vec = self - .sample_collection - .iter() - .map(|(label_set, measurement)| { - PrometheusMetricSample::::new(self, measurement, label_set).to_prometheus(&PrometheusType::Gauge) - }) - .collect(); - samples.join("\n") + self.to_prometheus(&PrometheusType::Gauge) } } diff --git a/packages/metrics/src/metric_collection.rs b/packages/metrics/src/metric_collection.rs index c53d02bcf..ff932caae 100644 --- a/packages/metrics/src/metric_collection.rs +++ b/packages/metrics/src/metric_collection.rs @@ -322,7 +322,7 @@ impl PrometheusSerializable for MetricCollection { .map(Metric::::to_prometheus), ) .collect::>() - .join("\n") + .join("\n\n") } } @@ -629,14 +629,14 @@ mod tests { fn prometheus() -> String { format_prometheus_output( - r#" - # HELP http_tracker_core_announce_requests_received_total The number of announce requests received. - # TYPE http_tracker_core_announce_requests_received_total counter - http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 - # HELP udp_tracker_server_performance_avg_announce_processing_time_ns The average announce processing time in nanoseconds. - # TYPE udp_tracker_server_performance_avg_announce_processing_time_ns gauge - udp_tracker_server_performance_avg_announce_processing_time_ns{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 - "#, + r#"# HELP http_tracker_core_announce_requests_received_total The number of announce requests received. +# TYPE http_tracker_core_announce_requests_received_total counter +http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 + +# HELP udp_tracker_server_performance_avg_announce_processing_time_ns The average announce processing time in nanoseconds. +# TYPE udp_tracker_server_performance_avg_announce_processing_time_ns gauge +udp_tracker_server_performance_avg_announce_processing_time_ns{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 +"#, ) } } @@ -750,7 +750,7 @@ mod tests { MetricKindCollection::new(vec![Metric::new( metric_name!("http_tracker_core_announce_requests_received_total"), None, - None, + Some(MetricDescription::new("The number of announce requests received.")), SampleCollection::new(vec![ Sample::new(Counter::new(1), time, label_set_1.clone()), Sample::new(Counter::new(2), time, label_set_2.clone()), @@ -765,12 +765,11 @@ mod tests { let prometheus_output = metric_collection.to_prometheus(); let expected_prometheus_output = format_prometheus_output( - r#" - # TYPE http_tracker_core_announce_requests_received_total counter - http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7171",server_binding_protocol="http"} 2 - # TYPE http_tracker_core_announce_requests_received_total counter - http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 - "#, + r#"# HELP http_tracker_core_announce_requests_received_total The number of announce requests received. +# TYPE http_tracker_core_announce_requests_received_total counter +http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7070",server_binding_protocol="http"} 1 +http_tracker_core_announce_requests_received_total{server_binding_ip="0.0.0.0",server_binding_port="7171",server_binding_protocol="http"} 2 +"#, ); // code-review: samples are not serialized in the same order as they are created. diff --git a/packages/metrics/src/sample.rs b/packages/metrics/src/sample.rs index ad4dff00e..b9cd6c312 100644 --- a/packages/metrics/src/sample.rs +++ b/packages/metrics/src/sample.rs @@ -50,7 +50,11 @@ impl Sample { impl PrometheusSerializable for Sample { fn to_prometheus(&self) -> String { - format!("{} {}", self.label_set.to_prometheus(), self.measurement.to_prometheus()) + if self.label_set.is_empty() { + format!(" {}", self.measurement.to_prometheus()) + } else { + format!("{} {}", self.label_set.to_prometheus(), self.measurement.to_prometheus()) + } } } diff --git a/packages/metrics/src/sample_collection.rs b/packages/metrics/src/sample_collection.rs index a87aacb63..ef88b27dd 100644 --- a/packages/metrics/src/sample_collection.rs +++ b/packages/metrics/src/sample_collection.rs @@ -155,7 +155,11 @@ impl PrometheusSerializable for SampleCollection { let mut output = String::new(); for (label_set, sample_data) in &self.samples { - let _ = write!(output, "{} {}", label_set.to_prometheus(), sample_data.to_prometheus()); + if label_set.is_empty() { + let _ = write!(output, "{}", sample_data.to_prometheus()); + } else { + let _ = write!(output, "{} {}", label_set.to_prometheus(), sample_data.to_prometheus()); + } } output