1#![warn(
78 future_incompatible,
79 missing_debug_implementations,
80 missing_docs,
81 nonstandard_style,
82 rust_2018_idioms,
83 unreachable_pub,
84 unused
85)]
86#![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))]
87#![doc(
88 html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg"
89)]
90#![cfg_attr(test, deny(warnings))]
91
92use once_cell::sync::OnceCell;
93use opentelemetry::{otel_error, otel_warn, InstrumentationScope, Key, Value};
94use opentelemetry_sdk::{
95 error::OTelSdkResult,
96 metrics::{
97 data::{self, ResourceMetrics},
98 reader::MetricReader,
99 InstrumentKind, ManualReader, Pipeline, Temporality,
100 },
101 Resource,
102};
103use prometheus::{
104 core::Desc,
105 proto::{LabelPair, MetricFamily, MetricType},
106};
107use std::{
108 borrow::Cow,
109 collections::{BTreeMap, HashMap},
110 sync::{Arc, Mutex},
111};
112use std::{fmt, sync::Weak};
113
114const TARGET_INFO_NAME: &str = "target_info";
115const TARGET_INFO_DESCRIPTION: &str = "Target metadata";
116
117const SCOPE_INFO_METRIC_NAME: &str = "otel_scope_info";
118const SCOPE_INFO_DESCRIPTION: &str = "Instrumentation Scope metadata";
119
120const SCOPE_INFO_KEYS: [&str; 2] = ["otel_scope_name", "otel_scope_version"];
121
122const COUNTER_SUFFIX: &str = "_total";
125
126mod config;
127mod resource_selector;
128mod utils;
129
130pub use config::ExporterBuilder;
131pub use resource_selector::ResourceSelector;
132
133pub fn exporter() -> ExporterBuilder {
135 ExporterBuilder::default()
136}
137
138#[derive(Debug)]
140pub struct PrometheusExporter {
141 reader: Arc<ManualReader>,
142}
143
144impl MetricReader for PrometheusExporter {
145 fn register_pipeline(&self, pipeline: Weak<Pipeline>) {
146 self.reader.register_pipeline(pipeline)
147 }
148
149 fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult {
150 self.reader.collect(rm)
151 }
152
153 fn force_flush(&self) -> OTelSdkResult {
154 self.reader.force_flush()
155 }
156
157 fn shutdown_with_timeout(&self, timeout: std::time::Duration) -> OTelSdkResult {
158 self.reader.shutdown_with_timeout(timeout)
159 }
160
161 fn temporality(&self, _kind: InstrumentKind) -> Temporality {
164 Temporality::Cumulative
165 }
166}
167
168struct Collector {
169 reader: Arc<ManualReader>,
170 disable_target_info: bool,
171 without_units: bool,
172 without_counter_suffixes: bool,
173 disable_scope_info: bool,
174 create_target_info_once: OnceCell<MetricFamily>,
175 resource_labels_once: OnceCell<Vec<LabelPair>>,
176 namespace: Option<String>,
177 inner: Mutex<CollectorInner>,
178 resource_selector: ResourceSelector,
179}
180
181#[derive(Default)]
182struct CollectorInner {
183 scope_infos: HashMap<InstrumentationScope, MetricFamily>,
184 metric_families: HashMap<String, MetricFamily>,
185}
186
187impl Collector {
188 fn metric_type_and_name(&self, m: &data::Metric) -> Option<(MetricType, Cow<'static, str>)> {
189 let mut name = self.get_name(m);
190
191 let result = match m.data() {
192 data::AggregatedMetrics::F64(metric_data) => match metric_data {
193 data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
194 data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
195 data::MetricData::Sum(sum) => {
196 if sum.is_monotonic() {
197 if !self.without_counter_suffixes {
198 name = format!("{name}{COUNTER_SUFFIX}").into();
199 }
200 Some(MetricType::COUNTER)
201 } else {
202 Some(MetricType::GAUGE)
203 }
204 }
205 data::MetricData::ExponentialHistogram(_) => None,
206 },
207 data::AggregatedMetrics::I64(metric_data) => match metric_data {
208 data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
209 data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
210 data::MetricData::Sum(sum) => {
211 if sum.is_monotonic() {
212 if !self.without_counter_suffixes {
213 name = format!("{name}{COUNTER_SUFFIX}").into();
214 }
215 Some(MetricType::COUNTER)
216 } else {
217 Some(MetricType::GAUGE)
218 }
219 }
220 data::MetricData::ExponentialHistogram(_) => None,
221 },
222 data::AggregatedMetrics::U64(metric_data) => match metric_data {
223 data::MetricData::Histogram(_) => Some(MetricType::HISTOGRAM),
224 data::MetricData::Gauge(_) => Some(MetricType::GAUGE),
225 data::MetricData::Sum(sum) => {
226 if sum.is_monotonic() {
227 if !self.without_counter_suffixes {
228 name = format!("{name}{COUNTER_SUFFIX}").into();
229 }
230 Some(MetricType::COUNTER)
231 } else {
232 Some(MetricType::GAUGE)
233 }
234 }
235 data::MetricData::ExponentialHistogram(_) => None,
236 },
237 };
238
239 result.map(|metric_type| (metric_type, name))
240 }
241
242 fn get_name(&self, m: &data::Metric) -> Cow<'static, str> {
243 let name: Cow<'static, str> = Cow::Owned(m.name().to_string());
244 let name = utils::sanitize_name(&name);
245 let unit_suffixes = if self.without_units {
246 None
247 } else {
248 utils::get_unit_suffixes(m.unit())
249 };
250 match (&self.namespace, unit_suffixes) {
251 (Some(namespace), Some(suffix)) => Cow::Owned(format!("{namespace}{name}_{suffix}")),
252 (Some(namespace), None) => Cow::Owned(format!("{namespace}{name}")),
253 (None, Some(suffix)) => Cow::Owned(format!("{name}_{suffix}")),
254 (None, None) => name,
255 }
256 }
257}
258
259impl prometheus::core::Collector for Collector {
260 fn desc(&self) -> Vec<&Desc> {
261 Vec::new()
262 }
263
264 fn collect(&self) -> Vec<MetricFamily> {
265 let mut inner = match self.inner.lock() {
266 Ok(guard) => guard,
267 Err(err) => {
268 otel_error!(
269 name: "MetricScrapeFailed",
270 message = err.to_string(),
271 );
272 return Vec::new();
273 }
274 };
275
276 let mut metrics = ResourceMetrics::default();
277 if let Err(err) = self.reader.collect(&mut metrics) {
278 otel_error!(
279 name: "MetricScrapeFailed",
280 message = err.to_string(),
281 );
282 return vec![];
283 }
284 let mut res = Vec::with_capacity(metrics.scope_metrics().count() + 1);
285
286 let target_info = self.create_target_info_once.get_or_init(|| {
287 create_info_metric(
289 TARGET_INFO_NAME,
290 TARGET_INFO_DESCRIPTION,
291 metrics.resource(),
292 )
293 });
294
295 if !self.disable_target_info && !metrics.resource().is_empty() {
296 res.push(target_info.clone())
297 }
298
299 let resource_labels = self
300 .resource_labels_once
301 .get_or_init(|| self.resource_selector.select(metrics.resource()));
302
303 for scope_metrics in metrics.scope_metrics() {
304 let scope_labels = if !self.disable_scope_info {
305 if scope_metrics.scope().attributes().count() > 0 {
306 let scope_info = inner
307 .scope_infos
308 .entry(scope_metrics.scope().clone())
309 .or_insert_with_key(create_scope_info_metric);
310 res.push(scope_info.clone());
311 }
312
313 let mut labels =
314 Vec::with_capacity(1 + scope_metrics.scope().version().is_some() as usize);
315 let mut name = LabelPair::default();
316 name.set_name(SCOPE_INFO_KEYS[0].into());
317 name.set_value(scope_metrics.scope().name().to_string());
318 labels.push(name);
319 if let Some(version) = &scope_metrics.scope().version() {
320 let mut l_version = LabelPair::default();
321 l_version.set_name(SCOPE_INFO_KEYS[1].into());
322 l_version.set_value(version.to_string());
323 labels.push(l_version);
324 }
325
326 if !resource_labels.is_empty() {
327 labels.extend(resource_labels.iter().cloned());
328 }
329 labels
330 } else {
331 Vec::new()
332 };
333
334 for metrics in scope_metrics.metrics() {
335 let (metric_type, name) = match self.metric_type_and_name(metrics) {
336 Some((metric_type, name)) => (metric_type, name),
337 _ => continue,
338 };
339
340 let mfs = &mut inner.metric_families;
341 let (drop, help) = validate_metrics(&name, metrics.description(), metric_type, mfs);
342 if drop {
343 continue;
344 }
345
346 let description = help.unwrap_or_else(|| metrics.description().into());
347
348 match metrics.data() {
349 data::AggregatedMetrics::F64(metric_data) => match metric_data {
350 data::MetricData::Histogram(hist) => {
351 add_histogram_metric(&mut res, hist, description, &scope_labels, name);
352 }
353 data::MetricData::Sum(sum) => {
354 add_sum_metric(&mut res, sum, description, &scope_labels, name);
355 }
356 data::MetricData::Gauge(gauge) => {
357 add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
358 }
359 data::MetricData::ExponentialHistogram(_) => {}
360 },
361 data::AggregatedMetrics::I64(metric_data) => match metric_data {
362 data::MetricData::Histogram(hist) => {
363 add_histogram_metric(&mut res, hist, description, &scope_labels, name);
364 }
365 data::MetricData::Sum(sum) => {
366 add_sum_metric(&mut res, sum, description, &scope_labels, name);
367 }
368 data::MetricData::Gauge(gauge) => {
369 add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
370 }
371 data::MetricData::ExponentialHistogram(_) => {}
372 },
373 data::AggregatedMetrics::U64(metric_data) => match metric_data {
374 data::MetricData::Histogram(hist) => {
375 add_histogram_metric(&mut res, hist, description, &scope_labels, name);
376 }
377 data::MetricData::Sum(sum) => {
378 add_sum_metric(&mut res, sum, description, &scope_labels, name);
379 }
380 data::MetricData::Gauge(gauge) => {
381 add_gauge_metric(&mut res, gauge, description, &scope_labels, name);
382 }
383 data::MetricData::ExponentialHistogram(_) => {}
384 },
385 }
386 }
387 }
388
389 res
390 }
391}
392
393fn get_attrs(kvs: &mut dyn Iterator<Item = (&Key, &Value)>, extra: &[LabelPair]) -> Vec<LabelPair> {
398 let mut keys_map = BTreeMap::<String, Vec<String>>::new();
399 for (key, value) in kvs {
400 let key = utils::sanitize_prom_kv(key.as_str());
401 keys_map
402 .entry(key)
403 .and_modify(|v| v.push(value.to_string()))
404 .or_insert_with(|| vec![value.to_string()]);
405 }
406
407 let mut res = Vec::with_capacity(keys_map.len() + extra.len());
408
409 for (key, mut values) in keys_map.into_iter() {
410 values.sort_unstable();
411
412 let mut lp = LabelPair::default();
413 lp.set_name(key);
414 lp.set_value(values.join(";"));
415 res.push(lp);
416 }
417
418 if !extra.is_empty() {
419 res.extend(&mut extra.iter().cloned());
420 }
421
422 res
423}
424
425fn validate_metrics(
426 name: &str,
427 description: &str,
428 metric_type: MetricType,
429 mfs: &mut HashMap<String, MetricFamily>,
430) -> (bool, Option<String>) {
431 if let Some(existing) = mfs.get(name) {
432 if existing.get_field_type() != metric_type {
433 otel_warn!(
434 name: "MetricValidationFailed",
435 message = "Instrument type conflict, using existing type definition",
436 metric_type = format!("Instrument {name}, Existing: {:?}, dropped: {:?}", existing.get_field_type(), metric_type).as_str(),
437 );
438 return (true, None);
439 }
440 if existing.help() != description {
441 otel_warn!(
442 name: "MetricValidationFailed",
443 message = "Instrument description conflict, using existing",
444 metric_description = format!("Instrument {name}, Existing: {:?}, dropped: {:?}", existing.help().to_string(), description.to_string()).as_str(),
445 );
446 return (false, Some(existing.help().to_string()));
447 }
448 (false, None)
449 } else {
450 let mut mf = MetricFamily::default();
451 mf.set_name(name.into());
452 mf.set_help(description.to_string());
453 mf.set_field_type(metric_type);
454 mfs.insert(name.to_string(), mf);
455
456 (false, None)
457 }
458}
459
460fn add_histogram_metric<T: Numeric + Copy>(
461 res: &mut Vec<MetricFamily>,
462 histogram: &data::Histogram<T>,
463 description: String,
464 extra: &[LabelPair],
465 name: Cow<'static, str>,
466) {
467 for dp in histogram.data_points() {
471 let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
472 let bounds: Vec<f64> = dp.bounds().collect();
473 let bucket_counts: Vec<u64> = dp.bucket_counts().collect();
474 let bounds_len = bounds.len();
475 let (bucket, _) = bounds.iter().enumerate().fold(
476 (Vec::with_capacity(bounds_len), 0),
477 |(mut acc, mut count), (i, bound)| {
478 count += bucket_counts[i];
479
480 let mut b = prometheus::proto::Bucket::default();
481 b.set_upper_bound(*bound);
482 b.set_cumulative_count(count);
483 acc.push(b);
484 (acc, count)
485 },
486 );
487
488 let mut h = prometheus::proto::Histogram::default();
489 h.set_sample_sum(dp.sum().as_f64());
490 h.set_sample_count(dp.count());
491 h.set_bucket(bucket);
492 let mut pm = prometheus::proto::Metric::default();
493 pm.set_label(kvs);
494 pm.set_histogram(h);
495
496 let mut mf = prometheus::proto::MetricFamily::default();
497 mf.set_name(name.to_string());
498 mf.set_help(description.clone());
499 mf.set_field_type(prometheus::proto::MetricType::HISTOGRAM);
500 mf.set_metric(vec![pm]);
501 res.push(mf);
502 }
503}
504
505fn add_sum_metric<T: Numeric + Copy>(
506 res: &mut Vec<MetricFamily>,
507 sum: &data::Sum<T>,
508 description: String,
509 extra: &[LabelPair],
510 name: Cow<'static, str>,
511) {
512 let metric_type = if sum.is_monotonic() {
513 MetricType::COUNTER
514 } else {
515 MetricType::GAUGE
516 };
517
518 for dp in sum.data_points() {
519 let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
520
521 let mut pm = prometheus::proto::Metric::default();
522 pm.set_label(kvs);
523
524 if sum.is_monotonic() {
525 let mut c = prometheus::proto::Counter::default();
526 c.set_value(dp.value().as_f64());
527 pm.set_counter(c);
528 } else {
529 let mut g = prometheus::proto::Gauge::default();
530 g.set_value(dp.value().as_f64());
531 pm.set_gauge(g);
532 }
533
534 let mut mf = prometheus::proto::MetricFamily::default();
535 mf.set_name(name.to_string());
536 mf.set_help(description.clone());
537 mf.set_field_type(metric_type);
538 mf.set_metric(vec![pm]);
539 res.push(mf);
540 }
541}
542
543fn add_gauge_metric<T: Numeric + Copy>(
544 res: &mut Vec<MetricFamily>,
545 gauge: &data::Gauge<T>,
546 description: String,
547 extra: &[LabelPair],
548 name: Cow<'static, str>,
549) {
550 for dp in gauge.data_points() {
551 let kvs = get_attrs(&mut dp.attributes().map(|kv| (&kv.key, &kv.value)), extra);
552
553 let mut g = prometheus::proto::Gauge::default();
554 g.set_value(dp.value().as_f64());
555 let mut pm = prometheus::proto::Metric::default();
556 pm.set_label(kvs);
557 pm.set_gauge(g);
558
559 let mut mf = prometheus::proto::MetricFamily::default();
560 mf.set_name(name.to_string());
561 mf.set_help(description.to_string());
562 mf.set_field_type(MetricType::GAUGE);
563 mf.set_metric(vec![pm]);
564 res.push(mf);
565 }
566}
567
568fn create_info_metric(
569 target_info_name: &str,
570 target_info_description: &str,
571 resource: &Resource,
572) -> MetricFamily {
573 let mut g = prometheus::proto::Gauge::default();
574 g.set_value(1.0);
575
576 let mut m = prometheus::proto::Metric::default();
577 m.set_label(get_attrs(&mut resource.iter(), &[]));
578 m.set_gauge(g);
579
580 let mut mf = MetricFamily::default();
581 mf.set_name(target_info_name.into());
582 mf.set_help(target_info_description.into());
583 mf.set_field_type(MetricType::GAUGE);
584 mf.set_metric(vec![m]);
585 mf
586}
587
588fn create_scope_info_metric(scope: &InstrumentationScope) -> MetricFamily {
589 let mut g = prometheus::proto::Gauge::default();
590 g.set_value(1.0);
591
592 let mut labels = Vec::with_capacity(1 + scope.version().is_some() as usize);
593 let mut name = LabelPair::default();
594 name.set_name(SCOPE_INFO_KEYS[0].into());
595 name.set_value(scope.name().to_string());
596 labels.push(name);
597 if let Some(version) = &scope.version() {
598 let mut v_label = LabelPair::default();
599 v_label.set_name(SCOPE_INFO_KEYS[1].into());
600 v_label.set_value(version.to_string());
601 labels.push(v_label);
602 }
603
604 let mut m = prometheus::proto::Metric::default();
605 m.set_label(labels);
606 m.set_gauge(g);
607
608 let mut mf = MetricFamily::default();
609 mf.set_name(SCOPE_INFO_METRIC_NAME.into());
610 mf.set_help(SCOPE_INFO_DESCRIPTION.into());
611 mf.set_field_type(MetricType::GAUGE);
612 mf.set_metric(vec![m]);
613 mf
614}
615
616trait Numeric: fmt::Debug {
617 fn as_f64(&self) -> f64;
619}
620
621impl Numeric for u64 {
622 fn as_f64(&self) -> f64 {
623 *self as f64
624 }
625}
626
627impl Numeric for i64 {
628 fn as_f64(&self) -> f64 {
629 *self as f64
630 }
631}
632
633impl Numeric for f64 {
634 fn as_f64(&self) -> f64 {
635 *self
636 }
637}