scuffle_metrics/prometheus/
mod.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use opentelemetry::{InstrumentationScope, KeyValue};
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            #[cfg(feature = "tracing")]
298            tracing::error!(
299                name = "prometheus_collector_collect_error",
300                target = env!("CARGO_PKG_NAME"),
301                error = err.to_string(),
302                ""
303            );
304            let _ = err;
305            return Err(std::fmt::Error);
306        }
307
308        let labels = KeyValueEncoder::new(self.prometheus_full_utf8);
309
310        encoder
311            .encode_descriptor("target", "Information about the target", None, MetricType::Info)?
312            .encode_info(&labels.with_resource(Some(&metrics.resource)))?;
313
314        for scope_metrics in &metrics.scope_metrics {
315            for metric in &scope_metrics.metrics {
316                let Some(known_metric) = KnownMetric::from_any(metric.data.as_any()) else {
317                    #[cfg(feature = "tracing")]
318                    tracing::warn!(
319                        name = "prometheus_collector_unknown_metric_type",
320                        target = env!("CARGO_PKG_NAME"),
321                        metric_name = metric.name.as_ref(),
322                        ""
323                    );
324                    continue;
325                };
326
327                let unit = if metric.unit.is_empty() {
328                    None
329                } else {
330                    Some(Unit::Other(metric.unit.to_string()))
331                };
332
333                known_metric.encode(
334                    encoder.encode_descriptor(
335                        &metric.name,
336                        &metric.description,
337                        unit.as_ref(),
338                        known_metric.metric_type(),
339                    )?,
340                    labels.with_scope(Some(&scope_metrics.scope)),
341                )?;
342            }
343        }
344
345        Ok(())
346    }
347}
348
349fn scope_to_iter(scope: &InstrumentationScope) -> impl Iterator<Item = (&str, Cow<'_, str>)> {
350    [
351        ("otel.scope.name", Some(Cow::Borrowed(scope.name()))),
352        ("otel.scope.version", scope.version().map(Cow::Borrowed)),
353        ("otel.scope.schema_url", scope.schema_url().map(Cow::Borrowed)),
354    ]
355    .into_iter()
356    .chain(scope.attributes().map(|kv| (kv.key.as_str(), Some(kv.value.as_str()))))
357    .filter_map(|(key, value)| value.map(|v| (key, v)))
358}
359
360#[derive(Debug, Clone, Copy)]
361struct KeyValueEncoder<'a> {
362    resource: Option<&'a Resource>,
363    scope: Option<&'a InstrumentationScope>,
364    attrs: Option<&'a [KeyValue]>,
365    prometheus_full_utf8: bool,
366}
367
368impl<'a> KeyValueEncoder<'a> {
369    fn new(prometheus_full_utf8: bool) -> Self {
370        Self {
371            resource: None,
372            scope: None,
373            attrs: None,
374            prometheus_full_utf8,
375        }
376    }
377
378    fn with_resource(self, resource: Option<&'a Resource>) -> Self {
379        Self { resource, ..self }
380    }
381
382    fn with_scope(self, scope: Option<&'a InstrumentationScope>) -> Self {
383        Self { scope, ..self }
384    }
385
386    fn with_attrs(self, attrs: Option<&'a [KeyValue]>) -> Self {
387        Self { attrs, ..self }
388    }
389}
390
391fn escape_key(s: &str) -> Cow<'_, str> {
392    // prefix chars to add in case name starts with number
393    let mut prefix = "";
394
395    // Find first invalid char
396    if let Some((replace_idx, _)) = s.char_indices().find(|(i, c)| {
397        if *i == 0 && c.is_ascii_digit() {
398            // first char is number, add prefix and replace reset of chars
399            prefix = "_";
400            true
401        } else {
402            // keep checking
403            !c.is_alphanumeric() && *c != '_' && *c != ':'
404        }
405    }) {
406        // up to `replace_idx` have been validated, convert the rest
407        let (valid, rest) = s.split_at(replace_idx);
408        Cow::Owned(
409            prefix
410                .chars()
411                .chain(valid.chars())
412                .chain(rest.chars().map(|c| {
413                    if c.is_ascii_alphanumeric() || c == '_' || c == ':' {
414                        c
415                    } else {
416                        '_'
417                    }
418                }))
419                .collect(),
420        )
421    } else {
422        Cow::Borrowed(s) // no invalid chars found, return existing
423    }
424}
425
426impl prometheus_client::encoding::EncodeLabelSet for KeyValueEncoder<'_> {
427    fn encode(&self, mut encoder: prometheus_client::encoding::LabelSetEncoder) -> Result<(), std::fmt::Error> {
428        use std::fmt::Write;
429
430        fn write_kv(
431            encoder: &mut prometheus_client::encoding::LabelSetEncoder,
432            key: &str,
433            value: &str,
434            prometheus_full_utf8: bool,
435        ) -> Result<(), std::fmt::Error> {
436            let mut label = encoder.encode_label();
437            let mut key_encoder = label.encode_label_key()?;
438            if prometheus_full_utf8 {
439                // TODO(troy): I am not sure if this is correct.
440                // See: https://github.com/prometheus/client_rust/issues/251
441                write!(&mut key_encoder, "{key}")?;
442            } else {
443                write!(&mut key_encoder, "{}", escape_key(key))?;
444            }
445
446            let mut value_encoder = key_encoder.encode_label_value()?;
447            write!(&mut value_encoder, "{value}")?;
448
449            value_encoder.finish()
450        }
451
452        if let Some(resource) = self.resource {
453            for (key, value) in resource.iter() {
454                write_kv(&mut encoder, key.as_str(), value.as_str().as_ref(), self.prometheus_full_utf8)?;
455            }
456        }
457
458        if let Some(scope) = self.scope {
459            for (key, value) in scope_to_iter(scope) {
460                write_kv(&mut encoder, key, value.as_ref(), self.prometheus_full_utf8)?;
461            }
462        }
463
464        if let Some(attrs) = self.attrs {
465            for kv in attrs {
466                write_kv(
467                    &mut encoder,
468                    kv.key.as_str(),
469                    kv.value.as_str().as_ref(),
470                    self.prometheus_full_utf8,
471                )?;
472            }
473        }
474
475        Ok(())
476    }
477}
478
479#[cfg(test)]
480#[cfg_attr(all(test, coverage_nightly), coverage(off))]
481mod tests {
482    use opentelemetry::KeyValue;
483    use opentelemetry::metrics::MeterProvider;
484    use opentelemetry_sdk::Resource;
485    use opentelemetry_sdk::metrics::SdkMeterProvider;
486    use prometheus_client::registry::Registry;
487
488    use super::*;
489
490    fn setup_prometheus_exporter(
491        temporality: opentelemetry_sdk::metrics::Temporality,
492        full_utf8: bool,
493    ) -> (PrometheusExporter, Registry) {
494        let exporter = PrometheusExporter::builder()
495            .with_temporality(temporality)
496            .with_prometheus_full_utf8(full_utf8)
497            .build();
498        let mut registry = Registry::default();
499        registry.register_collector(exporter.collector());
500        (exporter, registry)
501    }
502
503    fn collect_and_encode(registry: &Registry) -> String {
504        let mut buffer = String::new();
505        prometheus_client::encoding::text::encode(&mut buffer, registry).unwrap();
506        buffer
507    }
508
509    #[test]
510    fn test_prometheus_collect() {
511        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
512        let provider = SdkMeterProvider::builder()
513            .with_reader(exporter.clone())
514            .with_resource(
515                Resource::builder()
516                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
517                    .build(),
518            )
519            .build();
520        opentelemetry::global::set_meter_provider(provider.clone());
521
522        let meter = provider.meter("test_meter");
523        let counter = meter.u64_counter("test_counter").build();
524        counter.add(1, &[KeyValue::new("key", "value")]);
525
526        let encoded = collect_and_encode(&registry);
527
528        assert!(encoded.contains("test_counter"));
529        assert!(encoded.contains(r#"key="value""#));
530        assert!(encoded.contains(r#"test_counter_total{otel_scope_name="test_meter",key="value"} 1"#));
531    }
532
533    #[test]
534    fn test_prometheus_temporality() {
535        let exporter = PrometheusExporter::builder()
536            .with_temporality(opentelemetry_sdk::metrics::Temporality::Delta)
537            .build();
538
539        let temporality = exporter.temporality(opentelemetry_sdk::metrics::InstrumentKind::Counter);
540
541        assert_eq!(temporality, opentelemetry_sdk::metrics::Temporality::Delta);
542    }
543
544    #[test]
545    fn test_prometheus_full_utf8() {
546        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, true);
547        let provider = SdkMeterProvider::builder()
548            .with_reader(exporter.clone())
549            .with_resource(
550                Resource::builder()
551                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
552                    .build(),
553            )
554            .build();
555        opentelemetry::global::set_meter_provider(provider.clone());
556
557        let meter = provider.meter("test_meter");
558        let counter = meter.u64_counter("test_counter").build();
559        counter.add(1, &[KeyValue::new("key_😊", "value_😊")]);
560
561        let encoded = collect_and_encode(&registry);
562
563        assert!(encoded.contains(r#"key_😊="value_😊""#));
564    }
565
566    #[test]
567    fn test_raw_number_as_f64() {
568        assert_eq!(RawNumber::U64(42).as_f64(), 42.0);
569        assert_eq!(RawNumber::I64(-42).as_f64(), -42.0);
570        assert_eq!(RawNumber::F64(5.44).as_f64(), 5.44);
571    }
572
573    #[test]
574    fn test_known_metric_t_from_any() {
575        let time = std::time::SystemTime::now();
576        let gauge = Gauge::<u64> {
577            data_points: vec![],
578            start_time: Some(time - std::time::Duration::from_secs(10)),
579            time,
580        };
581        let sum = Sum::<u64> {
582            data_points: vec![],
583            is_monotonic: true,
584            start_time: time - std::time::Duration::from_secs(10),
585            time,
586            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
587        };
588        let histogram = Histogram::<u64> {
589            data_points: vec![],
590            start_time: time - std::time::Duration::from_secs(10),
591            time,
592            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
593        };
594
595        assert!(matches!(KnownMetricT::<u64>::from_any(&gauge), Some(KnownMetricT::Gauge(_))));
596        assert!(matches!(KnownMetricT::<u64>::from_any(&sum), Some(KnownMetricT::Sum(_))));
597        assert!(matches!(
598            KnownMetricT::<u64>::from_any(&histogram),
599            Some(KnownMetricT::Histogram(_))
600        ));
601    }
602
603    #[test]
604    fn test_known_metric_t_metric_type() {
605        let time = std::time::SystemTime::now();
606        let gauge = Gauge::<u64> {
607            data_points: vec![],
608            start_time: Some(time - std::time::Duration::from_secs(10)),
609            time,
610        };
611        let gauge = KnownMetricT::Gauge(&gauge);
612        matches!(gauge.metric_type(), MetricType::Gauge);
613
614        let sum = Sum::<u64> {
615            data_points: vec![],
616            is_monotonic: true,
617            start_time: time - std::time::Duration::from_secs(10),
618            time,
619            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
620        };
621        let sum_monotonic = KnownMetricT::Sum(&sum);
622        matches!(sum_monotonic.metric_type(), MetricType::Counter);
623
624        let sum = Sum::<u64> {
625            data_points: vec![],
626            is_monotonic: false,
627            start_time: time - std::time::Duration::from_secs(10),
628            time,
629            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
630        };
631        let sum_non_monotonic = KnownMetricT::Sum(&sum);
632        matches!(sum_non_monotonic.metric_type(), MetricType::Gauge);
633
634        let histogram = Histogram::<u64> {
635            data_points: vec![],
636            start_time: time - std::time::Duration::from_secs(10),
637            time,
638            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
639        };
640        let histogram = KnownMetricT::Histogram(&histogram);
641        matches!(histogram.metric_type(), MetricType::Histogram);
642    }
643
644    #[test]
645    fn test_known_metric_t_encode() {
646        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
647        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
648        let meter = provider.meter("test_meter");
649
650        let gauge_u64 = meter.u64_gauge("test_u64_gauge").build();
651        gauge_u64.record(42, &[KeyValue::new("key", "value")]);
652
653        let encoded = collect_and_encode(&registry);
654        assert!(encoded.contains(r#"test_u64_gauge{otel_scope_name="test_meter",key="value"} 42"#));
655
656        let counter_i64_sum = meter.i64_up_down_counter("test_i64_counter").build();
657        counter_i64_sum.add(-42, &[KeyValue::new("key", "value")]);
658
659        let encoded = collect_and_encode(&registry);
660        assert!(encoded.contains(r#"test_i64_counter{otel_scope_name="test_meter",key="value"} -42"#));
661    }
662
663    #[test]
664    fn test_known_metric_from_any() {
665        let time = std::time::SystemTime::now();
666        let gauge_u64 = Gauge::<u64> {
667            data_points: vec![],
668            start_time: Some(time),
669            time,
670        };
671        let sum_i64 = Sum::<i64> {
672            data_points: vec![],
673            is_monotonic: true,
674            start_time: time,
675            time,
676            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
677        };
678        let histogram_f64 = Histogram::<f64> {
679            data_points: vec![],
680            start_time: time,
681            time,
682            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
683        };
684
685        assert!(matches!(
686            KnownMetric::from_any(&gauge_u64),
687            Some(KnownMetric::U64(KnownMetricT::Gauge(_)))
688        ));
689        assert!(matches!(
690            KnownMetric::from_any(&sum_i64),
691            Some(KnownMetric::I64(KnownMetricT::Sum(_)))
692        ));
693        assert!(matches!(
694            KnownMetric::from_any(&histogram_f64),
695            Some(KnownMetric::F64(KnownMetricT::Histogram(_)))
696        ));
697        assert!(KnownMetric::from_any(&true).is_none());
698    }
699
700    #[test]
701    fn test_known_metric_metric_type() {
702        let time = std::time::SystemTime::now();
703        let gauge = Gauge::<u64> {
704            data_points: vec![],
705            start_time: Some(time),
706            time,
707        };
708        let metric = KnownMetric::U64(KnownMetricT::Gauge(&gauge));
709        assert!(matches!(metric.metric_type(), MetricType::Gauge));
710
711        let sum_mono = Sum::<i64> {
712            data_points: vec![],
713            is_monotonic: true,
714            start_time: time,
715            time,
716            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
717        };
718        let metric = KnownMetric::I64(KnownMetricT::Sum(&sum_mono));
719        assert!(matches!(metric.metric_type(), MetricType::Counter));
720
721        let sum_non_mono = Sum::<f64> {
722            data_points: vec![],
723            is_monotonic: false,
724            start_time: time,
725            time,
726            temporality: opentelemetry_sdk::metrics::Temporality::Cumulative,
727        };
728        let metric = KnownMetric::F64(KnownMetricT::Sum(&sum_non_mono));
729        assert!(matches!(metric.metric_type(), MetricType::Gauge));
730    }
731
732    #[test]
733    fn test_known_metric_encode() {
734        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
735        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
736        let meter = provider.meter("test_meter");
737
738        meter
739            .f64_counter("test_f64_counter")
740            .build()
741            .add(1.0, &[KeyValue::new("key", "value")]);
742        assert!(
743            collect_and_encode(&registry).contains(r#"test_f64_counter_total{otel_scope_name="test_meter",key="value"} 1"#)
744        );
745        meter
746            .u64_counter("test_u64_counter")
747            .build()
748            .add(1, &[KeyValue::new("key", "value")]);
749        assert!(
750            collect_and_encode(&registry).contains(r#"test_u64_counter_total{otel_scope_name="test_meter",key="value"} 1"#)
751        );
752        meter
753            .f64_up_down_counter("test_f64_up_down_counter")
754            .build()
755            .add(1.0, &[KeyValue::new("key", "value")]);
756        assert!(
757            collect_and_encode(&registry)
758                .contains(r#"test_f64_up_down_counter{otel_scope_name="test_meter",key="value"} 1"#)
759        );
760        meter
761            .i64_up_down_counter("test_i64_up_down_counter")
762            .build()
763            .add(-1, &[KeyValue::new("key", "value")]);
764        assert!(
765            collect_and_encode(&registry)
766                .contains(r#"test_i64_up_down_counter{otel_scope_name="test_meter",key="value"} -1"#)
767        );
768
769        meter
770            .f64_gauge("test_f64_gauge")
771            .build()
772            .record(1.0, &[KeyValue::new("key", "value")]);
773        assert!(collect_and_encode(&registry).contains(r#"test_f64_gauge{otel_scope_name="test_meter",key="value"} 1"#));
774        meter
775            .i64_gauge("test_i64_gauge")
776            .build()
777            .record(-1, &[KeyValue::new("key", "value")]);
778        assert!(collect_and_encode(&registry).contains(r#"test_i64_gauge{otel_scope_name="test_meter",key="value"} -1"#));
779        meter
780            .u64_gauge("test_u64_gauge")
781            .build()
782            .record(1, &[KeyValue::new("key", "value")]);
783        assert!(collect_and_encode(&registry).contains(r#"test_u64_gauge{otel_scope_name="test_meter",key="value"} 1"#));
784
785        meter
786            .f64_histogram("test_f64_histogram")
787            .build()
788            .record(1.0, &[KeyValue::new("key", "value")]);
789        assert!(
790            collect_and_encode(&registry).contains(r#"test_f64_histogram_sum{otel_scope_name="test_meter",key="value"} 1"#)
791        );
792        meter
793            .u64_histogram("test_u64_histogram")
794            .build()
795            .record(1, &[KeyValue::new("key", "value")]);
796        assert!(
797            collect_and_encode(&registry).contains(r#"test_u64_histogram_sum{otel_scope_name="test_meter",key="value"} 1"#)
798        );
799    }
800
801    #[test]
802    fn test_prometheus_collect_histogram() {
803        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
804        let provider = SdkMeterProvider::builder().with_reader(exporter.clone()).build();
805        let meter = provider.meter("test_meter");
806        let histogram = meter
807            .u64_histogram("test_histogram")
808            .with_boundaries(vec![5.0, 10.0, 20.0])
809            .build();
810        histogram.record(3, &[KeyValue::new("key", "value")]);
811        histogram.record(7, &[KeyValue::new("key", "value")]);
812        histogram.record(12, &[KeyValue::new("key", "value")]);
813        histogram.record(25, &[KeyValue::new("key", "value")]);
814
815        let mut metrics = ResourceMetrics {
816            scope_metrics: vec![],
817            resource: Resource::builder_empty().build(),
818        };
819        exporter.collect(&mut metrics).unwrap();
820
821        let scope_metrics = metrics.scope_metrics.first().expect("scope metrics should be present");
822        let metric = scope_metrics
823            .metrics
824            .iter()
825            .find(|m| m.name == "test_histogram")
826            .expect("histogram metric should be present");
827        let histogram_data = metric
828            .data
829            .as_any()
830            .downcast_ref::<Histogram<u64>>()
831            .expect("metric data should be a histogram");
832
833        let data_point = histogram_data.data_points.first().expect("data point should be present");
834        assert_eq!(data_point.sum, 47, "sum should be 3 + 7 + 12 + 25 = 47");
835        assert_eq!(data_point.count, 4, "count should be 4");
836        assert_eq!(
837            data_point.bucket_counts,
838            vec![1, 1, 1, 1],
839            "each value should fall into a separate bucket"
840        );
841        assert_eq!(
842            data_point.bounds,
843            vec![5.0, 10.0, 20.0],
844            "boundaries should match the defined ones"
845        );
846
847        let encoded = collect_and_encode(&registry);
848        assert!(encoded.contains(r#"test_histogram_sum{otel_scope_name="test_meter",key="value"} 47"#));
849    }
850
851    #[test]
852    fn test_non_monotonic_sum_as_gauge() {
853        let (exporter, registry) = setup_prometheus_exporter(opentelemetry_sdk::metrics::Temporality::Cumulative, false);
854        let provider = SdkMeterProvider::builder()
855            .with_reader(exporter.clone())
856            .with_resource(
857                Resource::builder()
858                    .with_attributes(vec![KeyValue::new("service.name", "test_service")])
859                    .build(),
860            )
861            .build();
862        opentelemetry::global::set_meter_provider(provider.clone());
863
864        let meter = provider.meter("test_meter");
865        let sum_metric = meter.i64_up_down_counter("test_non_monotonic_sum").build();
866        sum_metric.add(10, &[KeyValue::new("key", "value")]);
867        sum_metric.add(-5, &[KeyValue::new("key", "value")]);
868
869        let encoded = collect_and_encode(&registry);
870
871        assert!(encoded.contains(r#"test_non_monotonic_sum{otel_scope_name="test_meter",key="value"} 5"#));
872        assert!(
873            !encoded.contains("test_non_monotonic_sum_total"),
874            "Non-monotonic sum should not have '_total' suffix"
875        );
876    }
877
878    #[test]
879    fn test_escape_key() {
880        assert_eq!(escape_key("valid_key"), "valid_key");
881        assert_eq!(escape_key("123start"), "_123start");
882        assert_eq!(escape_key("key with spaces"), "key_with_spaces");
883        assert_eq!(escape_key("key_with:dots"), "key_with:dots");
884        assert_eq!(escape_key("!@#$%"), "_____");
885    }
886}