metrics_exporter_prometheus/
protobuf.rs1use metrics::Unit;
4use prost::Message;
5use std::collections::HashMap;
6
7use crate::common::{LabelSet, Snapshot};
8use crate::distribution::Distribution;
9use crate::formatting::sanitize_metric_name;
10
11mod pb {
13 #![allow(missing_docs, clippy::trivially_copy_pass_by_ref, clippy::doc_markdown)]
14 include!(concat!(env!("OUT_DIR"), "/io.prometheus.client.rs"));
15}
16
17#[cfg(feature = "http-listener")]
18pub(crate) const PROTOBUF_CONTENT_TYPE: &str =
19 "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited";
20
21#[allow(clippy::too_many_lines)]
27pub(crate) fn render_protobuf(
28 snapshot: Snapshot,
29 descriptions: &HashMap<String, (metrics::SharedString, Option<Unit>)>,
30 counter_suffix: Option<&'static str>,
31) -> Vec<u8> {
32 let mut output = Vec::new();
33
34 for (name, by_labels) in snapshot.counters {
36 let sanitized_name = sanitize_metric_name(&name);
37 let help =
38 descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
39
40 let mut metrics = Vec::new();
41 for (labels, value) in by_labels {
42 let label_pairs = label_set_to_protobuf(labels);
43
44 metrics.push(pb::Metric {
45 label: label_pairs,
46 counter: Some(pb::Counter {
47 #[allow(clippy::cast_precision_loss)]
48 value: Some(value as f64),
49
50 ..Default::default()
51 }),
52
53 ..Default::default()
54 });
55 }
56
57 let metric_family = pb::MetricFamily {
58 name: Some(add_suffix_to_name(&sanitized_name, counter_suffix)),
59 help: if help.is_empty() { None } else { Some(help) },
60 r#type: Some(pb::MetricType::Counter as i32),
61 metric: metrics,
62 unit: None,
63 };
64
65 metric_family.encode_length_delimited(&mut output).unwrap();
66 }
67
68 for (name, by_labels) in snapshot.gauges {
70 let sanitized_name = sanitize_metric_name(&name);
71 let help =
72 descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
73
74 let mut metrics = Vec::new();
75 for (labels, value) in by_labels {
76 let label_pairs = label_set_to_protobuf(labels);
77
78 metrics.push(pb::Metric {
79 label: label_pairs,
80 gauge: Some(pb::Gauge { value: Some(value) }),
81
82 ..Default::default()
83 });
84 }
85
86 let metric_family = pb::MetricFamily {
87 name: Some(sanitized_name),
88 help: if help.is_empty() { None } else { Some(help) },
89 r#type: Some(pb::MetricType::Gauge as i32),
90 metric: metrics,
91 unit: None,
92 };
93
94 metric_family.encode_length_delimited(&mut output).unwrap();
95 }
96
97 for (name, by_labels) in snapshot.distributions {
99 let sanitized_name = sanitize_metric_name(&name);
100 let help =
101 descriptions.get(name.as_str()).map(|(desc, _)| desc.to_string()).unwrap_or_default();
102
103 let mut metrics = Vec::new();
104 let mut metric_type = None;
105 for (labels, distribution) in by_labels {
106 let label_pairs = label_set_to_protobuf(labels);
107
108 let metric = match distribution {
109 Distribution::Summary(summary, quantiles, sum) => {
110 use quanta::Instant;
111 metric_type = Some(pb::MetricType::Summary);
112 let snapshot = summary.snapshot(Instant::now());
113 let quantile_values: Vec<pb::Quantile> = quantiles
114 .iter()
115 .map(|q| pb::Quantile {
116 quantile: Some(q.value()),
117 value: Some(snapshot.quantile(q.value()).unwrap_or(0.0)),
118 })
119 .collect();
120
121 pb::Metric {
122 label: label_pairs,
123 summary: Some(pb::Summary {
124 sample_count: Some(summary.count() as u64),
125 sample_sum: Some(sum),
126 quantile: quantile_values,
127
128 created_timestamp: None,
129 }),
130
131 ..Default::default()
132 }
133 }
134 Distribution::Histogram(histogram) => {
135 metric_type = Some(pb::MetricType::Histogram);
136 let mut buckets = Vec::new();
137 for (le, count) in histogram.buckets() {
138 buckets.push(pb::Bucket {
139 cumulative_count: Some(count),
140 upper_bound: Some(le),
141
142 ..Default::default()
143 });
144 }
145 buckets.push(pb::Bucket {
147 cumulative_count: Some(histogram.count()),
148 upper_bound: Some(f64::INFINITY),
149
150 ..Default::default()
151 });
152
153 pb::Metric {
154 label: label_pairs,
155 histogram: Some(pb::Histogram {
156 sample_count: Some(histogram.count()),
157 sample_sum: Some(histogram.sum()),
158 bucket: buckets,
159
160 ..Default::default()
161 }),
162
163 ..Default::default()
164 }
165 }
166 Distribution::NativeHistogram(native_hist) => {
167 let positive_buckets = native_hist.positive_buckets();
169 let negative_buckets = native_hist.negative_buckets();
170
171 let schema = native_hist.schema();
173
174 let (positive_spans, positive_deltas) = make_buckets(&positive_buckets);
176 let (negative_spans, negative_deltas) = make_buckets(&negative_buckets);
177
178 let mut histogram = pb::Histogram {
180 sample_count: Some(native_hist.count()),
181 sample_sum: Some(native_hist.sum()),
182
183 zero_threshold: Some(native_hist.config().zero_threshold()),
185 schema: Some(schema),
186 zero_count: Some(native_hist.zero_count()),
187
188 positive_span: positive_spans,
189 positive_delta: positive_deltas,
190
191 negative_span: negative_spans,
192 negative_delta: negative_deltas,
193
194 ..Default::default()
195 };
196
197 if histogram.zero_threshold == Some(0.0)
199 && histogram.zero_count == Some(0)
200 && histogram.positive_span.is_empty()
201 && histogram.negative_span.is_empty()
202 {
203 histogram.positive_span =
204 vec![pb::BucketSpan { offset: Some(0), length: Some(0) }];
205 }
206
207 pb::Metric {
208 label: label_pairs,
209 histogram: Some(histogram),
210 ..Default::default()
211 }
212 }
213 };
214
215 metrics.push(metric);
216 }
217
218 let Some(metric_type) = metric_type else {
219 continue;
221 };
222
223 let metric_family = pb::MetricFamily {
224 name: Some(sanitized_name),
225 help: if help.is_empty() { None } else { Some(help) },
226 r#type: Some(metric_type as i32),
227 metric: metrics,
228 unit: None,
229 };
230
231 metric_family.encode_length_delimited(&mut output).unwrap();
232 }
233
234 output
235}
236
237fn label_set_to_protobuf(labels: LabelSet) -> Vec<pb::LabelPair> {
238 let mut label_pairs = Vec::new();
239
240 for (key, value) in labels.labels {
241 label_pairs.push(pb::LabelPair { name: Some(key), value: Some(value) });
242 }
243
244 label_pairs
245}
246
247fn add_suffix_to_name(name: &str, suffix: Option<&'static str>) -> String {
248 match suffix {
249 Some(suffix) if !name.ends_with(suffix) => format!("{name}_{suffix}"),
250 _ => name.to_string(),
251 }
252}
253
254fn make_buckets(buckets: &std::collections::BTreeMap<i32, u64>) -> (Vec<pb::BucketSpan>, Vec<i64>) {
257 if buckets.is_empty() {
258 return (vec![], vec![]);
259 }
260
261 let mut indices: Vec<i32> = buckets.keys().copied().collect();
263 indices.sort_unstable();
264
265 let mut spans = Vec::new();
266 let mut deltas = Vec::new();
267 let mut prev_count = 0i64;
268 let mut next_i = 0i32;
269
270 for (n, &i) in indices.iter().enumerate() {
271 #[allow(clippy::cast_possible_wrap)]
272 let count = buckets[&i] as i64;
273
274 let i_delta = i - next_i;
278
279 if n == 0 || i_delta > 2 {
280 spans.push(pb::BucketSpan { offset: Some(i_delta), length: Some(0) });
282 } else {
283 for _ in 0..i_delta {
285 if let Some(last_span) = spans.last_mut() {
286 *last_span.length.as_mut().unwrap() += 1;
287 }
288 deltas.push(-prev_count);
289 prev_count = 0;
290 }
291 }
292
293 if let Some(last_span) = spans.last_mut() {
295 *last_span.length.as_mut().unwrap() += 1;
296 }
297 deltas.push(count - prev_count);
298 prev_count = count;
299 next_i = i + 1;
300 }
301
302 (spans, deltas)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::common::Snapshot;
309 use indexmap::IndexMap;
310 use metrics::SharedString;
311 use prost::Message;
312 use std::collections::HashMap;
313
314 #[test]
315 fn test_render_protobuf_counters() {
316 let mut counters = HashMap::new();
317 let mut counter_labels = HashMap::new();
318 let labels = LabelSet::from_key_and_global(
319 &metrics::Key::from_parts("", vec![metrics::Label::new("method", "GET")]),
320 &IndexMap::new(),
321 );
322 counter_labels.insert(labels, 42u64);
323 counters.insert("http_requests".to_string(), counter_labels);
324
325 let snapshot = Snapshot { counters, gauges: HashMap::new(), distributions: HashMap::new() };
326
327 let descriptions = HashMap::new();
328
329 let protobuf_data = render_protobuf(snapshot, &descriptions, Some("total"));
330
331 assert!(!protobuf_data.is_empty(), "Protobuf data should not be empty");
332
333 let metric_family = pb::MetricFamily::decode_length_delimited(&protobuf_data[..]).unwrap();
335
336 assert_eq!(metric_family.name.as_ref().unwrap(), "http_requests_total");
337 assert_eq!(metric_family.r#type.unwrap(), pb::MetricType::Counter as i32);
338 assert_eq!(metric_family.metric.len(), 1);
339
340 let metric = &metric_family.metric[0];
341 assert!(metric.counter.is_some());
342 let counter_value = metric.counter.as_ref().unwrap().value.unwrap();
343 assert!((counter_value - 42.0).abs() < f64::EPSILON);
344 }
345
346 #[test]
347 fn test_render_protobuf_gauges() {
348 let mut gauges = HashMap::new();
349 let mut gauge_labels = HashMap::new();
350 let labels = LabelSet::from_key_and_global(
351 &metrics::Key::from_parts("", vec![metrics::Label::new("instance", "localhost")]),
352 &IndexMap::new(),
353 );
354 gauge_labels.insert(labels, 0.75f64);
355 gauges.insert("cpu_usage".to_string(), gauge_labels);
356
357 let snapshot = Snapshot { counters: HashMap::new(), gauges, distributions: HashMap::new() };
358
359 let mut descriptions = HashMap::new();
360 descriptions.insert(
361 "cpu_usage".to_string(),
362 (SharedString::const_str("CPU usage percentage"), None),
363 );
364
365 let protobuf_data = render_protobuf(snapshot, &descriptions, None);
366
367 assert!(!protobuf_data.is_empty(), "Protobuf data should not be empty");
368
369 let metric_family = pb::MetricFamily::decode_length_delimited(&protobuf_data[..]).unwrap();
371
372 assert_eq!(metric_family.name.as_ref().unwrap(), "cpu_usage");
373 assert_eq!(metric_family.r#type.unwrap(), pb::MetricType::Gauge as i32);
374 assert_eq!(metric_family.help.as_ref().unwrap(), "CPU usage percentage");
375
376 let metric = &metric_family.metric[0];
377 assert!(metric.gauge.is_some());
378 let gauge_value = metric.gauge.as_ref().unwrap().value.unwrap();
379 assert!((gauge_value - 0.75).abs() < f64::EPSILON);
380 }
381
382 #[test]
383 fn test_add_suffix_to_name() {
384 assert_eq!(add_suffix_to_name("requests", Some("total")), "requests_total");
385 assert_eq!(add_suffix_to_name("requests_total", Some("total")), "requests_total");
386 assert_eq!(add_suffix_to_name("requests", None), "requests");
387 }
388}