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#[derive(Debug, Clone)]
27pub struct PrometheusExporter {
28 reader: Arc<ManualReader>,
29 prometheus_full_utf8: bool,
30}
31
32impl PrometheusExporter {
33 pub fn builder() -> PrometheusExporterBuilder {
35 PrometheusExporterBuilder::default()
36 }
37
38 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#[derive(Default)]
72pub struct PrometheusExporterBuilder {
73 reader: ManualReaderBuilder,
74 prometheus_full_utf8: bool,
75}
76
77impl PrometheusExporterBuilder {
78 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 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 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
102pub 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 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 let mut prefix = "";
394
395 if let Some((replace_idx, _)) = s.char_indices().find(|(i, c)| {
397 if *i == 0 && c.is_ascii_digit() {
398 prefix = "_";
400 true
401 } else {
402 !c.is_alphanumeric() && *c != '_' && *c != ':'
404 }
405 }) {
406 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) }
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 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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry).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(®istry).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(®istry)
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(®istry)
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(®istry).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(®istry).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(®istry).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(®istry).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(®istry).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(®istry);
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(®istry);
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}