Skip to content

Commit fb71520

Browse files
committed
refactor how collected metrics are stored to avoid parsing labels
currently the prootbuf implementation takes the stringified-labels and parses them back into their original form, which is silly. refactored to avoid that, and in the process also avoid some copies and allocations. this changes public APIs around these, so should only be landed with an appropriate version bump
1 parent a44949c commit fb71520

File tree

4 files changed

+88
-91
lines changed

4 files changed

+88
-91
lines changed

metrics-exporter-prometheus/src/common.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,42 @@ pub enum BuildError {
8080
ZeroBucketDuration,
8181
}
8282

83+
/// Represents a set of labels as structured key-value pairs
84+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85+
pub struct LabelSet {
86+
pub labels: Vec<(String, String)>,
87+
}
88+
89+
impl LabelSet {
90+
pub fn from_key_and_global(
91+
key: &metrics::Key,
92+
global_labels: &IndexMap<String, String>,
93+
) -> Self {
94+
let mut labels = global_labels.clone();
95+
key.labels().for_each(|label| {
96+
labels.insert(label.key().to_string(), label.value().to_string());
97+
});
98+
Self { labels: labels.into_iter().collect() }
99+
}
100+
101+
pub fn is_empty(&self) -> bool {
102+
self.labels.is_empty()
103+
}
104+
105+
pub fn to_strings(&self) -> impl Iterator<Item = String> + '_ {
106+
self.labels.iter().map(|(k, v)| {
107+
format!(
108+
"{}=\"{}\"",
109+
crate::formatting::sanitize_label_key(k),
110+
crate::formatting::sanitize_label_value(v)
111+
)
112+
})
113+
}
114+
}
115+
83116
#[derive(Debug)]
84117
pub struct Snapshot {
85-
pub counters: HashMap<String, HashMap<Vec<String>, u64>>,
86-
pub gauges: HashMap<String, HashMap<Vec<String>, f64>>,
87-
pub distributions: HashMap<String, IndexMap<Vec<String>, Distribution>>,
118+
pub counters: HashMap<String, HashMap<LabelSet, u64>>,
119+
pub gauges: HashMap<String, HashMap<LabelSet, f64>>,
120+
pub distributions: HashMap<String, IndexMap<LabelSet, Distribution>>,
88121
}

metrics-exporter-prometheus/src/formatting.rs

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,8 @@
11
//! Helpers for rendering metrics in the Prometheus exposition format.
22
3-
use indexmap::IndexMap;
4-
use metrics::{Key, Unit};
3+
use metrics::Unit;
54

6-
/// Breaks a key into the name and label components, with optional default labels.
7-
///
8-
/// If any of the default labels are not already present, they will be added to the overall list of labels.
9-
///
10-
/// Both the metric name, and labels, are sanitized. See [`sanitize_metric_name`], [`sanitize_label_key`],
11-
/// and [`sanitize_label_value`] for more information.
12-
pub fn key_to_parts(
13-
key: &Key,
14-
default_labels: Option<&IndexMap<String, String>>,
15-
) -> (String, Vec<String>) {
16-
let name = sanitize_metric_name(key.name());
17-
let mut values = default_labels.cloned().unwrap_or_default();
18-
key.labels().for_each(|label| {
19-
values.insert(label.key().to_string(), label.value().to_string());
20-
});
21-
let labels = values
22-
.iter()
23-
.map(|(k, v)| format!("{}=\"{}\"", sanitize_label_key(k), sanitize_label_value(v)))
24-
.collect();
25-
26-
(name, labels)
27-
}
5+
use crate::common::LabelSet;
286

