Skip to main content

opentelemetry_prometheus/
lib.rs

1//! An OpenTelemetry exporter for [Prometheus] metrics.
2//!
3//! [Prometheus]: https://prometheus.io
4//!
5//! ```
6//! use opentelemetry::{metrics::MeterProvider, KeyValue};
7//! use opentelemetry_sdk::metrics::SdkMeterProvider;
8//! use prometheus::{Encoder, TextEncoder};
9//!
10//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
11//!
12//! // create a new prometheus registry
13//! let registry = prometheus::Registry::new();
14//!
15//! // configure OpenTelemetry to use this registry
16//! let exporter = opentelemetry_prometheus::exporter()
17//!     .with_registry(registry.clone())
18//!     .build()?;
19//!
20//! // set up a meter to create instruments
21//! let provider = SdkMeterProvider::builder().with_reader(exporter).build();
22//! let meter = provider.meter("my-app");
23//!
24//! // Use two instruments
25//! let counter = meter
26//!     .u64_counter("a.counter")
27//!     .with_description("Counts things")
28//!     .build();
29//! let histogram = meter
30//!     .u64_histogram("a.histogram")
31//!     .with_description("Records values")
32//!     .build();
33//!
34//! counter.add(100, &[KeyValue::new("key", "value")]);
35//! histogram.record(100, &[KeyValue::new("key", "value")]);
36//!
37//! // Encode data as text or protobuf
38//! let encoder = TextEncoder::new();
39//! let metric_families = registry.gather();
40//! let mut result = Vec::new();
41//! encoder.encode(&metric_families, &mut result)?;
42//!
43//! // result now contains encoded metrics:
44//! //
45//! // # HELP a_counter_total Counts things
46//! // # TYPE a_counter_total counter
47//! // a_counter_total{key="value",otel_scope_name="my-app"} 100
48//! // # HELP a_histogram Records values
49//! // # TYPE a_histogram histogram
50//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="0"} 0
51//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="5"} 0
52//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="10"} 0
53//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="25"} 0
54//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="50"} 0
55//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="75"} 0
56//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="100"} 1
57//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="250"} 1
58//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="500"} 1
59//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="750"} 1
60//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="1000"} 1
61//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="2500"} 1
62//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="5000"} 1
63//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="7500"} 1
64//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="10000"} 1
65//! // a_histogram_bucket{key="value",otel_scope_name="my-app",le="+Inf"} 1
66//! // a_histogram_sum{key="value",otel_scope_name="my-app"} 100
67//! // a_histogram_count{key="value",otel_scope_name="my-app"} 1
68//! // # HELP otel_scope_info Instrumentation Scope metadata
69//! // # TYPE otel_scope_info gauge
70//! // otel_scope_info{otel_scope_name="my-app"} 1
71//! // # HELP target_info Target metadata
72//! // # TYPE target_info gauge
73//! // target_info{service_name="unknown_service"} 1
74//! # Ok(())
75//! # }
76//! ```
77#![warn(
78    future_incompatible,
79    missing_debug_implementations,
80    missing_docs,
81    nonstandard_style,
82    rust_2018_idioms,
83    unreachable_pub,
84    unused
85)]
86#![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))]
87#![doc(
88    html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg"
89)]
90#![cfg_attr(test, deny(warnings))]
91
92use once_cell::sync::OnceCell;
93use opentelemetry::{otel_error, otel_warn, InstrumentationScope, Key, Value};
94use opentelemetry_sdk::{
95    error::OTelSdkResult,
96    metrics::{
97        data::{self, ResourceMetrics},
98        reader::MetricReader,
99        InstrumentKind, ManualReader, Pipeline, Temporality,
100    },
101    Resource,
102};
103use prometheus::{
104    core::Desc,
105    proto::{LabelPair, MetricFamily, MetricType},
106};
107use std::{
108    borrow::Cow,
109    collections::{BTreeMap, HashMap},
110    sync::{Arc, Mutex},
111};
112use std::{fmt, sync::Weak};
113
114const TARGET_INFO_NAME: &str = "target_info";
115const TARGET_INFO_DESCRIPTION: &str = "Target metadata";
116
117const SCOPE_INFO_METRIC_NAME: &str = "otel_scope_info";
118const SCOPE_INFO_DESCRIPTION: &str = "Instrumentation Scope metadata";
119
120const SCOPE_INFO_KEYS: [&str; 2] = ["otel_scope_name", "otel_scope_version"];
121
122// prometheus counters MUST have a _total suffix by default:
123// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/compatibility/prometheus_and_openmetrics.md
124const COUNTER_SUFFIX: &str = "_total";
125
126mod config;
127mod resource_selector;
128mod utils;
129
130pub use config::ExporterBuilder;
131pub use resource_selector::ResourceSelector;
132
133/// Creates a builder to configure a [PrometheusExporter]
134pub fn exporter() -> ExporterBuilder {
135    ExporterBuilder::default()
136}
137
138/// Prometheus metrics exporter
139#[derive(Debug)]
140pub struct PrometheusExporter {
141    reader: Arc<ManualReader>,
142}
143
144impl MetricReader for PrometheusExporter {
145    fn register_pipeline(&self, pipeline: Weak<Pipeline>) {
146        self.reader.register_pipeline(pipeline)
147    }
148
149    fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
150        self.reader.collect(rm)
151    }
152
153    fn force_flush(&self) -> OTelSdkResult {
154        self.reader.force_flush()
155    }
156
157    fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> OTelSdkResult {
158        self.reader.shutdown_with_timeout(timeout)
159    }
160
161    /// Note: Prometheus only supports cumulative temporality, so this will always be
162    /// [Temporality::Cumulative].
163    fn temporality(&self, _kind: InstrumentKind) -> Temporality {
164        Temporality::Cumulative
165    }
166}
167
168struct Collector {
169    reader: Arc<ManualReader>,
170    disable_target_info: bool,
171    without_units: bool,
172    without_counter_suffixes: bool,
173    disable_scope_info: bool,
174    create_target_info_once: OnceCell<MetricFamily>,
175    resource_labels_once: OnceCell<Vec<LabelPair>>,
176    namespace: Option<String>,
177    inner: Mutex<CollectorInner>,
178    resource_selector: ResourceSelector,
179}
180
181#[derive(Default)]
182struct CollectorInner {
183    scope_infos: HashMap<InstrumentationScope, MetricFamily>,
184    metric_families: HashMap<String, MetricFamily>,
185}
186
187impl Collector {
188    fn metric_type_and_name(&self, m: &data::Metric) -> Option<(MetricType, Cow<'static, str>)> {
189        let mut name = self.get_name(m);
190
191        let result = match m.data() {
192            data::AggregatedMetrics::F64(metric_data) => match metric_data {
193                data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
194                data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
195                data::MetricData::Sum(sum) => {
196                    if sum.is_monotonic() {
197                        if !self.without_counter_suffixes {
198                            name = format!("{name}{COUNTER_SUFFIX}").into();
199                        }
200                        Some(MetricType::COUNTER)
201                    } else {
202                        Some(MetricType::GAUGE)
203                    }
204                }
205                data::MetricData::ExponentialHistogram(_) => None,
206            },
207            data::AggregatedMetrics::I64(metric_data) => match metric_data {
208                data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
209                data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
210                data::MetricData::Sum(sum) => {
211                    if sum.is_monotonic() {
212                        if !self.without_counter_suffixes {
213                            name = format!("{name}{COUNTER_SUFFIX}").into();
214                        }
215                        Some(MetricType::COUNTER)
216                    } else {
217                        Some(MetricType::GAUGE)
218                    }
219                }
220                data::MetricData::ExponentialHistogram(_) => None,
221            },
222            data::AggregatedMetrics::U64(metric_data) => match metric_data {
223                data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
224                data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
225                data::MetricData::Sum(sum) => {
226                    if sum.is_monotonic() {
227                        if !self.without_counter_suffixes {
228                            name = format!("{name}{COUNTER_SUFFIX}").into();
229                        }
230                        Some(MetricType::COUNTER)
231                    } else {
232                        Some(MetricType::GAUGE)
233                    }
234                }
235                data::MetricData::ExponentialHistogram(_) => None,
236            },
237        };
238
239        result.map(|metric_type| (metric_type, name))
240    }
241
242    fn get_name(&self, m: &data::Metric) -> Cow<'static, str> {
243        let name: Cow<'static, str> = Cow::Owned(m.name().to_string());
244        let name = utils::sanitize_name(&name);
245        let unit_suffixes = if self.without_units {
246            None
247        } else {
248            utils::get_unit_suffixes(m.unit())
249        };
250        match (&self.namespace, unit_suffixes) {
251            (Some(namespace), Some(suffix)) => Cow::Owned(format!("{namespace}{name}_{suffix}")),
252            (Some(namespace), None) => Cow::Owned(format!("{namespace}{name}")),
253            (None, Some(suffix)) => Cow::Owned(format!("{name}_{suffix}")),
254            (None, None) => name,
255        }
256    }
257}
258
259impl prometheus::core::Collector for Collector {
260    fn desc(&self) -> Vec<&Desc> {
261        Vec::new()
262    }
263
264    fn collect(&self) -> Vec<MetricFamily> {
265        let mut inner = match self.inner.lock() {
266            Ok(guard) => guard,
267            Err(err) => {
268                otel_error!(
269                    name: "MetricScrapeFailed",
270                    message = err.to_string(),
271                );
272                return Vec::new();
273            }
274        };
275
276        let mut metrics = ResourceMetrics::default();
277        if let Err(err) = self.reader.collect(&mut metrics) {
278            otel_error!(
279                name: "MetricScrapeFailed",
280                message = err.to_string(),
281            );
282            return vec![];
283        }
284        let mut res = Vec::with_capacity(metrics.scope_metrics().count() + 1);
285
286        let target_info = self.create_target_info_once.get_or_init(|| {
287            // Resource should be immutable, we don't need to compute again
288            create_info_metric(
289                TARGET_INFO_NAME,
290                TARGET_INFO_DESCRIPTION,
291                metrics.resource(),
292            )
293        });
294
295        if !self.disable_target_info && !metrics.resource().is_empty() {
296            res.push(target_info.clone())
297        }
298
299        let resource_labels = self
300            .resource_labels_once
301            .get_or_init(|| self.resource_selector.select(metrics.resource()));
302
303        for scope_metrics in metrics.scope_metrics() {
304            let scope_labels = if !self.disable_scope_info {
305                if scope_metrics.scope().attributes().count() > 0 {
306                    let scope_info = inner
307                        .scope_infos
308                        .entry(scope_metrics.scope().clone())
309                        .or_insert_with_key(create_scope_info_metric);
310                    res.push(scope_info.clone());
311                }
312
313                let mut labels =
314                    Vec::with_capacity(1 + scope_metrics.scope().version().is_some() as usize);
315                let mut name = LabelPair::default();
316                name.set_name(SCOPE_INFO_KEYS[0].into());
317                name.set_value(scope_metrics.scope().name().to_string());
318                labels.push(name);
319                if let Some(version) = &scope_metrics.scope().version() {
320                    let mut l_version = LabelPair::default();
321                    l_version.set_name(SCOPE_INFO_KEYS[1].into());
322                    l_version.set_value(version.to_string());
323                    labels.push(l_version);
324                }
325
326                if !resource_labels.is_empty() {
327                    labels.extend(resource_labels.iter().cloned());
328                }
329                labels
330            } else {
331                Vec::new()
332            };
333
334            for metrics in scope_metrics.metrics() {
335                let (metric_type, name) = match self.metric_type_and_name(metrics) {
336                    Some((metric_type, name)) => (metric_type, name),
337                    _ => continue,
338                };
339
340                let mfs = &mut inner.metric_families;
341                let (drop, help) = validate_metrics(&name, metrics.description(), metric_type, mfs);
342                if drop {
343                    continue;
344                }
345
346                let description = help.unwrap_or_else(|| metrics.description().into());
347
348                match metrics.data() {
349                    data::AggregatedMetrics::F64(metric_data) => match metric_data {
350                        data::MetricData::Histogram(hist) => {
351                            add_histogram_metric(&mut res, hist, description, &scope_labels, name);
352                        }
353                        data::MetricData::Sum(sum) => {
354                            add_sum_metric(&mut res, sum, description, &scope_labels, name);
355                        }
356                        data::MetricData::Gauge(gauge) => {
357                            add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
358                        }
359                        data::MetricData::ExponentialHistogram(_) => {}
360                    },
361                    data::AggregatedMetrics::I64(metric_data) => match metric_data {
362                        data::MetricData::Histogram(hist) => {
363                            add_histogram_metric(&mut res, hist, description, &scope_labels, name);
364                        }
365                        data::MetricData::Sum(sum) => {
366                            add_sum_metric(&mut res, sum, description, &scope_labels, name);
367                        }
368                        data::MetricData::Gauge(gauge) => {
369                            add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
370                        }
371                        data::MetricData::ExponentialHistogram(_) => {}
372                    },
373                    data::AggregatedMetrics::U64(metric_data) => match metric_data {
374                        data::MetricData::Histogram(hist) => {
375                            add_histogram_metric(&mut res, hist, description, &scope_labels, name);
376                        }
377                        data::MetricData::Sum(sum) => {
378                            add_sum_metric(&mut res, sum, description, &scope_labels, name);
379                        }
380                        data::MetricData::Gauge(gauge) => {
381                            add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
382                        }
383                        data::MetricData::ExponentialHistogram(_) => {}
384                    },
385                }
386            }
387        }
388
389        res
390    }
391}
392
393/// Maps attributes into Prometheus-style label pairs.
394///
395/// It sanitizes invalid characters and handles duplicate keys (due to
396/// sanitization) by sorting and concatenating the values following the spec.
397fn get_attrs(kvs: &mut dyn Iterator<Item = (&Key, &Value)>, extra: &[LabelPair]) -> Vec<LabelPair> {
398    let mut keys_map = BTreeMap::<String, Vec<String>>::new();
399    for (key, value) in kvs {
400        let key = utils::sanitize_prom_kv(key.as_str());
401        keys_map
402            .entry(key)
403            .and_modify(|v| v.push(value.to_string()))
404            .or_insert_with(|| vec![value.to_string()]);
405    }
406
407    let mut res = Vec::with_capacity(keys_map.len() + extra.len());
408
409    for (key, mut values) in keys_map.into_iter() {
410        values.sort_unstable();
411
412        let mut lp = LabelPair::default();
413        lp.set_name(key);
414        lp.set_value(values.join(";"));
415        res.push(lp);
416    }
417
418    if !extra.is_empty() {
419        res.extend(&mut extra.iter().cloned());
420    }
421
422    res
423}
424
425fn validate_metrics(
426    name: &str,
427    description: &str,
428    metric_type: MetricType,
429    mfs: &mut HashMap<String, MetricFamily>,
430) -> (bool, Option<String>) {
431    if let Some(existing) = mfs.get(name) {
432        if existing.get_field_type() != metric_type {
433            otel_warn!(
434                name: "MetricValidationFailed",
435                message = "Instrument type conflict, using existing type definition",
436                metric_type = format!("Instrument {name}, Existing: {:?}, dropped: {:?}", existing.get_field_type(), metric_type).as_str(),
437            );
438            return (true, None);
439        }
440        if existing.help() != description {
441            otel_warn!(
442                name: "MetricValidationFailed",
443                message = "Instrument description conflict, using existing",
444                metric_description = format!("Instrument {name}, Existing: {:?}, dropped: {:?}", existing.help().to_string(), description.to_string()).as_str(),
445            );
446            return (false, Some(existing.help().to_string()));
447        }
448        (false, None)
449    } else {
450        let mut mf = MetricFamily::default();
451        mf.set_name(name.into());
452        mf.set_help(description.to_string());
453        mf.set_field_type(metric_type);
454        mfs.insert(name.to_string(), mf);
455
456        (false, None)
457    }
458}
459
460fn add_histogram_metric<T: Numeric + Copy>(
461    res: &mut Vec<MetricFamily>,
462    histogram: &data::Histogram<T>,
463    description: String,
464    extra: &[LabelPair],
465    name: Cow<'static, str>,
466) {
467    // Consider supporting exemplars when `prometheus` crate has the feature
468    // See: https://github.com/tikv/rust-prometheus/issues/393
469
470    for dp in histogram.data_points() {
471        let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
472        let bounds: Vec<f64> = dp.bounds().collect();
473        let bucket_counts: Vec<u64> = dp.bucket_counts().collect();
474        let bounds_len = bounds.len();
475        let (bucket, _) = bounds.iter().enumerate().fold(
476            (Vec::with_capacity(bounds_len), 0),
477            |(mut acc, mut count), (i, bound)| {
478                count += bucket_counts[i];
479
480                let mut b = prometheus::proto::Bucket::default();
481                b.set_upper_bound(*bound);
482                b.set_cumulative_count(count);
483                acc.push(b);
484                (acc, count)
485            },
486        );
487
488        let mut h = prometheus::proto::Histogram::default();
489        h.set_sample_sum(dp.sum().as_f64());
490        h.set_sample_count(dp.count());
491        h.set_bucket(bucket);
492        let mut pm = prometheus::proto::Metric::default();
493        pm.set_label(kvs);
494        pm.set_histogram(h);
495
496        let mut mf = prometheus::proto::MetricFamily::default();
497        mf.set_name(name.to_string());
498        mf.set_help(description.clone());
499        mf.set_field_type(prometheus::proto::MetricType::HISTOGRAM);
500        mf.set_metric(vec![pm]);
501        res.push(mf);
502    }
503}
504
505fn add_sum_metric<T: Numeric + Copy>(
506    res: &mut Vec<MetricFamily>,
507    sum: &data::Sum<T>,
508    description: String,
509    extra: &[LabelPair],
510    name: Cow<'static, str>,
511) {
512    let metric_type = if sum.is_monotonic() {
513        MetricType::COUNTER
514    } else {
515        MetricType::GAUGE
516    };
517
518    for dp in sum.data_points() {
519        let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
520
521        let mut pm = prometheus::proto::Metric::default();
522        pm.set_label(kvs);
523
524        if sum.is_monotonic() {
525            let mut c = prometheus::proto::Counter::default();
526            c.set_value(dp.value().as_f64());
527            pm.set_counter(c);
528        } else {
529            let mut g = prometheus::proto::Gauge::default();
530            g.set_value(dp.value().as_f64());
531            pm.set_gauge(g);
532        }
533
534        let mut mf = prometheus::proto::MetricFamily::default();
535        mf.set_name(name.to_string());
536        mf.set_help(description.clone());
537        mf.set_field_type(metric_type);
538        mf.set_metric(vec![pm]);
539        res.push(mf);
540    }
541}
542
543fn add_gauge_metric<T: Numeric + Copy>(
544    res: &mut Vec<MetricFamily>,
545    gauge: &data::Gauge<T>,
546    description: String,
547    extra: &[LabelPair],
548    name: Cow<'static, str>,
549) {
550    for dp in gauge.data_points() {
551        let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
552
553        let mut g = prometheus::proto::Gauge::default();
554        g.set_value(dp.value().as_f64());
555        let mut pm = prometheus::proto::Metric::default();
556        pm.set_label(kvs);
557        pm.set_gauge(g);
558
559        let mut mf = prometheus::proto::MetricFamily::default();
560        mf.set_name(name.to_string());
561        mf.set_help(description.to_string());
562        mf.set_field_type(MetricType::GAUGE);
563        mf.set_metric(vec![pm]);
564        res.push(mf);
565    }
566}
567
568fn create_info_metric(
569    target_info_name: &str,
570    target_info_description: &str,
571    resource: &Resource,
572) -> MetricFamily {
573    let mut g = prometheus::proto::Gauge::default();
574    g.set_value(1.0);
575
576    let mut m = prometheus::proto::Metric::default();
577    m.set_label(get_attrs(&mut resource.iter(), &[]));
578    m.set_gauge(g);
579
580    let mut mf = MetricFamily::default();
581    mf.set_name(target_info_name.into());
582    mf.set_help(target_info_description.into());
583    mf.set_field_type(MetricType::GAUGE);
584    mf.set_metric(vec![m]);
585    mf
586}
587
588fn create_scope_info_metric(scope: &InstrumentationScope) -> MetricFamily {
589    let mut g = prometheus::proto::Gauge::default();
590    g.set_value(1.0);
591
592    let mut labels = Vec::with_capacity(1 + scope.version().is_some() as usize);
593    let mut name = LabelPair::default();
594    name.set_name(SCOPE_INFO_KEYS[0].into());
595    name.set_value(scope.name().to_string());
596    labels.push(name);
597    if let Some(version) = &scope.version() {
598        let mut v_label = LabelPair::default();
599        v_label.set_name(SCOPE_INFO_KEYS[1].into());
600        v_label.set_value(version.to_string());
601        labels.push(v_label);
602    }
603
604    let mut m = prometheus::proto::Metric::default();
605    m.set_label(labels);
606    m.set_gauge(g);
607
608    let mut mf = MetricFamily::default();
609    mf.set_name(SCOPE_INFO_METRIC_NAME.into());
610    mf.set_help(SCOPE_INFO_DESCRIPTION.into());
611    mf.set_field_type(MetricType::GAUGE);
612    mf.set_metric(vec![m]);
613    mf
614}
615
616trait Numeric: fmt::Debug {
617    // lossy at large values for u64 and i64 but prometheus only handles floats
618    fn as_f64(&self) -> f64;
619}
620
621impl Numeric for u64 {
622    fn as_f64(&self) -> f64 {
623        *self as f64
624    }
625}
626
627impl Numeric for i64 {
628    fn as_f64(&self) -> f64 {
629        *self as f64
630    }
631}
632
633impl Numeric for f64 {
634    fn as_f64(&self) -> f64 {
635        *self
636    }
637}