Skip to content

Commit

Permalink
feat: add otel_scope_info and scope labels (#974)
Browse files Browse the repository at this point in the history
* feat: add scope attr

* feat: add scope attr

* feat: add scope attr

* feat: must have version scope

* feat: add scope metric info

* feat: add test

* style: rust fmt

* style: rust fmt

* feat: change disable_scope_info to with_scope_info, and let the default value be true
  • Loading branch information
zengxilong authored Feb 27, 2023
1 parent 10b0c4c commit acd061d
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 22 deletions.
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

0 comments on commit acd061d

Please sign in to comment.