Skip to content

Commit 374e409

Browse files
authored
Merge branch 'main' into feature/prometheus-exporter-no-tls
2 parents 3fd8bb0 + 32c62dc commit 374e409

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)