Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion examples/metrics-advanced/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
50 changes: 49 additions & 1 deletion examples/metrics-advanced/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -112,6 +143,23 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {

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")]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#record

-ve values are not expected to be provided, though we dont validate that!

See #3260 for open PR/discussions.

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.
Expand Down
4 changes: 2 additions & 2 deletions opentelemetry-sdk/src/metrics/aggregation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)));
}

Expand Down
2 changes: 2 additions & 0 deletions opentelemetry-stdout/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## vNext

- ExponentialHistogram supported in stdout

## 0.31.0

Released 2025-Sep-25
Expand Down
92 changes: 88 additions & 4 deletions opentelemetry-stdout/src/metrics/exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -120,9 +120,9 @@ fn print_metrics<'a>(metrics: impl Iterator<Item = &'a ScopeMetrics>) {
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);
}
}
}
Expand Down Expand Up @@ -193,6 +193,26 @@ fn print_histogram<T: Debug + Copy>(histogram: &Histogram<T>) {
print_hist_data_points(histogram.data_points());
}

fn print_exponential_histogram<T: Debug + Copy>(histogram: &ExponentialHistogram<T>) {
if histogram.temporality() == Temporality::Cumulative {
println!("\t\tTemporality : Cumulative");
} else {
println!("\t\tTemporality : Delta");
}
let datetime: DateTime<Utc> = histogram.start_time().into();
println!(
"\t\tStartTime : {}",
datetime.format("%Y-%m-%d %H:%M:%S%.6f")
);
let datetime: DateTime<Utc> = 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<Item = &'a SumDataPoint<T>>,
) {
Expand Down Expand Up @@ -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<Item = &'a ExponentialHistogramDataPoint<T>>,
) {
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::<Vec<_>>()
.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 {
Expand Down