diff --git a/opentelemetry-prometheus/src/lib.rs b/opentelemetry-prometheus/src/lib.rs index 7eb6c331cb..14f5803481 100644 --- a/opentelemetry-prometheus/src/lib.rs +++ b/opentelemetry-prometheus/src/lib.rs @@ -48,15 +48,18 @@ //! // //! // # HELP a_counter Counts things //! // # TYPE a_counter counter -//! // a_counter{R="V",key="value"} 100 +//! // a_counter{R="V",key="value",otel_scope_name="my-app",otel_scope_version=""} 100 //! // # HELP a_histogram Records values //! // # TYPE a_histogram histogram -//! // a_histogram_bucket{R="V",key="value",le="0.5"} 0 -//! // a_histogram_bucket{R="V",key="value",le="0.9"} 0 -//! // a_histogram_bucket{R="V",key="value",le="0.99"} 0 -//! // a_histogram_bucket{R="V",key="value",le="+Inf"} 1 -//! // a_histogram_sum{R="V",key="value"} 100 -//! // a_histogram_count{R="V",key="value"} 1 +//! // a_histogram_bucket{R="V",key="value",le="0.5",otel_scope_name="my-app",otel_scope_version=""} 0 +//! // a_histogram_bucket{R="V",key="value",le="0.9",otel_scope_name="my-app",otel_scope_version=""} 0 +//! // a_histogram_bucket{R="V",key="value",le="0.99",otel_scope_name="my-app",otel_scope_version=""} 0 +//! // a_histogram_bucket{R="V",key="value",le="+Inf",otel_scope_name="my-app",otel_scope_version=""} 1 +//! // a_histogram_sum{R="V",key="value",otel_scope_name="my-app",otel_scope_version=""} 100 +//! // a_histogram_count{R="V",key="value",otel_scope_name="my-app",otel_scope_version=""} 1 +//! // HELP otel_scope_info Instrumentation Scope metadata +//! // TYPE otel_scope_info gauge +//! // otel_scope_info{otel_scope_name="ex.com/B",otel_scope_version=""} 1 //! ``` #![warn( future_incompatible, @@ -86,7 +89,6 @@ use opentelemetry::sdk::metrics::sdk_api::Descriptor; #[cfg(feature = "prometheus-encoding")] pub use prometheus::{Encoder, TextEncoder}; -use opentelemetry::global; use opentelemetry::sdk::{ export::metrics::{ aggregation::{Histogram, LastValue, Sum}, @@ -100,6 +102,7 @@ use opentelemetry::sdk::{ Resource, }; use opentelemetry::{attributes, metrics::MetricsError, Context, Key, Value}; +use opentelemetry::{global, InstrumentationLibrary, StringValue}; use std::sync::{Arc, Mutex}; mod sanitize; @@ -110,6 +113,17 @@ use sanitize::sanitize; /// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.14.0/specification/metrics/data-model.md#sums-1 const MONOTONIC_COUNTER_SUFFIX: &str = "_total"; +/// Instrumentation Scope name MUST added as otel_scope_name label. +const OTEL_SCOPE_NAME: &str = "otel_scope_name"; + +/// Instrumentation Scope version MUST added as otel_scope_name label. +const OTEL_SCOPE_VERSION: &str = "otel_scope_version"; + +/// otel_scope_name metric name. +const SCOPE_INFO_METRIC_NAME: &str = "otel_scope_info"; + +/// otel_scope_name metric help. +const SCOPE_INFO_DESCRIPTION: &str = "Instrumentation Scope metadata"; /// Create a new prometheus exporter builder. pub fn exporter(controller: BasicController) -> ExporterBuilder { ExporterBuilder::new(controller) @@ -125,6 +139,9 @@ pub struct ExporterBuilder { /// The metrics controller controller: BasicController, + + /// config for exporter + config: Option, } impl ExporterBuilder { @@ -133,6 +150,7 @@ impl ExporterBuilder { ExporterBuilder { registry: None, controller, + config: Some(Default::default()), } } @@ -144,13 +162,24 @@ impl ExporterBuilder { } } + /// Set config to be used by this exporter + pub fn with_config(self, config: ExporterConfig) -> Self { + ExporterBuilder { + config: Some(config), + ..self + } + } + /// Sets up a complete export pipeline with the recommended setup, using the /// recommended selector and standard processor. pub fn try_init(self) -> Result { + let config = self.config.unwrap_or_default(); + let registry = self.registry.unwrap_or_else(prometheus::Registry::new); let controller = Arc::new(Mutex::new(self.controller)); - let collector = Collector::with_controller(controller.clone()); + let collector = + Collector::with_controller(controller.clone()).with_scope_info(config.with_scope_info); registry .register(Box::new(collector)) .map_err(|e| MetricsError::Other(e.to_string()))?; @@ -175,6 +204,30 @@ impl ExporterBuilder { } } +/// Config for prometheus exporter +#[derive(Debug)] +pub struct ExporterConfig { + /// Add the otel_scope_info metric and otel_scope_ labels when with_scope_info is true, and the default value is true. + with_scope_info: bool, +} + +impl Default for ExporterConfig { + fn default() -> Self { + ExporterConfig { + with_scope_info: true, + } + } +} + +impl ExporterConfig { + /// Set with_scope_info for [`ExporterConfig`]. + /// It's the flag to add the otel_scope_info metric and otel_scope_ labels. + pub fn with_scope_info(mut self, enabled: bool) -> Self { + self.with_scope_info = enabled; + self + } +} + /// An implementation of `metrics::Exporter` that sends metrics to Prometheus. /// /// This exporter supports Prometheus pulls, as such it does not @@ -203,6 +256,7 @@ impl PrometheusExporter { #[derive(Debug)] struct Collector { controller: Arc>, + with_scope_info: bool, } impl TemporalitySelector for Collector { @@ -213,7 +267,14 @@ impl TemporalitySelector for Collector { impl Collector { fn with_controller(controller: Arc>) -> Self { - Collector { controller } + Collector { + controller, + with_scope_info: true, + } + } + fn with_scope_info(mut self, with_scope_info: bool) -> Self { + self.with_scope_info = with_scope_info; + self } } @@ -233,14 +294,20 @@ impl prometheus::core::Collector for Collector { return metrics; } - if let Err(err) = controller.try_for_each(&mut |_library, reader| { + if let Err(err) = controller.try_for_each(&mut |library, reader| { + let mut scope_labels: Vec = Vec::new(); + if self.with_scope_info { + scope_labels = get_scope_labels(library); + metrics.push(build_scope_metric(scope_labels.clone())); + } reader.try_for_each(self, &mut |record| { let agg = record.aggregator().ok_or(MetricsError::NoDataCollected)?; let number_kind = record.descriptor().number_kind(); let instrument_kind = record.descriptor().instrument_kind(); let desc = get_metric_desc(record); - let labels = get_metric_labels(record, controller.resource()); + let labels = + get_metric_labels(record, controller.resource(), &mut scope_labels.clone()); if let Some(hist) = agg.as_any().downcast_ref::() { metrics.push(build_histogram(hist, number_kind, desc, labels)?); @@ -380,6 +447,45 @@ fn build_histogram( Ok(mf) } +fn build_scope_metric( + labels: Vec, +) -> prometheus::proto::MetricFamily { + let mut g = prometheus::proto::Gauge::new(); + g.set_value(1.0); + + let mut m = prometheus::proto::Metric::default(); + m.set_label(protobuf::RepeatedField::from_vec(labels)); + m.set_gauge(g); + + let mut mf = prometheus::proto::MetricFamily::default(); + mf.set_name(String::from(SCOPE_INFO_METRIC_NAME)); + mf.set_help(String::from(SCOPE_INFO_DESCRIPTION)); + mf.set_field_type(prometheus::proto::MetricType::GAUGE); + mf.set_metric(protobuf::RepeatedField::from_vec(vec![m])); + + mf +} + +fn get_scope_labels(library: &InstrumentationLibrary) -> Vec { + let mut labels = Vec::new(); + labels.push(build_label_pair( + &Key::new(OTEL_SCOPE_NAME), + &Value::String(StringValue::from(library.name.clone().to_string())), + )); + if let Some(version) = library.version.to_owned() { + labels.push(build_label_pair( + &Key::new(OTEL_SCOPE_VERSION), + &Value::String(StringValue::from(version.to_string())), + )); + } else { + labels.push(build_label_pair( + &Key::new(OTEL_SCOPE_VERSION), + &Value::String(StringValue::from("")), + )); + } + labels +} + fn build_label_pair(key: &Key, value: &Value) -> prometheus::proto::LabelPair { let mut lp = prometheus::proto::LabelPair::new(); lp.set_name(sanitize(key.as_str())); @@ -391,12 +497,17 @@ fn build_label_pair(key: &Key, value: &Value) -> prometheus::proto::LabelPair { fn get_metric_labels( record: &Record<'_>, resource: &Resource, + scope_labels: &mut Vec, ) -> Vec { // Duplicate keys are resolved by taking the record label value over // the resource value. let iter = attributes::merge_iters(record.attributes().iter(), resource.iter()); - iter.map(|(key, value)| build_label_pair(key, value)) - .collect() + let mut labels: Vec = iter + .map(|(key, value)| build_label_pair(key, value)) + .collect(); + + labels.append(scope_labels); + labels } struct PrometheusMetricDesc { diff --git a/opentelemetry-prometheus/tests/integration_test.rs b/opentelemetry-prometheus/tests/integration_test.rs index e58496f68a..0cd3fbe5c1 100644 --- a/opentelemetry-prometheus/tests/integration_test.rs +++ b/opentelemetry-prometheus/tests/integration_test.rs @@ -3,7 +3,7 @@ use opentelemetry::sdk::metrics::{controllers, processors, selectors}; use opentelemetry::sdk::Resource; use opentelemetry::Context; use opentelemetry::{metrics::MeterProvider, KeyValue}; -use opentelemetry_prometheus::PrometheusExporter; +use opentelemetry_prometheus::{ExporterConfig, PrometheusExporter}; use prometheus::{Encoder, TextEncoder}; #[test] @@ -19,10 +19,11 @@ fn free_unused_instruments() { let mut expected = Vec::new(); { - let meter = exporter - .meter_provider() - .unwrap() - .versioned_meter("test", None, None); + let meter = + exporter + .meter_provider() + .unwrap() + .versioned_meter("test", Some("v0.1.0"), None); let counter = meter.f64_counter("counter").init(); let attributes = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")]; @@ -30,7 +31,8 @@ fn free_unused_instruments() { counter.add(&cx, 10.0, &attributes); counter.add(&cx, 5.3, &attributes); - expected.push(r#"counter_total{A="B",C="D",R="V"} 15.3"#); + expected.push(r#"counter_total{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 15.3"#); + expected.push(r#"otel_scope_info{otel_scope_name="test",otel_scope_version="v0.1.0"} 1"#); } // Standard export compare_export(&exporter, expected.clone()); @@ -49,7 +51,9 @@ fn test_add() { )) .with_resource(Resource::new(vec![KeyValue::new("R", "V")])) .build(); - let exporter = opentelemetry_prometheus::exporter(controller).init(); + let exporter = opentelemetry_prometheus::exporter(controller) + .with_config(ExporterConfig::default().with_scope_info(false)) + .init(); let meter = exporter .meter_provider() @@ -108,7 +112,9 @@ fn test_sanitization() { "Test Service", )])) .build(); - let exporter = opentelemetry_prometheus::exporter(controller).init(); + let exporter = opentelemetry_prometheus::exporter(controller) + .with_config(ExporterConfig::default().with_scope_info(false)) + .init(); let meter = exporter .meter_provider() .unwrap() @@ -134,6 +140,65 @@ fn test_sanitization() { compare_export(&exporter, expected) } +#[test] +fn test_scope_info() { + let cx = Context::new(); + let controller = controllers::basic(processors::factory( + selectors::simple::histogram(vec![-0.5, 1.0]), + aggregation::cumulative_temporality_selector(), + )) + .with_resource(Resource::new(vec![KeyValue::new("R", "V")])) + .build(); + let exporter = opentelemetry_prometheus::exporter(controller).init(); + + let meter = exporter + .meter_provider() + .unwrap() + .versioned_meter("test", Some("v0.1.0"), None); + + let up_down_counter = meter.f64_up_down_counter("updowncounter").init(); + let counter = meter.f64_counter("counter").init(); + let histogram = meter.f64_histogram("my.histogram").init(); + + let attributes = vec![KeyValue::new("A", "B"), KeyValue::new("C", "D")]; + + let mut expected = Vec::new(); + + counter.add(&cx, 10.0, &attributes); + counter.add(&cx, 5.3, &attributes); + + expected.push(r#"counter_total{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 15.3"#); + + let cb_attributes = attributes.clone(); + let gauge = meter.i64_observable_gauge("intgauge").init(); + meter + .register_callback(move |cx| gauge.observe(cx, 1, cb_attributes.as_ref())) + .unwrap(); + + expected.push( + r#"intgauge{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 1"#, + ); + + histogram.record(&cx, -0.6, &attributes); + histogram.record(&cx, -0.4, &attributes); + histogram.record(&cx, 0.6, &attributes); + histogram.record(&cx, 20.0, &attributes); + + expected.push(r#"my_histogram_bucket{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0",le="+Inf"} 4"#); + expected.push(r#"my_histogram_bucket{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0",le="-0.5"} 1"#); + expected.push(r#"my_histogram_bucket{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0",le="1"} 3"#); + expected.push(r#"my_histogram_count{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 4"#); + expected.push(r#"my_histogram_sum{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 19.6"#); + + up_down_counter.add(&cx, 10.0, &attributes); + up_down_counter.add(&cx, -3.2, &attributes); + + expected.push(r#"updowncounter{A="B",C="D",R="V",otel_scope_name="test",otel_scope_version="v0.1.0"} 6.8"#); + expected.push(r#"otel_scope_info{otel_scope_name="test",otel_scope_version="v0.1.0"} 1"#); + + compare_export(&exporter, expected) +} + fn compare_export(exporter: &PrometheusExporter, mut expected: Vec<&'static str>) { let mut output = Vec::new(); let encoder = TextEncoder::new();