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#[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 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 let mut prefix = "";
381
382 if let Some((replace_idx, _)) = s.char_indices().find(|(i, c)| {
384 if *i == 0 && c.is_ascii_digit() {
385 prefix = "_";
387 true
388 } else {
389 !c.is_alphanumeric() && *c != '_' && *c != ':'
391 }
392 }) {
393 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) }
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 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(®istry);
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(®istry);
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(®istry);
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(®istry);
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(®istry).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(®istry).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(®istry)
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(®istry)
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(®istry).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(®istry).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(®istry).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(®istry).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(®istry).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(®istry);
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(®istry);
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}