Skip to content
Open
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
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", "myvalue1")]);
histogram3.record(-5.5, &[KeyValue::new("mykey1", "myvalue1")]);
// is intentionally at the boundary of bucket
histogram3.record(-4.0, &[KeyValue::new("mykey1", "myvalue1")]);
histogram3.record(7.5, &[KeyValue::new("mykey1", "myvalue1")]);
// 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", "myvalue1")]);

// 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 : {:.3}", 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\t -> Bucket {} ({:.3}, {:.3}] : {}",
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\t -> Bucket {} ({:.3}, {:.3}] : {}",
i, lower, upper, count
);
}
}
}

/// Configuration for the stdout metrics exporter
#[derive(Default)]
pub struct MetricExporterBuilder {
Expand Down