scuffle_metrics/prometheus/
mod.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use opentelemetry::{InstrumentationScope, KeyValue, otel_error, otel_warn};
5use opentelemetry_sdk::Resource;
6use opentelemetry_sdk::metrics::data::{Gauge, Histogram, ResourceMetrics, Sum};
7use opentelemetry_sdk::metrics::reader::MetricReader;
8use opentelemetry_sdk::metrics::{ManualReader, ManualReaderBuilder};
9use prometheus_client::encoding::{EncodeCounterValue, EncodeGaugeValue, NoLabelSet};
10use prometheus_client::metrics::MetricType;
11use prometheus_client::registry::Unit;
12
13/// A Prometheus exporter for OpenTelemetry metrics.
14///
15/// Responsible for encoding OpenTelemetry metrics into Prometheus format.
16/// The exporter implements the
17/// [`opentelemetry_sdk::metrics::reader::MetricReader`](https://docs.rs/opentelemetry_sdk/0.27.0/opentelemetry_sdk/metrics/reader/trait.MetricReader.html)
18/// trait and therefore can be passed to a
19/// [`opentelemetry_sdk::metrics::SdkMeterProvider`](https://docs.rs/opentelemetry_sdk/0.27.0/opentelemetry_sdk/metrics/struct.SdkMeterProvider.html).
20///
21/// Use [`collector`](PrometheusExporter::collector) to get a
22/// [`prometheus_client::collector::Collector`](https://docs.rs/prometheus-client/0.22.3/prometheus_client/collector/trait.Collector.html)
23/// that can be registered with a
24/// [`prometheus_client::registry::Registry`](https://docs.rs/prometheus-client/0.22.3/prometheus_client/registry/struct.Registry.html)
25/// to provide metrics to Prometheus.
26#[derive(Debug, Clone)]
27pub struct PrometheusExporter {
28    reader: Arc<ManualReader>,
29    prometheus_full_utf8: bool,
30}
31
32impl PrometheusExporter {
33    /// Returns a new [`PrometheusExporterBuilder`] to configure a [`PrometheusExporter`].
34    pub fn builder() -> PrometheusExporterBuilder {
35        PrometheusExporterBuilder::default()
36    }
37
38    /// Returns a [`prometheus_client::collector::Collector`] that can be registered
39    /// with a [`prometheus_client::registry::Registry`] to provide metrics to Prometheus.
40    pub fn collector(&self) -> Box<dyn prometheus_client::collector::Collector> {
41        Box::new(self.clone())
42    }
43}
44
45impl MetricReader for PrometheusExporter {
46    fn register_pipeline(&self, pipeline: std::sync::Weak<opentelemetry_sdk::metrics::Pipeline>) {
47        self.reader.register_pipeline(pipeline)
48    }
49
50    fn collect(
51        &self,
52        rm: &mut opentelemetry_sdk::metrics::data::ResourceMetrics,
53    ) -> opentelemetry_sdk::metrics::MetricResult<()> {
54        self.reader.collect(rm)
55    }
56
57    fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
58        self.reader.force_flush()
59    }
60
61    fn shutdown(&self) -> opentelemetry_sdk::error::OTelSdkResult {
62        self.reader.shutdown()
63    }
64
65    fn temporality(&self, kind: opentelemetry_sdk::metrics::InstrumentKind) -> opentelemetry_sdk::metrics::Temporality {
66        self.reader.temporality(kind)
67    }
68}
69
70/// Builder for [`PrometheusExporter`].
71#[derive(Default)]
72pub struct PrometheusExporterBuilder {
73    reader: ManualReaderBuilder,
74    prometheus_full_utf8: bool,
75}
76
77impl PrometheusExporterBuilder {
78    /// Set the reader temporality.
79    pub fn with_temporality(mut self, temporality: opentelemetry_sdk::metrics::Temporality) -> Self {
80        self.reader = self.reader.with_temporality(temporality);
81        self
82    }
83
84    /// Allow full UTF-8 labels in Prometheus.
85    ///
86    /// This is disabled by default however if you are using a newer version of
87    /// Prometheus that supports full UTF-8 labels you may enable this feature.
88    pub fn with_prometheus_full_utf8(mut self, prometheus_full_utf8: bool) -> Self {
89        self.prometheus_full_utf8 = prometheus_full_utf8;
90        self
91    }
92
93    /// Build the [`PrometheusExporter`].
94    pub fn build(self) -> PrometheusExporter {
95        PrometheusExporter {
96            reader: Arc::new(self.reader.build()),
97            prometheus_full_utf8: self.prometheus_full_utf8,
98        }
99    }
100}
101
102/// Returns a new [`PrometheusExporterBuilder`] to configure a [`PrometheusExporter`].
103pub fn exporter() -> PrometheusExporterBuilder {
104    PrometheusExporter::builder()
105}
106
107#[derive(Debug, Clone, Copy)]
108enum RawNumber {
109    U64(u64),
110    I64(i64),
111    F64(f64),
112}
113
114impl RawNumber {
115    fn as_f64(&self) -> f64 {
116        match *self {
117            RawNumber::U64(value) => value as f64,
118            RawNumber::I64(value) => value as f64,
119            RawNumber::F64(value) => value,
120        }
121    }
122}
123
124impl EncodeGaugeValue for RawNumber {
125    fn encode(&self, encoder: &mut prometheus_client::encoding::GaugeValueEncoder) -> Result<(), std::fmt::Error> {
126        match *self {
127            RawNumber::U64(value) => EncodeGaugeValue::encode(&(value as i64), encoder),
128            RawNumber::I64(value) => EncodeGaugeValue::encode(&value, encoder),
129            RawNumber::F64(value) => EncodeGaugeValue::encode(&value, encoder),
130        }
131    }
132}
133
134impl EncodeCounterValue for RawNumber {
135    fn encode(&self, encoder: &mut prometheus_client::encoding::CounterValueEncoder) -> Result<(), std::fmt::Error> {
136        match *self {
137            RawNumber::U64(value) => EncodeCounterValue::encode(&value, encoder),
138            RawNumber::I64(value) => EncodeCounterValue::encode(&(value as f64), encoder),
139            RawNumber::F64(value) => EncodeCounterValue::encode(&value, encoder),
140        }
141    }
142}
143
144macro_rules! impl_raw_number {
145    ($t:ty, $variant:ident) => {
146        impl From<$t> for RawNumber {
147            fn from(value: $t) -> Self {
148                RawNumber::$variant(value)
149            }
150        }
151    };
152}
153
154impl_raw_number!(u64, U64);
155impl_raw_number!(i64, I64);
156impl_raw_number!(f64, F64);
157
158enum KnownMetricT<'a, T> {
159    Gauge(&'a Gauge<T>),
160    Sum(&'a Sum<T>),
161    Histogram(&'a Histogram<T>),
162}
163
164impl<'a, T: 'static> KnownMetricT<'a, T>
165where
166    RawNumber: From<T>,
167    T: Copy,
168{
169    fn from_any(any: &'a dyn std::any::Any) -> Option<Self> {
170        if let Some(gauge) = any.downcast_ref::<Gauge<T>>() {
171            Some(KnownMetricT::Gauge(gauge))
172        } else if let Some(sum) = any.downcast_ref::<Sum<T>>() {
173            Some(KnownMetricT::Sum(sum))
174        } else {
175            any.downcast_ref::<Histogram<T>>()
176                .map(|histogram| KnownMetricT::Histogram(histogram))
177        }
178    }
179
180    fn metric_type(&self) -> MetricType {
181        match self {
182            KnownMetricT::Gauge(_) => MetricType::Gauge,
183            KnownMetricT::Sum(sum) => {
184                if sum.is_monotonic {
185                    MetricType::Counter
186                } else {
187                    MetricType::Gauge
188                }
189            }
190            KnownMetricT::Histogram(_) => MetricType::Histogram,
191        }
192    }
193
194    fn encode(
195        &self,
196        mut encoder: prometheus_client::encoding::MetricEncoder,
197        labels: KeyValueEncoder<'a>,
198    ) -> Result<(), std::fmt::Error> {
199        match self {
200            KnownMetricT::Gauge(gauge) => {
201                for data_point in &gauge.data_points {
202                    let number = RawNumber::from(data_point.value);
203                    encoder
204                        .encode_family(&labels.with_attrs(Some(&data_point.attributes)))?
205                        .encode_gauge(&number)?;
206                }
207            }
208            KnownMetricT::Sum(sum) => {
209                for data_point in &sum.data_points {
210                    let number = RawNumber::from(data_point.value);
211                    let attrs = labels.with_attrs(Some(&data_point.attributes));
212                    let mut encoder = encoder.encode_family(&attrs)?;
213
214                    if sum.is_monotonic {
215                        // TODO(troy): Exemplar support
216                        encoder.encode_counter::<NoLabelSet, _, f64>(&number, None)?;
217                    } else {
218                        encoder.encode_gauge(&number)?;
219                    }
220                }
221            }
222            KnownMetricT::Histogram(histogram) => {
223                for data_point in &histogram.data_points {
224                    let attrs = labels.with_attrs(Some(&data_point.attributes));
225                    let mut encoder = encoder.encode_family(&attrs)?;
226
227                    let sum = RawNumber::from(data_point.sum);
228
229                    let buckets = data_point
230                        .bounds
231                        .iter()
232                        .copied()
233                        .zip(data_point.bucket_counts.iter().copied())
234                        .collect::<Vec<_>>();
235
236                    encoder.encode_histogram::<NoLabelSet>(sum.as_f64(), data_point.count, &buckets, None)?;
237                }
238            }
239        }
240
241        Ok(())
242    }
243}
244
245enum KnownMetric<'a> {
246    U64(KnownMetricT<'a, u64>),
247    I64(KnownMetricT<'a, i64>),
248    F64(KnownMetricT<'a, f64>),
249}
250
251impl<'a> KnownMetric<'a> {
252    fn from_any(any: &'a dyn std::any::Any) -> Option<Self> {
253        macro_rules! try_decode {
254            ($t:ty, $variant:ident) => {
255                if let Some(metric) = KnownMetricT::<$t>::from_any(any) {
256                    return Some(KnownMetric::$variant(metric));
257                }
258            };
259        }
260
261        try_decode!(u64, U64);
262        try_decode!(i64, I64);
263        try_decode!(f64, F64);
264
265        None
266    }
267
268    fn metric_type(&self) -> MetricType {
269        match self {
270            KnownMetric::U64(metric) => metric.metric_type(),
271            KnownMetric::I64(metric) => metric.metric_type(),
272            KnownMetric::F64(metric) => metric.metric_type(),
273        }
274    }
275
276    fn encode(
277        &self,
278        encoder: prometheus_client::encoding::MetricEncoder,
279        labels: KeyValueEncoder<'a>,
280    ) -> Result<(), std::fmt::Error> {
281        match self {
282            KnownMetric::U64(metric) => metric.encode(encoder, labels),
283            KnownMetric::I64(metric) => metric.encode(encoder, labels),
284            KnownMetric::F64(metric) => metric.encode(encoder, labels),
285        }
286    }
287}
288
289impl prometheus_client::collector::Collector for PrometheusExporter {
290    fn encode(&self, mut encoder: prometheus_client::encoding::DescriptorEncoder) -> Result<(), std::fmt::Error> {
291        let mut metrics = ResourceMetrics {
292            resource: Resource::builder_empty().build(),
293            scope_metrics: vec![],
294        };
295
296        if let Err(err) = self.reader.collect(&mut metrics) {
297            otel_error!(name: "prometheus_collector_collect_error", error = err.to_string());
298            return Err(std::fmt::Error);
299        }
300
301        let labels = KeyValueEncoder::new(self.prometheus_full_utf8);
302
303        encoder
304            .encode_descriptor("target", "Information about the target", None, MetricType::Info)?
305            .encode_info(&labels.with_resource(Some(&metrics.resource)))?;
306
307        for scope_metrics in &metrics.scope_metrics {
308            for metric in &scope_metrics.metrics {
309                let Some(known_metric) = KnownMetric::from_any(metric.data.as_any()) else {
310                    otel_warn!(name: "prometheus_collector_unknown_metric_type", metric_name = metric.name.as_ref());
311                    continue;
312                };
313
314                let unit = if metric.unit.is_empty() {
315                    None
316                } else {
317                    Some(Unit::Other(metric.unit.to_string()))
318                };
319
320                known_metric.encode(
321                    encoder.encode_descriptor(
322                        &metric.name,
323                        &metric.description,
324                        unit.as_ref(),
325                        known_metric.metric_type(),
326                    )?,
327                    labels.with_scope(Some(&scope_metrics.scope)),
328                )?;
329            }
330        }
331
332        Ok(())
333    }
334}
335
336fn scope_to_iter(scope: &InstrumentationScope) -> impl Iterator<Item = (&str, Cow<'_, str>)> {
337    [
338        ("otel.scope.name", Some(Cow::Borrowed(scope.name()))),
339        ("otel.scope.version", scope.version().map(Cow::Borrowed)),
340        ("otel.scope.schema_url", scope.schema_url().map(Cow::Borrowed)),
341    ]
342    .into_iter()
343    .chain(scope.attributes().map(|kv| (kv.key.as_str(), Some(kv.value.as_str()))))
344    .filter_map(|(key, value)| value.map(|v| (key, v)))
345}
346
347#[derive(Debug, Clone, Copy)]
348struct KeyValueEncoder<'a> {
349    resource: Option<&'a Resource>,
350    scope: Option<&'a InstrumentationScope>,
351    attrs: Option<&'a [KeyValue]>,
352    prometheus_full_utf8: bool,
353}
354
355impl<'a> KeyValueEncoder<'a> {
356    fn new(prometheus_full_utf8: bool) -> Self {
357        Self {
358            resource: None,
359            scope: None,
360            attrs: None,
361            prometheus_full_utf8,
362        }
363    }
364
365    fn with_resource(self, resource: Option<&'a Resource>) -> Self {
366        Self { resource, ..self }
367    }
368
369    fn with_scope(self, scope: Option<&'a InstrumentationScope>) -> Self {
370        Self { scope, ..self }
371    }
372
373    fn with_attrs(self, attrs: Option<&'a [KeyValue]>) -> Self {
374        Self { attrs, ..self }
375    }
376}
377
378fn escape_key(s: &str) -> Cow<'_, str> {
379    // prefix chars to add in case name starts with number
380    let mut prefix = "";
381
382    // Find first invalid char
383    if let Some((replace_idx, _)) = s.char_indices().find(|(i, c)| {
384        if *i == 0 && c.is_ascii_digit() {
385            // first char is number, add prefix and replace reset of chars
386            prefix = "_";
387            true
388        } else {
389            // keep checking
390            !c.is_alphanumeric() && *c != '_' && *c != ':'
391        }
392    }) {
393        // up to `replace_idx` have been validated, convert the rest
394        let (valid, rest) = s.split_at(replace_idx);
395        Cow::Owned(
396            prefix
397                .chars()
398                .chain(valid.chars())
399                .chain(rest.chars().map(|c| {
400                    if c.is_ascii_alphanumeric() || c == '_' || c == ':' {
401                        c
402                    } else {
403                        '_'
404                    }
405                }))
406                .collect(),
407        )
408    } else {
409        Cow::Borrowed(s) // no invalid chars found, return existing
410    }
411}
412
413impl prometheus_client::encoding::EncodeLabelSet for KeyValueEncoder<'_> {
414    fn encode(&self, mut encoder: prometheus_client::encoding::LabelSetEncoder) -> Result<(), std::fmt::Error> {
415        use std::fmt::Write;
416
417        fn write_kv(
418            encoder: &mut prometheus_client::encoding::LabelSetEncoder,
419            key: &str,
420            value: &str,
421            prometheus_full_utf8: bool,
422        ) -> Result<(), std::fmt::Error> {
423            let mut label = encoder.encode_label();
424            let mut key_encoder = label.encode_label_key()?;
425            if prometheus_full_utf8 {
426                // TODO(troy): I am not sure if this is correct.
427                // See: https://github.com/prometheus/client_rust/issues/251
428                write!(&mut key_encoder, "{key}")?;
429            } else {
430                write!(&mut key_encoder, "{}", escape_key(key))?;
431            }
432
433            let mut value_encoder = key_encoder.encode_label_value()?;
434            write!(&mut value_encoder, "{value}")?;
435
436            value_encoder.finish()
437        }
438
439        if let Some(resource) = self.resource {
440            for (key, value) in resource.iter() {
441                write_kv(&mut encoder, key.as_str(), value.as_str().as_ref(), self.prometheus_full_utf8)?;
442            }
443        }
444
445        if let Some(scope) = self.scope {
446            for (key, value) in scope_to_iter(scope) {
447                write_kv(&mut encoder, key, value.as_ref(), self.prometheus_full_utf8)?;
448            }
449        }
450
451        if let Some(attrs) = self.attrs {
452            for kv in attrs {
453                write_kv(
454                    &mut encoder,
455                    kv.key.as_str(),
456                    kv.value.as_str().as_ref(),
457                    self.prometheus_full_utf8,
458                )?;
459            }
460        }
461
462        Ok(())
463    }
464}
465
466#[cfg(test)]
467#[cfg_attr(all(test, coverage_nightly), coverage(off))]
468mod tests {
469    use opentelemetry::KeyValue;
470    use opentelemetry::metrics::MeterProvider;
471    use opentelemetry_sdk::Resource;
472    use opentelemetry_sdk::metrics::SdkMeterProvider;
473    use prometheus_client::registry::Registry;
474
475    use super::*;
476
477    fn setup_prometheus_exporter(
478        temporality: opentelemetry_sdk::metrics::Temporality,
479        full_utf8: bool,
480    ) -> (PrometheusExporter, Registry) {
481        let exporter = PrometheusExporter::builder()
482            .with_temporality(temporality)
483            .with_prometheus_full_utf8(full_utf8)
484            .build();
485        let mut registry = Registry::default();
486        registry.register_collector(exporter.collector());
487        (exporter, registry)
488    }
489
490    fn collect_and_encode(registry: &Registry) -> String {
491        let mut buffer = String::new();
492        prometheus_client::encoding::text::encode(&mut buffer, registry).unwrap();
493        buffer
494    }
495
496    #[test]
497    fn test_prometheus_collect() {
498        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
499        let provider = SdkMeterProvider::builder()
500            .with_reader(exporter.clone())
501            .with_resource(
502                Resource::builder()
503                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
504                    .build(),
505            )
506            .build();
507        opentelemetry::global::set_meter_provider(provider.clone());
508
509        let meter = provider.meter("test_meter");
510        let counter = meter.u64_counter("test_counter").build();
511        counter.add(1, &[KeyValue::new("key", "value")]);
512
513        let encoded = collect_and_encode(&registry);
514
515        assert!(encoded.contains("test_counter"));
516        assert!(encoded.contains(r#"key="value""#));
517        assert!(encoded.contains(r#"test_counter_total{otel_scope_name="test_meter",key="value"} 1"#));
518    }
519
520    #[test]
521    fn test_prometheus_temporality() {
522        let exporter = PrometheusExporter::builder()
523            .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
524            .build();
525
526        let temporality = exporter.temporality(opentelemetry_sdk::metrics::InstrumentKind::Counter);
527
528        assert_eq!(temporality, opentelemetry_sdk::metrics::Temporality::Delta);
529    }
530
531    #[test]
532    fn test_prometheus_full_utf8() {
533        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, true);
534        let provider = SdkMeterProvider::builder()
535            .with_reader(exporter.clone())
536            .with_resource(
537                Resource::builder()
538                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
539                    .build(),
540            )
541            .build();
542        opentelemetry::global::set_meter_provider(provider.clone());
543
544        let meter = provider.meter("test_meter");
545        let counter = meter.u64_counter("test_counter").build();
546        counter.add(1, &[KeyValue::new("key_😊", "value_😊")]);
547
548        let encoded = collect_and_encode(&registry);
549
550        assert!(encoded.contains(r#"key_😊="value_😊""#));
551    }
552
553    #[test]
554    fn test_raw_number_as_f64() {
555        assert_eq!(RawNumber::U64(42).as_f64(), 42.0);
556        assert_eq!(RawNumber::I64(-42).as_f64(), -42.0);
557        assert_eq!(RawNumber::F64(5.44).as_f64(), 5.44);
558    }
559
560    #[test]
561    fn test_known_metric_t_from_any() {
562        let time = std::time::SystemTime::now();
563        let gauge = Gauge::<u64> {
564            data_points: vec![],
565            start_time: Some(time - std::time::Duration::from_secs(10)),
566            time,
567        };
568        let sum = Sum::<u64> {
569            data_points: vec![],
570            is_monotonic: true,
571            start_time: time - std::time::Duration::from_secs(10),
572            time,
573            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
574        };
575        let histogram = Histogram::<u64> {
576            data_points: vec![],
577            start_time: time - std::time::Duration::from_secs(10),
578            time,
579            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
580        };
581
582        assert!(matches!(KnownMetricT::<u64>::from_any(&gauge), Some(KnownMetricT::Gauge(_))));
583        assert!(matches!(KnownMetricT::<u64>::from_any(&sum), Some(KnownMetricT::Sum(_))));
584        assert!(matches!(
585            KnownMetricT::<u64>::from_any(&histogram),
586            Some(KnownMetricT::Histogram(_))
587        ));
588    }
589
590    #[test]
591    fn test_known_metric_t_metric_type() {
592        let time = std::time::SystemTime::now();
593        let gauge = Gauge::<u64> {
594            data_points: vec![],
595            start_time: Some(time - std::time::Duration::from_secs(10)),
596            time,
597        };
598        let gauge = KnownMetricT::Gauge(&gauge);
599        matches!(gauge.metric_type(), MetricType::Gauge);
600
601        let sum = Sum::<u64> {
602            data_points: vec![],
603            is_monotonic: true,
604            start_time: time - std::time::Duration::from_secs(10),
605            time,
606            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
607        };
608        let sum_monotonic = KnownMetricT::Sum(&sum);
609        matches!(sum_monotonic.metric_type(), MetricType::Counter);
610
611        let sum = Sum::<u64> {
612            data_points: vec![],
613            is_monotonic: false,
614            start_time: time - std::time::Duration::from_secs(10),
615            time,
616            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
617        };
618        let sum_non_monotonic = KnownMetricT::Sum(&sum);
619        matches!(sum_non_monotonic.metric_type(), MetricType::Gauge);
620
621        let histogram = Histogram::<u64> {
622            data_points: vec![],
623            start_time: time - std::time::Duration::from_secs(10),
624            time,
625            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
626        };
627        let histogram = KnownMetricT::Histogram(&histogram);
628        matches!(histogram.metric_type(), MetricType::Histogram);
629    }
630
631    #[test]
632    fn test_known_metric_t_encode() {
633        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
634        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
635        let meter = provider.meter("test_meter");
636
637        let gauge_u64 = meter.u64_gauge("test_u64_gauge").build();
638        gauge_u64.record(42, &[KeyValue::new("key", "value")]);
639
640        let encoded = collect_and_encode(&registry);
641        assert!(encoded.contains(r#"test_u64_gauge{otel_scope_name="test_meter",key="value"} 42"#));
642
643        let counter_i64_sum = meter.i64_up_down_counter("test_i64_counter").build();
644        counter_i64_sum.add(-42, &[KeyValue::new("key", "value")]);
645
646        let encoded = collect_and_encode(&registry);
647        assert!(encoded.contains(r#"test_i64_counter{otel_scope_name="test_meter",key="value"} -42"#));
648    }
649
650    #[test]
651    fn test_known_metric_from_any() {
652        let time = std::time::SystemTime::now();
653        let gauge_u64 = Gauge::<u64> {
654            data_points: vec![],
655            start_time: Some(time),
656            time,
657        };
658        let sum_i64 = Sum::<i64> {
659            data_points: vec![],
660            is_monotonic: true,
661            start_time: time,
662            time,
663            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
664        };
665        let histogram_f64 = Histogram::<f64> {
666            data_points: vec![],
667            start_time: time,
668            time,
669            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
670        };
671
672        assert!(matches!(
673            KnownMetric::from_any(&gauge_u64),
674            Some(KnownMetric::U64(KnownMetricT::Gauge(_)))
675        ));
676        assert!(matches!(
677            KnownMetric::from_any(&sum_i64),
678            Some(KnownMetric::I64(KnownMetricT::Sum(_)))
679        ));
680        assert!(matches!(
681            KnownMetric::from_any(&histogram_f64),
682            Some(KnownMetric::F64(KnownMetricT::Histogram(_)))
683        ));
684        assert!(KnownMetric::from_any(&true).is_none());
685    }
686
687    #[test]
688    fn test_known_metric_metric_type() {
689        let time = std::time::SystemTime::now();
690        let gauge = Gauge::<u64> {
691            data_points: vec![],
692            start_time: Some(time),
693            time,
694        };
695        let metric = KnownMetric::U64(KnownMetricT::Gauge(&gauge));
696        assert!(matches!(metric.metric_type(), MetricType::Gauge));
697
698        let sum_mono = Sum::<i64> {
699            data_points: vec![],
700            is_monotonic: true,
701            start_time: time,
702            time,
703            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
704        };
705        let metric = KnownMetric::I64(KnownMetricT::Sum(&sum_mono));
706        assert!(matches!(metric.metric_type(), MetricType::Counter));
707
708        let sum_non_mono = Sum::<f64> {
709            data_points: vec![],
710            is_monotonic: false,
711            start_time: time,
712            time,
713            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
714        };
715        let metric = KnownMetric::F64(KnownMetricT::Sum(&sum_non_mono));
716        assert!(matches!(metric.metric_type(), MetricType::Gauge));
717    }
718
719    #[test]
720    fn test_known_metric_encode() {
721        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
722        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
723        let meter = provider.meter("test_meter");
724
725        meter
726            .f64_counter("test_f64_counter")
727            .build()
728            .add(1.0, &[KeyValue::new("key", "value")]);
729        assert!(
730            collect_and_encode(&registry).contains(r#"test_f64_counter_total{otel_scope_name="test_meter",key="value"} 1"#)
731        );
732        meter
733            .u64_counter("test_u64_counter")
734            .build()
735            .add(1, &[KeyValue::new("key", "value")]);
736        assert!(
737            collect_and_encode(&registry).contains(r#"test_u64_counter_total{otel_scope_name="test_meter",key="value"} 1"#)
738        );
739        meter
740            .f64_up_down_counter("test_f64_up_down_counter")
741            .build()
742            .add(1.0, &[KeyValue::new("key", "value")]);
743        assert!(
744            collect_and_encode(&registry)
745                .contains(r#"test_f64_up_down_counter{otel_scope_name="test_meter",key="value"} 1"#)
746        );
747        meter
748            .i64_up_down_counter("test_i64_up_down_counter")
749            .build()
750            .add(-1, &[KeyValue::new("key", "value")]);
751        assert!(
752            collect_and_encode(&registry)
753                .contains(r#"test_i64_up_down_counter{otel_scope_name="test_meter",key="value"} -1"#)
754        );
755
756        meter
757            .f64_gauge("test_f64_gauge")
758            .build()
759            .record(1.0, &[KeyValue::new("key", "value")]);
760        assert!(collect_and_encode(&registry).contains(r#"test_f64_gauge{otel_scope_name="test_meter",key="value"} 1"#));
761        meter
762            .i64_gauge("test_i64_gauge")
763            .build()
764            .record(-1, &[KeyValue::new("key", "value")]);
765        assert!(collect_and_encode(&registry).contains(r#"test_i64_gauge{otel_scope_name="test_meter",key="value"} -1"#));
766        meter
767            .u64_gauge("test_u64_gauge")
768            .build()
769            .record(1, &[KeyValue::new("key", "value")]);
770        assert!(collect_and_encode(&registry).contains(r#"test_u64_gauge{otel_scope_name="test_meter",key="value"} 1"#));
771
772        meter
773            .f64_histogram("test_f64_histogram")
774            .build()
775            .record(1.0, &[KeyValue::new("key", "value")]);
776        assert!(
777            collect_and_encode(&registry).contains(r#"test_f64_histogram_sum{otel_scope_name="test_meter",key="value"} 1"#)
778        );
779        meter
780            .u64_histogram("test_u64_histogram")
781            .build()
782            .record(1, &[KeyValue::new("key", "value")]);
783        assert!(
784            collect_and_encode(&registry).contains(r#"test_u64_histogram_sum{otel_scope_name="test_meter",key="value"} 1"#)
785        );
786    }
787
788    #[test]
789    fn test_prometheus_collect_histogram() {
790        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
791        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
792        let meter = provider.meter("test_meter");
793        let histogram = meter
794            .u64_histogram("test_histogram")
795            .with_boundaries(vec![5.0, 10.0, 20.0])
796            .build();
797        histogram.record(3, &[KeyValue::new("key", "value")]);
798        histogram.record(7, &[KeyValue::new("key", "value")]);
799        histogram.record(12, &[KeyValue::new("key", "value")]);
800        histogram.record(25, &[KeyValue::new("key", "value")]);
801
802        let mut metrics = ResourceMetrics {
803            scope_metrics: vec![],
804            resource: Resource::builder_empty().build(),
805        };
806        exporter.collect(&mut metrics).unwrap();
807
808        let scope_metrics = metrics.scope_metrics.first().expect("scope metrics should be present");
809        let metric = scope_metrics
810            .metrics
811            .iter()
812            .find(|m| m.name == "test_histogram")
813            .expect("histogram metric should be present");
814        let histogram_data = metric
815            .data
816            .as_any()
817            .downcast_ref::<Histogram<u64>>()
818            .expect("metric data should be a histogram");
819
820        let data_point = histogram_data.data_points.first().expect("data point should be present");
821        assert_eq!(data_point.sum, 47, "sum should be 3 + 7 + 12 + 25 = 47");
822        assert_eq!(data_point.count, 4, "count should be 4");
823        assert_eq!(
824            data_point.bucket_counts,
825            vec![1, 1, 1, 1],
826            "each value should fall into a separate bucket"
827        );
828        assert_eq!(
829            data_point.bounds,
830            vec![5.0, 10.0, 20.0],
831            "boundaries should match the defined ones"
832        );
833
834        let encoded = collect_and_encode(&registry);
835        assert!(encoded.contains(r#"test_histogram_sum{otel_scope_name="test_meter",key="value"} 47"#));
836    }
837
838    #[test]
839    fn test_non_monotonic_sum_as_gauge() {
840        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
841        let provider = SdkMeterProvider::builder()
842            .with_reader(exporter.clone())
843            .with_resource(
844                Resource::builder()
845                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
846                    .build(),
847            )
848            .build();
849        opentelemetry::global::set_meter_provider(provider.clone());
850
851        let meter = provider.meter("test_meter");
852        let sum_metric = meter.i64_up_down_counter("test_non_monotonic_sum").build();
853        sum_metric.add(10, &[KeyValue::new("key", "value")]);
854        sum_metric.add(-5, &[KeyValue::new("key", "value")]);
855
856        let encoded = collect_and_encode(&registry);
857
858        assert!(encoded.contains(r#"test_non_monotonic_sum{otel_scope_name="test_meter",key="value"} 5"#));
859        assert!(
860            !encoded.contains("test_non_monotonic_sum_total"),
861            "Non-monotonic sum should not have '_total' suffix"
862        );
863    }
864
865    #[test]
866    fn test_escape_key() {
867        assert_eq!(escape_key("valid_key"), "valid_key");
868        assert_eq!(escape_key("123start"), "_123start");
869        assert_eq!(escape_key("key with spaces"), "key_with_spaces");
870        assert_eq!(escape_key("key_with:dots"), "key_with:dots");
871        assert_eq!(escape_key("!@#$%"), "_____");
872    }
873}