Skip to content

Commit 07bb613

Browse files
feat: Improve support for exponential histogram (open-telemetry#3259)
Co-authored-by: Cijo Thomas <[email protected]>
1 parent 693dcc0 commit 07bb613

File tree

5 files changed

+142
-8
lines changed

5 files changed

+142
-8
lines changed

examples/metrics-advanced/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ bench = false
1414

1515
[dependencies]
1616
opentelemetry = { workspace = true, features = ["metrics"] }
17-
opentelemetry_sdk = { workspace = true }
17+
opentelemetry_sdk = { workspace = true, features = ["spec_unstable_metrics_views"] }
1818
opentelemetry-stdout = { workspace = true, features = ["metrics"] }
1919
tokio = { workspace = true, features = ["full"] }

examples/metrics-advanced/src/main.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use opentelemetry::global;
22
use opentelemetry::KeyValue;
3-
use opentelemetry_sdk::metrics::{Instrument, SdkMeterProvider, Stream, Temporality};
3+
use opentelemetry_sdk::metrics::{Aggregation, Instrument, SdkMeterProvider, Stream, Temporality};
44
use opentelemetry_sdk::Resource;
55
use std::error::Error;
66

@@ -33,6 +33,36 @@ fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider {
3333
}
3434
};
3535

36+
// for example 3
37+
// Unlike a regular OpenTelemetry histogram with fixed buckets, which can be
38+
// specified explicitly, an exponential histogram calculates bucket widths
39+
// automatically, growing them exponentially. The configuration is
40+
// controlled by two parameters: max_size defines the maximum number of
41+
// buckets, while max_scale adjusts the resolution, with higher values
42+
// providing greater precision.
43+
// If the minimum and maximum values are known in advance, a regular
44+
// histogram is often the better choice. However, if the range of values is
45+
// unpredictable e.g. may include extreme outliers, an exponential histogram
46+
// is more suitable. A example is measuring packet round-trip time in a
47+
// WLAN: while most packets return in milliseconds, some may occasionally
48+
// take hundreds of milliseconds or even seconds.
49+
// Details are in:
50+
// https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram
51+
let my_view_change_aggregation = |i: &Instrument| {
52+
if i.name() == "my_third_histogram" {
53+
Stream::builder()
54+
.with_aggregation(Aggregation::Base2ExponentialHistogram {
55+
max_size: 10,
56+
max_scale: 5,
57+
record_min_max: true,
58+
})
59+
.build()
60+
.ok()
61+
} else {
62+
None
63+
}
64+
};
65+
3666
// Build exporter using Delta Temporality.
3767
let exporter = opentelemetry_stdout::MetricExporterBuilder::default()
3868
.with_temporality(Temporality::Delta)
@@ -47,6 +77,7 @@ fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider {
4777
.with_resource(resource)
4878
.with_view(my_view_rename_and_unit)
4979
.with_view(my_view_change_cardinality)
80+
.with_view(my_view_change_aggregation)
5081
.build();
5182
global::set_meter_provider(provider.clone());
5283
provider
@@ -112,6 +143,23 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
112143

113144
histogram2.record(1.8, &[KeyValue::new("mykey1", "v7")]);
114145

146+
// Example 3 - Use exponential histogram.
147+
let histogram3 = meter
148+
.f64_histogram("my_third_histogram")
149+
.with_description("My histogram example description")
150+
.build();
151+
histogram3.record(-1.3, &[KeyValue::new("mykey1", "v1")]);
152+
histogram3.record(-5.5, &[KeyValue::new("mykey1", "v1")]);
153+
// is intentionally at the boundary of bucket
154+
histogram3.record(-4.0, &[KeyValue::new("mykey1", "v1")]);
155+
histogram3.record(16.0, &[KeyValue::new("mykey1", "v1")]);
156+
// Internally the exponential histogram puts values either into a list of
157+
// negative buckets or a list of positive buckets. Based on the values which
158+
// are added the buckets are adjusted automatically. E.g. depending if the
159+
// next record is commented/uncommented, then exponential histogram will
160+
// have a different scale.
161+
histogram3.record(0.4, &[KeyValue::new("mykey1", "v1")]);
162+
115163
// Metrics are exported by default every 60 seconds when using stdout exporter,
116164
// however shutting down the MeterProvider here instantly flushes
117165
// the metrics, instead of waiting for the 60 sec interval.

opentelemetry-sdk/src/metrics/aggregation.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ impl Aggregation {
130130
Aggregation::Base2ExponentialHistogram { max_scale, .. } => {
131131
if *max_scale > EXPO_MAX_SCALE {
132132
return Err(MetricError::Config(format!(
133-
"aggregation: exponential histogram: max scale ({max_scale}) is greater than 20",
133+
"aggregation: exponential histogram: max scale ({max_scale}) is greater than {}", EXPO_MAX_SCALE
134134
)));
135135
}
136136
if *max_scale < EXPO_MIN_SCALE {
137137
return Err(MetricError::Config(format!(
138-
"aggregation: exponential histogram: max scale ({max_scale}) is less than -10",
138+
"aggregation: exponential histogram: max scale ({max_scale}) is less than {}", EXPO_MIN_SCALE
139139
)));
140140
}
141141

opentelemetry-stdout/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## vNext
44

5+
- ExponentialHistogram supported in stdout
6+
57
## 0.31.0
68

79
Released 2025-Sep-25

opentelemetry-stdout/src/metrics/exporter.rs

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use opentelemetry_sdk::{
66
error::OTelSdkResult,
77
metrics::{
88
data::{
9-
Gauge, GaugeDataPoint, Histogram, HistogramDataPoint, ResourceMetrics, ScopeMetrics,
10-
Sum, SumDataPoint,
9+
ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, GaugeDataPoint, Histogram,
10+
HistogramDataPoint, ResourceMetrics, ScopeMetrics, Sum, SumDataPoint,
1111
},
1212
exporter::PushMetricExporter,
1313
},
@@ -120,9 +120,9 @@ fn print_metrics<'a>(metrics: impl Iterator<Item = &'a ScopeMetrics>) {
120120
println!("\t\tType : Histogram");
121121
print_histogram(hist);
122122
}
123-
MetricData::ExponentialHistogram(_) => {
123+
MetricData::ExponentialHistogram(hist) => {
124124
println!("\t\tType : Exponential Histogram");
125-
// TODO: add support for ExponentialHistogram
125+
print_exponential_histogram(hist);
126126
}
127127
}
128128
}
@@ -193,6 +193,26 @@ fn print_histogram<T: Debug + Copy>(histogram: &Histogram<T>) {
193193
print_hist_data_points(histogram.data_points());
194194
}
195195

196+
fn print_exponential_histogram<T: Debug + Copy>(histogram: &ExponentialHistogram<T>) {
197+
if histogram.temporality() == Temporality::Cumulative {
198+
println!("\t\tTemporality : Cumulative");
199+
} else {
200+
println!("\t\tTemporality : Delta");
201+
}
202+
let datetime: DateTime<Utc> = histogram.start_time().into();
203+
println!(
204+
"\t\tStartTime : {}",
205+
datetime.format("%Y-%m-%d %H:%M:%S%.6f")
206+
);
207+
let datetime: DateTime<Utc> = histogram.time().into();
208+
println!(
209+
"\t\tEndTime : {}",
210+
datetime.format("%Y-%m-%d %H:%M:%S%.6f")
211+
);
212+
println!("\t\tExponential Histogram DataPoints");
213+
print_exponential_hist_data_points(histogram.data_points());
214+
}
215+
196216
fn print_sum_data_points<'a, T: Debug + Copy + 'a>(
197217
data_points: impl Iterator<Item = &'a SumDataPoint<T>>,
198218
) {
@@ -266,6 +286,70 @@ fn print_hist_data_points<'a, T: Debug + Copy + 'a>(
266286
}
267287
}
268288

289+
fn print_exponential_hist_data_points<'a, T: Debug + Copy + 'a>(
290+
data_points: impl Iterator<Item = &'a ExponentialHistogramDataPoint<T>>,
291+
) {
292+
for (i, data_point) in data_points.enumerate() {
293+
println!("\t\tDataPoint #{i}");
294+
println!("\t\t\tCount : {}", data_point.count());
295+
println!("\t\t\tSum : {:?}", data_point.sum());
296+
if let Some(min) = &data_point.min() {
297+
println!("\t\t\tMin : {min:?}");
298+
}
299+
300+
if let Some(max) = &data_point.max() {
301+
println!("\t\t\tMax : {max:?}");
302+
}
303+
304+
let scale = data_point.scale();
305+
let base = 2.0f64.powf(2.0f64.powf(-scale as f64));
306+
307+
println!("\t\t\tScale : {:?}", scale);
308+
println!("\t\t\tBase : {:?}", base);
309+
println!("\t\t\tZeroCount : {}", data_point.zero_count());
310+
println!("\t\t\tZeroThreshold : {}", data_point.zero_threshold());
311+
312+
println!("\t\t\tAttributes :");
313+
for kv in data_point.attributes() {
314+
println!("\t\t\t\t -> {} : {}", kv.key, kv.value.as_str());
315+
}
316+
317+
// Bucket upper-bounds are inclusive while bucket lower-bounds are
318+
// exclusive. Details if a bound is including/excluding can be found in:
319+
// https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram-bucket-inclusivity
320+
321+
let negative_bucket = data_point.negative_bucket();
322+
let negative_offset = negative_bucket.offset();
323+
println!("\t\t\tNegativeOffset : {}", negative_offset);
324+
for (i, count) in negative_bucket
325+
.counts()
326+
.collect::<Vec<_>>()
327+
.into_iter()
328+
.enumerate()
329+
.rev()
330+
{
331+
let lower = -base.powf(i as f64 + negative_offset as f64 + 1.0f64);
332+
let upper = -base.powf(i as f64 + negative_offset as f64);
333+
println!(
334+
"\t\t\t\tBucket {} ({:?}, {:?}] : {}",
335+
i, lower, upper, count
336+
);
337+
}
338+
339+
let positive_bucket = data_point.positive_bucket();
340+
let positive_offset = positive_bucket.offset();
341+
println!("\t\t\tPositiveOffset : {}", positive_offset);
342+
for (i, count) in positive_bucket.counts().enumerate() {
343+
let lower = base.powf(i as f64 + positive_offset as f64);
344+
let upper = base.powf(i as f64 + positive_offset as f64 + 1.0f64);
345+
println!(
346+
"\t\t\t\tBucket {} ({:?}, {:?}] : {}",
347+
i, lower, upper, count
348+
);
349+
}
350+
}
351+
}
352+
269353
/// Configuration for the stdout metrics exporter
270354
#[derive(Default)]
271355
pub struct MetricExporterBuilder {

0 commit comments

Comments
 (0)