From ae7224cc835c2170960e77d5a18e463d2f41ee57 Mon Sep 17 00:00:00 2001 From: Thomas Stauffer Date: Sat, 22 Nov 2025 16:29:48 +0100 Subject: [PATCH 1/3] examples/metrics-advanced: add example with exponential histogram --- examples/metrics-advanced/Cargo.toml | 2 +- examples/metrics-advanced/src/main.rs | 50 ++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/examples/metrics-advanced/Cargo.toml b/examples/metrics-advanced/Cargo.toml index ed9349d8b1..22ef0bb1c2 100644 --- a/examples/metrics-advanced/Cargo.toml +++ b/examples/metrics-advanced/Cargo.toml @@ -14,6 +14,6 @@ bench = false [dependencies] opentelemetry = { workspace = true, features = ["metrics"] } -opentelemetry_sdk = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["spec_unstable_metrics_views"] } opentelemetry-stdout = { workspace = true, features = ["metrics"] } tokio = { workspace = true, features = ["full"] } diff --git a/examples/metrics-advanced/src/main.rs b/examples/metrics-advanced/src/main.rs index 41861e7f27..6589754b28 100644 --- a/examples/metrics-advanced/src/main.rs +++ b/examples/metrics-advanced/src/main.rs @@ -1,6 +1,6 @@ use opentelemetry::global; use opentelemetry::KeyValue; -use opentelemetry_sdk::metrics::{Instrument, SdkMeterProvider, Stream, Temporality}; +use opentelemetry_sdk::metrics::{Aggregation, Instrument, SdkMeterProvider, Stream, Temporality}; use opentelemetry_sdk::Resource; use std::error::Error; @@ -33,6 +33,36 @@ fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider { } }; + // for example 3 + // Unlike a regular OpenTelemetry histogram with fixed buckets, which can be + // specified explicitly, an exponential histogram calculates bucket widths + // automatically, growing them exponentially. The configuration is + // controlled by two parameters: max_size defines the maximum number of + // buckets, while max_scale adjusts the resolution, with higher values + // providing greater precision. + // If the minimum and maximum values are known in advance, a regular + // histogram is often the better choice. However, if the range of values is + // unpredictable e.g. may include extreme outliers, an exponential histogram + // is more suitable. A example is measuring packet round-trip time in a + // WLAN: while most packets return in milliseconds, some may occasionally + // take hundreds of milliseconds or even seconds. + // Details are in: + // https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram + let my_view_change_aggregation = |i: &Instrument| { + if i.name() == "my_third_histogram" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 10, + max_scale: 5, + record_min_max: true, + }) + .build() + .ok() + } else { + None + } + }; + // Build exporter using Delta Temporality. let exporter = opentelemetry_stdout::MetricExporterBuilder::default() .with_temporality(Temporality::Delta) @@ -47,6 +77,7 @@ fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider { .with_resource(resource) .with_view(my_view_rename_and_unit) .with_view(my_view_change_cardinality) + .with_view(my_view_change_aggregation) .build(); global::set_meter_provider(provider.clone()); provider @@ -112,6 +143,23 @@ async fn main() -> Result<(), Box> { histogram2.record(1.8, &[KeyValue::new("mykey1", "v7")]); + // Example 3 - Use exponential histogram. + let histogram3 = meter + .f64_histogram("my_third_histogram") + .with_description("My histogram example description") + .build(); + histogram3.record(-1.3, &[KeyValue::new("mykey1", "v1")]); + histogram3.record(-5.5, &[KeyValue::new("mykey1", "v1")]); + // is intentionally at the boundary of bucket + histogram3.record(-4.0, &[KeyValue::new("mykey1", "v1")]); + histogram3.record(16.0, &[KeyValue::new("mykey1", "v1")]); + // Internally the exponential histogram puts values either into a list of + // negative buckets or a list of positive buckets. Based on the values which + // are added the buckets are adjusted automatically. E.g. depending if the + // next record is commented/uncommented, then exponential histogram will + // have a different scale. + histogram3.record(0.4, &[KeyValue::new("mykey1", "v1")]); + // Metrics are exported by default every 60 seconds when using stdout exporter, // however shutting down the MeterProvider here instantly flushes // the metrics, instead of waiting for the 60 sec interval. From ee28930136f1153a06f9f06f9acd1df4de783348 Mon Sep 17 00:00:00 2001 From: Thomas Stauffer Date: Sat, 22 Nov 2025 16:31:00 +0100 Subject: [PATCH 2/3] stdout: also can print extended histogram --- opentelemetry-stdout/CHANGELOG.md | 2 + opentelemetry-stdout/src/metrics/exporter.rs | 92 +++++++++++++++++++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/opentelemetry-stdout/CHANGELOG.md b/opentelemetry-stdout/CHANGELOG.md index dce380699f..a32f2b1c8b 100644 --- a/opentelemetry-stdout/CHANGELOG.md +++ b/opentelemetry-stdout/CHANGELOG.md @@ -2,6 +2,8 @@ ## vNext +- ExponentialHistogram supported in stdout + ## 0.31.0 Released 2025-Sep-25 diff --git a/opentelemetry-stdout/src/metrics/exporter.rs b/opentelemetry-stdout/src/metrics/exporter.rs index 11d5494f03..570c5c2e1a 100644 --- a/opentelemetry-stdout/src/metrics/exporter.rs +++ b/opentelemetry-stdout/src/metrics/exporter.rs @@ -6,8 +6,8 @@ use opentelemetry_sdk::{ error::OTelSdkResult, metrics::{ data::{ - Gauge, GaugeDataPoint, Histogram, HistogramDataPoint, ResourceMetrics, ScopeMetrics, - Sum, SumDataPoint, + ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, GaugeDataPoint, Histogram, + HistogramDataPoint, ResourceMetrics, ScopeMetrics, Sum, SumDataPoint, }, exporter::PushMetricExporter, }, @@ -120,9 +120,9 @@ fn print_metrics<'a>(metrics: impl Iterator) { println!("\t\tType : Histogram"); print_histogram(hist); } - MetricData::ExponentialHistogram(_) => { + MetricData::ExponentialHistogram(hist) => { println!("\t\tType : Exponential Histogram"); - // TODO: add support for ExponentialHistogram + print_exponential_histogram(hist); } } } @@ -193,6 +193,26 @@ fn print_histogram(histogram: &Histogram) { print_hist_data_points(histogram.data_points()); } +fn print_exponential_histogram(histogram: &ExponentialHistogram) { + if histogram.temporality() == Temporality::Cumulative { + println!("\t\tTemporality : Cumulative"); + } else { + println!("\t\tTemporality : Delta"); + } + let datetime: DateTime = histogram.start_time().into(); + println!( + "\t\tStartTime : {}", + datetime.format("%Y-%m-%d %H:%M:%S%.6f") + ); + let datetime: DateTime = histogram.time().into(); + println!( + "\t\tEndTime : {}", + datetime.format("%Y-%m-%d %H:%M:%S%.6f") + ); + println!("\t\tExponential Histogram DataPoints"); + print_exponential_hist_data_points(histogram.data_points()); +} + fn print_sum_data_points<'a, T: Debug + Copy + 'a>( data_points: impl Iterator>, ) { @@ -266,6 +286,70 @@ fn print_hist_data_points<'a, T: Debug + Copy + 'a>( } } +fn print_exponential_hist_data_points<'a, T: Debug + Copy + 'a>( + data_points: impl Iterator>, +) { + for (i, data_point) in data_points.enumerate() { + println!("\t\tDataPoint #{i}"); + println!("\t\t\tCount : {}", data_point.count()); + println!("\t\t\tSum : {:?}", data_point.sum()); + if let Some(min) = &data_point.min() { + println!("\t\t\tMin : {min:?}"); + } + + if let Some(max) = &data_point.max() { + println!("\t\t\tMax : {max:?}"); + } + + let scale = data_point.scale(); + let base = 2.0f64.powf(2.0f64.powf(-scale as f64)); + + println!("\t\t\tScale : {:?}", scale); + println!("\t\t\tBase : {:?}", base); + println!("\t\t\tZeroCount : {}", data_point.zero_count()); + println!("\t\t\tZeroThreshold : {}", data_point.zero_threshold()); + + println!("\t\t\tAttributes :"); + for kv in data_point.attributes() { + println!("\t\t\t\t -> {} : {}", kv.key, kv.value.as_str()); + } + + // Bucket upper-bounds are inclusive while bucket lower-bounds are + // exclusive. Details if a bound is including/excluding can be found in: + // https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram-bucket-inclusivity + + let negative_bucket = data_point.negative_bucket(); + let negative_offset = negative_bucket.offset(); + println!("\t\t\tNegativeOffset : {}", negative_offset); + for (i, count) in negative_bucket + .counts() + .collect::>() + .into_iter() + .enumerate() + .rev() + { + let lower = -base.powf(i as f64 + negative_offset as f64 + 1.0f64); + let upper = -base.powf(i as f64 + negative_offset as f64); + println!( + "\t\t\t\tBucket {} ({:?}, {:?}] : {}", + i, lower, upper, count + ); + } + + let positive_bucket = data_point.positive_bucket(); + let positive_offset = positive_bucket.offset(); + println!("\t\t\tPositiveOffset : {}", positive_offset); + for (i, count) in positive_bucket.counts().enumerate() { + let lower = base.powf(i as f64 + positive_offset as f64); + let upper = base.powf(i as f64 + positive_offset as f64 + 1.0f64); + println!( + "\t\t\t\tBucket {} ({:?}, {:?}] : {}", + i, lower, upper, count + ); + } + } +} + /// Configuration for the stdout metrics exporter #[derive(Default)] pub struct MetricExporterBuilder { From 29045c25431ff9f92b5ba52a1cafda739fedf4a9 Mon Sep 17 00:00:00 2001 From: Thomas Stauffer Date: Sat, 22 Nov 2025 23:57:56 +0100 Subject: [PATCH 3/3] aggregation validate: use constants for output --- opentelemetry-sdk/src/metrics/aggregation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/metrics/aggregation.rs b/opentelemetry-sdk/src/metrics/aggregation.rs index b03bc4f6d6..af9cb02adc 100644 --- a/opentelemetry-sdk/src/metrics/aggregation.rs +++ b/opentelemetry-sdk/src/metrics/aggregation.rs @@ -130,12 +130,12 @@ impl Aggregation { Aggregation::Base2ExponentialHistogram { max_scale, .. } => { if *max_scale > EXPO_MAX_SCALE { return Err(MetricError::Config(format!( - "aggregation: exponential histogram: max scale ({max_scale}) is greater than 20", + "aggregation: exponential histogram: max scale ({max_scale}) is greater than {}", EXPO_MAX_SCALE ))); } if *max_scale < EXPO_MIN_SCALE { return Err(MetricError::Config(format!( - "aggregation: exponential histogram: max scale ({max_scale}) is less than -10", + "aggregation: exponential histogram: max scale ({max_scale}) is less than {}", EXPO_MIN_SCALE ))); }