Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add otel_scope_info and scope labels #974

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 125 additions & 14 deletions opentelemetry-prometheus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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},
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -125,6 +139,9 @@ pub struct ExporterBuilder {

/// The metrics controller
controller: BasicController,

/// config for exporter
config: Option<ExporterConfig>,
}

impl ExporterBuilder {
Expand All @@ -133,6 +150,7 @@ impl ExporterBuilder {
ExporterBuilder {
registry: None,
controller,
config: Some(Default::default()),
}
}

Expand All @@ -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<PrometheusExporter, MetricsError> {
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()))?;
Expand All @@ -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
Expand Down Expand Up @@ -203,6 +256,7 @@ impl PrometheusExporter {
#[derive(Debug)]
struct Collector {
controller: Arc<Mutex<BasicController>>,
with_scope_info: bool,
}

impl TemporalitySelector for Collector {
Expand All @@ -213,7 +267,14 @@ impl TemporalitySelector for Collector {

impl Collector {
fn with_controller(controller: Arc<Mutex<BasicController>>) -> 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
}
}

Expand All @@ -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<prometheus::proto::LabelPair> = 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::<HistogramAggregator>() {
metrics.push(build_histogram(hist, number_kind, desc, labels)?);
Expand Down Expand Up @@ -380,6 +447,45 @@ fn build_histogram(
Ok(mf)
}

fn build_scope_metric(
labels: Vec<prometheus::proto::LabelPair>,
) -> 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<prometheus::proto::LabelPair> {
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()));
Expand All @@ -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<prometheus::proto::LabelPair>,
) -> Vec<prometheus::proto::LabelPair> {
// 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<prometheus::proto::LabelPair> = iter
.map(|(key, value)| build_label_pair(key, value))
.collect();

labels.append(scope_labels);
labels
}

struct PrometheusMetricDesc {
Expand Down
81 changes: 73 additions & 8 deletions opentelemetry-prometheus/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -19,18 +19,20 @@ 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")];

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());
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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();
Expand Down