297
/// Writes a help (description) line in the Prometheus [exposition format].
308
///
@@ -73,7 +51,7 @@ pub fn write_metric_line<T, T2>(
7351
buffer: &mut String,
7452
name: &str,
7553
suffix: Option<&'static str>,
76-
labels: &[String],
54+
labels: &LabelSet,
7755
additional_label: Option<(&'static str, T)>,
7856
value: T2,
7957
unit: Option<Unit>,
@@ -87,13 +65,13 @@ pub fn write_metric_line<T, T2>(
8765
buffer.push('{');
8866

8967
let mut first = true;
90-
for label in labels {
68+
for label in labels.to_strings() {
9169
if first {
9270
first = false;
9371
} else {
9472
buffer.push(',');
9573
}
96-
buffer.push_str(label);
74+
buffer.push_str(&label);
9775
}
9876

9977
if let Some((name, value)) = additional_label {

metrics-exporter-prometheus/src/protobuf.rs

Lines changed: 35 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
//! Protobuf serialization support for Prometheus metrics.
22
3-
use indexmap::IndexMap;
43
use metrics::Unit;
54
use prost::Message;
65
use std::collections::HashMap;
76

8-
use crate::common::Snapshot;
7+
use crate::common::{LabelSet, Snapshot};
98
use crate::distribution::Distribution;
109
use crate::formatting::sanitize_metric_name;
1110

@@ -26,28 +25,27 @@ pub(crate) const PROTOBUF_CONTENT_TYPE: &str =
2625
/// length header.
2726
#[allow(clippy::too_many_lines)]
2827
pub(crate) fn render_protobuf(
29-
snapshot: &Snapshot,
28+
snapshot: Snapshot,
3029
descriptions: &HashMap<String, (metrics::SharedString, Option<Unit>)>,
31-
global_labels: &IndexMap<String, String>,
3230
counter_suffix: Option<&'static str>,
3331
) -> Vec<u8> {
3432
let mut output = Vec::new();
3533

3634
// Process counters
37-
for (name, by_labels) in &snapshot.counters {
38-
let sanitized_name = sanitize_metric_name(name);
35+
for (name, by_labels) in snapshot.counters {
36+
let sanitized_name = sanitize_metric_name(&name);
3937
let help =
4038
descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
4139

4240
let mut metrics = Vec::new();
4341
for (labels, value) in by_labels {
44-
let label_pairs = parse_labels(labels, global_labels);
42+
let label_pairs = label_set_to_protobuf(labels);
4543

4644
metrics.push(pb::Metric {
4745
label: label_pairs,
4846
counter: Some(pb::Counter {
4947
#[allow(clippy::cast_precision_loss)]
50-
value: Some(*value as f64),
48+
value: Some(value as f64),
5149

5250
..Default::default()
5351
}),
@@ -68,18 +66,18 @@ pub(crate) fn render_protobuf(
6866
}
6967

7068
// Process gauges
71-
for (name, by_labels) in &snapshot.gauges {
72-
let sanitized_name = sanitize_metric_name(name);
69+
for (name, by_labels) in snapshot.gauges {
70+
let sanitized_name = sanitize_metric_name(&name);
7371
let help =
7472
descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
7573

7674
let mut metrics = Vec::new();
7775
for (labels, value) in by_labels {
78-
let label_pairs = parse_labels(labels, global_labels);
76+
let label_pairs = label_set_to_protobuf(labels);
7977

8078
metrics.push(pb::Metric {
8179
label: label_pairs,
82-
gauge: Some(pb::Gauge { value: Some(*value) }),
80+
gauge: Some(pb::Gauge { value: Some(value) }),
8381

8482
..Default::default()
8583
});
@@ -97,18 +95,20 @@ pub(crate) fn render_protobuf(
9795
}
9896

9997
// Process distributions (histograms and summaries)
100-
for (name, by_labels) in &snapshot.distributions {
101-
let sanitized_name = sanitize_metric_name(name);
98+
for (name, by_labels) in snapshot.distributions {
99+
let sanitized_name = sanitize_metric_name(&name);
102100
let help =
103101
descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
104102

105103
let mut metrics = Vec::new();
104+
let mut metric_type = None;
106105
for (labels, distribution) in by_labels {
107-
let label_pairs = parse_labels(labels, global_labels);
106+
let label_pairs = label_set_to_protobuf(labels);
108107

109108
let metric = match distribution {
110109
Distribution::Summary(summary, quantiles, sum) => {
111110
use quanta::Instant;
111+
metric_type = Some(pb::MetricType::Summary);
112112
let snapshot = summary.snapshot(Instant::now());
113113
let quantile_values: Vec<pb::Quantile> = quantiles
114114
.iter()
@@ -122,7 +122,7 @@ pub(crate) fn render_protobuf(
122122
label: label_pairs,
123123
summary: Some(pb::Summary {
124124
sample_count: Some(summary.count() as u64),
125-
sample_sum: Some(*sum),
125+
sample_sum: Some(sum),
126126
quantile: quantile_values,
127127

128128
created_timestamp: None,
@@ -132,6 +132,7 @@ pub(crate) fn render_protobuf(
132132
}
133133
}
134134
Distribution::Histogram(histogram) => {
135+
metric_type = Some(pb::MetricType::Histogram);
135136
let mut buckets = Vec::new();
136137
for (le, count) in histogram.buckets() {
137138
buckets.push(pb::Bucket {
@@ -167,10 +168,9 @@ pub(crate) fn render_protobuf(
167168
metrics.push(metric);
168169
}
169170

170-
let metric_type = match by_labels.values().next() {
171-
Some(Distribution::Summary(_, _, _)) => pb::MetricType::Summary,
172-
Some(Distribution::Histogram(_)) => pb::MetricType::Histogram,
173-
None => continue, // Skip empty metric families
171+
let Some(metric_type) = metric_type else {
172+
// Skip empty metric families
173+
continue;
174174
};
175175

176176
let metric_family = pb::MetricFamily {
@@ -187,29 +187,11 @@ pub(crate) fn render_protobuf(
187187
output
188188
}
189189

190-
fn parse_labels(labels: &[String], global_labels: &IndexMap<String, String>) -> Vec<pb::LabelPair> {
190+
fn label_set_to_protobuf(labels: LabelSet) -> Vec<pb::LabelPair> {
191191
let mut label_pairs = Vec::new();
192192

193-
// Add global labels first
194-
for (key, value) in global_labels {
195-
label_pairs.push(pb::LabelPair { name: Some(key.clone()), value: Some(value.clone()) });
196-
}
197-
198-
// Add metric-specific labels
199-
for label_str in labels {
200-
if let Some(eq_pos) = label_str.find('=') {
201-
let key = &label_str[..eq_pos];
202-
let value = &label_str[eq_pos + 1..];
203-
let value = value.trim_matches('"');
204-
205-
// Skip if this label key already exists from global labels
206-
if !global_labels.contains_key(key) {
207-
label_pairs.push(pb::LabelPair {
208-
name: Some(key.to_string()),
209-
value: Some(value.to_string()),
210-
});
211-
}
212-
}
193+
for (key, value) in labels.labels {
194+
label_pairs.push(pb::LabelPair { name: Some(key), value: Some(value) });
213195
}
214196

215197
label_pairs
@@ -235,16 +217,18 @@ mod tests {
235217
fn test_render_protobuf_counters() {
236218
let mut counters = HashMap::new();
237219
let mut counter_labels = HashMap::new();
238-
counter_labels.insert(vec!["method=\"GET\"".to_string()], 42u64);
220+
let labels = LabelSet::from_key_and_global(
221+
&metrics::Key::from_parts("", vec![metrics::Label::new("method", "GET")]),
222+
&IndexMap::new(),
223+
);
224+
counter_labels.insert(labels, 42u64);
239225
counters.insert("http_requests".to_string(), counter_labels);
240226

241227
let snapshot = Snapshot { counters, gauges: HashMap::new(), distributions: HashMap::new() };
242228

243229
let descriptions = HashMap::new();
244-
let global_labels = IndexMap::new();
245230

246-
let protobuf_data =
247-
render_protobuf(&snapshot, &descriptions, &global_labels, Some("total"));
231+
let protobuf_data = render_protobuf(snapshot, &descriptions, Some("total"));
248232

249233
assert!(!protobuf_data.is_empty(), "Protobuf data should not be empty");
250234

@@ -264,7 +248,11 @@ mod tests {
264248
fn test_render_protobuf_gauges() {
265249
let mut gauges = HashMap::new();
266250
let mut gauge_labels = HashMap::new();
267-
gauge_labels.insert(vec!["instance=\"localhost\"".to_string()], 0.75f64);
251+
let labels = LabelSet::from_key_and_global(
252+
&metrics::Key::from_parts("", vec![metrics::Label::new("instance", "localhost")]),
253+
&IndexMap::new(),
254+
);
255+
gauge_labels.insert(labels, 0.75f64);
268256
gauges.insert("cpu_usage".to_string(), gauge_labels);
269257

270258
let snapshot = Snapshot { counters: HashMap::new(), gauges, distributions: HashMap::new() };
@@ -274,9 +262,8 @@ mod tests {
274262
"cpu_usage".to_string(),
275263
(SharedString::const_str("CPU usage percentage"), None),
276264
);
277-
let global_labels = IndexMap::new();
278265

279-
let protobuf_data = render_protobuf(&snapshot, &descriptions, &global_labels, None);
266+
let protobuf_data = render_protobuf(snapshot, &descriptions, None);
280267

281268
assert!(!protobuf_data.is_empty(), "Protobuf data should not be empty");
282269

metrics-exporter-prometheus/src/recorder.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, Share
88
use metrics_util::registry::{Recency, Registry};
99
use quanta::Instant;
1010

11-
use crate::common::Snapshot;
11+
use crate::common::{LabelSet, Snapshot};
1212
use crate::distribution::{Distribution, DistributionBuilder};
1313
use crate::formatting::{
14-
key_to_parts, sanitize_metric_name, write_help_line, write_metric_line, write_type_line,
14+
sanitize_metric_name, write_help_line, write_metric_line, write_type_line,
1515
};
1616
use crate::registry::GenerationalAtomicStorage;
1717

1818
#[derive(Debug)]
1919
pub(crate) struct Inner {
2020
pub registry: Registry<Key, GenerationalAtomicStorage>,
2121
pub recency: Recency<Key>,
22-
pub distributions: RwLock<HashMap<String, IndexMap<Vec<String>, Distribution>>>,
22+
pub distributions: RwLock<HashMap<String, IndexMap<LabelSet, Distribution>>>,
2323
pub distribution_builder: DistributionBuilder,
2424
pub descriptions: RwLock<HashMap<String, (SharedString, Option<Unit>)>>,
2525
pub global_labels: IndexMap<String, String>,
@@ -37,7 +37,8 @@ impl Inner {
3737
continue;
3838
}
3939

40-
let (name, labels) = key_to_parts(&key, Some(&self.global_labels));
40+
let name = sanitize_metric_name(key.name());
41+
let labels = LabelSet::from_key_and_global(&key, &self.global_labels);
4142
let value = counter.get_inner().load(Ordering::Acquire);
4243
let entry =
4344
counters.entry(name).or_insert_with(HashMap::new).entry(labels).or_insert(0);
@@ -52,7 +53,8 @@ impl Inner {
5253
continue;
5354
}
5455

55-
let (name, labels) = key_to_parts(&key, Some(&self.global_labels));
56+
let name = sanitize_metric_name(key.name());
57+
let labels = LabelSet::from_key_and_global(&key, &self.global_labels);
5658
let value = f64::from_bits(gauge.get_inner().load(Ordering::Acquire));
5759
let entry =
5860
gauges.entry(name).or_insert_with(HashMap::new).entry(labels).or_insert(0.0);
@@ -69,7 +71,8 @@ impl Inner {
6971
// Since we store aggregated distributions directly, when we're told that a metric
7072
// is not recent enough and should be/was deleted from the registry, we also need to
7173
// delete it on our side as well.
72-
let (name, labels) = key_to_parts(&key, Some(&self.global_labels));
74+
let name = sanitize_metric_name(key.name());
75+
let labels = LabelSet::from_key_and_global(&key, &self.global_labels);
7376
let mut wg = self.distributions.write().unwrap_or_else(PoisonError::into_inner);
7477
let delete_by_name = if let Some(by_name) = wg.get_mut(&name) {
7578
by_name.swap_remove(&labels);
@@ -98,7 +101,8 @@ impl Inner {
98101
fn drain_histograms_to_distributions(&self) {
99102
let histogram_handles = self.registry.get_histogram_handles();
100103
for (key, histogram) in histogram_handles {
101-
let (name, labels) = key_to_parts(&key, Some(&self.global_labels));
104+
let name = sanitize_metric_name(key.name());
105+
let labels = LabelSet::from_key_and_global(&key, &self.global_labels);
102106

103107
let mut wg = self.distributions.write().unwrap_or_else(PoisonError::into_inner);
104108
let entry = wg
@@ -332,12 +336,7 @@ impl PrometheusHandle {
332336
let snapshot = self.inner.get_recent_metrics();
333337
let descriptions = self.inner.descriptions.read().unwrap_or_else(PoisonError::into_inner);
334338

335-
crate::protobuf::render_protobuf(
336-
&snapshot,
337-
&descriptions,
338-
&self.inner.global_labels,
339-
self.inner.counter_suffix,
340-
)
339+
crate::protobuf::render_protobuf(snapshot, &descriptions, self.inner.counter_suffix)
341340
}
342341

343342
/// Performs upkeeping operations to ensure metrics held by recorder are up-to-date and do not

0 commit comments

Comments
 (0)