From 055d5f6675c26d0939c31e043181e03bae5d5177 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 23 Mar 2026 17:53:07 +0100 Subject: [PATCH 1/5] feat(opentelemetry-sdk): add HistogramAggregation resolution --- opentelemetry-sdk/src/metrics/exporter.rs | 5 +- .../src/metrics/in_memory_exporter.rs | 6 ++- opentelemetry-sdk/src/metrics/mod.rs | 29 ++++++++++++ .../src/metrics/periodic_reader.rs | 20 +++++++- .../periodic_reader_with_async_runtime.rs | 5 ++ opentelemetry-sdk/src/metrics/pipeline.rs | 46 +++++++++++++------ opentelemetry-sdk/src/metrics/reader.rs | 13 +++++- 7 files changed, 106 insertions(+), 18 deletions(-) diff --git a/opentelemetry-sdk/src/metrics/exporter.rs b/opentelemetry-sdk/src/metrics/exporter.rs index c1280db7be..0b79850325 100644 --- a/opentelemetry-sdk/src/metrics/exporter.rs +++ b/opentelemetry-sdk/src/metrics/exporter.rs @@ -5,7 +5,7 @@ use std::time::Duration; use crate::metrics::data::ResourceMetrics; -use super::Temporality; +use super::{HistogramAggregation, Temporality}; /// Exporter handles the delivery of metric data to external receivers. /// @@ -37,4 +37,7 @@ pub trait PushMetricExporter: Send + Sync + 'static { /// Access the [Temporality] of the MetricExporter. fn temporality(&self) -> Temporality; + + /// The default aggregation to use for histogram instruments. + fn default_histogram_aggregation(&self) -> HistogramAggregation; } diff --git a/opentelemetry-sdk/src/metrics/in_memory_exporter.rs b/opentelemetry-sdk/src/metrics/in_memory_exporter.rs index 903c2a46ec..6ea3672e3d 100644 --- a/opentelemetry-sdk/src/metrics/in_memory_exporter.rs +++ b/opentelemetry-sdk/src/metrics/in_memory_exporter.rs @@ -3,7 +3,7 @@ use crate::metrics::data::{ ExponentialHistogram, Gauge, Histogram, MetricData, ResourceMetrics, Sum, }; use crate::metrics::exporter::PushMetricExporter; -use crate::metrics::Temporality; +use crate::metrics::{HistogramAggregation, Temporality}; use crate::InMemoryExporterError; use std::collections::VecDeque; use std::fmt; @@ -261,4 +261,8 @@ impl PushMetricExporter for InMemoryMetricExporter { fn temporality(&self) -> Temporality { self.temporality } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + HistogramAggregation::ExplicitBucketHistogram + } } diff --git a/opentelemetry-sdk/src/metrics/mod.rs b/opentelemetry-sdk/src/metrics/mod.rs index 2d358be73b..9ebbae46ad 100644 --- a/opentelemetry-sdk/src/metrics/mod.rs +++ b/opentelemetry-sdk/src/metrics/mod.rs @@ -120,6 +120,35 @@ impl FromStr for Temporality { } } +/// The default histogram aggregation selection for a [`exporter::PushMetricExporter`]. +/// +/// This controls which aggregation type is used for histogram instruments +/// when no explicit aggregation is configured via a view. +/// +/// Corresponds to the `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` +/// environment variable. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum HistogramAggregation { + /// Use explicit bucket histogram aggregation with default boundaries. + #[default] + ExplicitBucketHistogram, + + /// Use base2 exponential bucket histogram aggregation. + Base2ExponentialBucketHistogram, +} + +impl FromStr for HistogramAggregation { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "explicit_bucket_histogram" => Ok(Self::ExplicitBucketHistogram), + "base2_exponential_bucket_histogram" => Ok(Self::Base2ExponentialBucketHistogram), + _ => Err(()), + } + } +} + #[cfg(all(test, feature = "testing"))] mod tests { use self::data::{HistogramDataPoint, MetricData, ScopeMetrics, SumDataPoint}; diff --git a/opentelemetry-sdk/src/metrics/periodic_reader.rs b/opentelemetry-sdk/src/metrics/periodic_reader.rs index 70e78f9253..d4e76b2ddd 100644 --- a/opentelemetry-sdk/src/metrics/periodic_reader.rs +++ b/opentelemetry-sdk/src/metrics/periodic_reader.rs @@ -18,7 +18,7 @@ use crate::{ use super::{ data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader, - Temporality, + HistogramAggregation, Temporality, }; const DEFAULT_INTERVAL: Duration = Duration::from_secs(60); @@ -363,6 +363,10 @@ impl PeriodicReaderInner { self.exporter.temporality() } + fn default_histogram_aggregation(&self) -> HistogramAggregation { + self.exporter.default_histogram_aggregation() + } + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { let producer = self.producer.lock().expect("lock poisoned"); if let Some(p) = producer.as_ref() { @@ -506,6 +510,10 @@ impl MetricReader for PeriodicReader { fn temporality(&self, kind: InstrumentKind) -> Temporality { kind.temporality_preference(self.inner.temporality(kind)) } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + self.inner.default_histogram_aggregation() + } } #[cfg(all(test, feature = "testing"))] @@ -515,7 +523,7 @@ mod tests { error::{OTelSdkError, OTelSdkResult}, metrics::{ data::ResourceMetrics, exporter::PushMetricExporter, reader::MetricReader, - InMemoryMetricExporter, SdkMeterProvider, Temporality, + HistogramAggregation, InMemoryMetricExporter, SdkMeterProvider, Temporality, }, Resource, }; @@ -574,6 +582,10 @@ mod tests { fn temporality(&self) -> Temporality { Temporality::Cumulative } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + HistogramAggregation::ExplicitBucketHistogram + } } #[derive(Debug, Clone, Default)] @@ -602,6 +614,10 @@ mod tests { fn temporality(&self) -> Temporality { Temporality::Cumulative } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + HistogramAggregation::ExplicitBucketHistogram + } } #[test] diff --git a/opentelemetry-sdk/src/metrics/periodic_reader_with_async_runtime.rs b/opentelemetry-sdk/src/metrics/periodic_reader_with_async_runtime.rs index 066db5b340..26e6b20894 100644 --- a/opentelemetry-sdk/src/metrics/periodic_reader_with_async_runtime.rs +++ b/opentelemetry-sdk/src/metrics/periodic_reader_with_async_runtime.rs @@ -22,6 +22,7 @@ use crate::{ use super::{ data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader, + HistogramAggregation, }; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); @@ -437,6 +438,10 @@ impl MetricReader for PeriodicReader { fn temporality(&self, kind: InstrumentKind) -> super::Temporality { kind.temporality_preference(self.exporter.temporality()) } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + self.exporter.default_histogram_aggregation() + } } #[cfg(all(test, feature = "testing"))] diff --git a/opentelemetry-sdk/src/metrics/pipeline.rs b/opentelemetry-sdk/src/metrics/pipeline.rs index cf08027462..97f805102c 100644 --- a/opentelemetry-sdk/src/metrics/pipeline.rs +++ b/opentelemetry-sdk/src/metrics/pipeline.rs @@ -23,7 +23,7 @@ use crate::{ use self::internal::AggregateFns; -use super::{aggregation::Aggregation, Temporality}; +use super::{aggregation::Aggregation, HistogramAggregation, Temporality}; /// Connects all of the instruments created by a meter provider to a [MetricReader]. /// @@ -378,14 +378,15 @@ where // TODO: Create a separate pub (crate) Stream struct for the pipeline, // as Stream will not have any optional fields as None at this point and // new struct can better reflect this. + let histogram_agg = self.pipeline.reader.default_histogram_aggregation(); let mut agg = stream .aggregation .take() - .unwrap_or_else(|| default_aggregation_selector(kind)); + .unwrap_or_else(|| default_aggregation_selector(kind, histogram_agg)); // Apply default if stream or reader aggregation returns default if matches!(agg, aggregation::Aggregation::Default) { - agg = default_aggregation_selector(kind); + agg = default_aggregation_selector(kind, histogram_agg); } if let Err(err) = is_aggregator_compatible(&kind, &agg) { @@ -421,7 +422,8 @@ where filter, cardinality_limit, ); - let AggregateFns { measure, collect } = match aggregate_fn(b, &agg, kind) { + let AggregateFns { measure, collect } = match aggregate_fn(b, &agg, kind, histogram_agg) + { Ok(Some(inst)) => inst, other => return other.map(|fs| fs.map(|inst| inst.measure)), // Drop aggregator or error }; @@ -497,10 +499,13 @@ where /// * Observable UpDownCounter ⇨ Sum /// * Gauge ⇨ LastValue /// * Observable Gauge ⇨ LastValue -/// * Histogram ⇨ ExplicitBucketHistogram +/// * Histogram ⇨ determined by `histogram_agg` /// /// [the spec]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.19.0/specification/metrics/sdk.md#default-aggregation -fn default_aggregation_selector(kind: InstrumentKind) -> Aggregation { +fn default_aggregation_selector( + kind: InstrumentKind, + histogram_agg: HistogramAggregation, +) -> Aggregation { match kind { InstrumentKind::Counter | InstrumentKind::UpDownCounter @@ -508,12 +513,21 @@ fn default_aggregation_selector(kind: InstrumentKind) -> Aggregation { | InstrumentKind::ObservableUpDownCounter => Aggregation::Sum, InstrumentKind::Gauge => Aggregation::LastValue, InstrumentKind::ObservableGauge => Aggregation::LastValue, - InstrumentKind::Histogram => Aggregation::ExplicitBucketHistogram { - boundaries: vec![ - 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, - 5000.0, 7500.0, 10000.0, - ], - record_min_max: true, + InstrumentKind::Histogram => match histogram_agg { + HistogramAggregation::ExplicitBucketHistogram => Aggregation::ExplicitBucketHistogram { + boundaries: vec![ + 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, + 5000.0, 7500.0, 10000.0, + ], + record_min_max: true, + }, + HistogramAggregation::Base2ExponentialBucketHistogram => { + Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + } + } }, } } @@ -525,9 +539,15 @@ fn aggregate_fn( b: AggregateBuilder, agg: &aggregation::Aggregation, kind: InstrumentKind, + histogram_agg: HistogramAggregation, ) -> MetricResult>> { match agg { - Aggregation::Default => aggregate_fn(b, &default_aggregation_selector(kind), kind), + Aggregation::Default => aggregate_fn( + b, + &default_aggregation_selector(kind, histogram_agg), + kind, + histogram_agg, + ), Aggregation::Drop => Ok(None), Aggregation::LastValue => { match kind { diff --git a/opentelemetry-sdk/src/metrics/reader.rs b/opentelemetry-sdk/src/metrics/reader.rs index ae19841155..792e49efde 100644 --- a/opentelemetry-sdk/src/metrics/reader.rs +++ b/opentelemetry-sdk/src/metrics/reader.rs @@ -3,7 +3,10 @@ use crate::error::OTelSdkResult; use std::time::Duration; use std::{fmt, sync::Weak}; -use super::{data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, Temporality}; +use super::{ + data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, HistogramAggregation, + Temporality, +}; /// The interface used between the SDK and an exporter. /// @@ -58,6 +61,14 @@ pub trait MetricReader: fmt::Debug + Send + Sync + 'static { /// /// If not configured, the Cumulative temporality SHOULD be used. fn temporality(&self, kind: InstrumentKind) -> Temporality; + + /// The default histogram aggregation. + /// This SHOULD be obtained from the exporter. + /// + /// If not configured, [`HistogramAggregation::ExplicitBucketHistogram`] is used. + fn default_histogram_aggregation(&self) -> HistogramAggregation { + HistogramAggregation::default() + } } /// Produces metrics for a [MetricReader]. From b8ef12de9802bdfa7e0ee4c76b890b83d1d2d9f1 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 23 Mar 2026 17:53:23 +0100 Subject: [PATCH 2/5] feat(opentelemetry-otlp): support histogram aggregation configuration --- opentelemetry-otlp/src/exporter/http/mod.rs | 7 +- opentelemetry-otlp/src/exporter/tonic/mod.rs | 7 +- opentelemetry-otlp/src/lib.rs | 6 +- opentelemetry-otlp/src/metric.rs | 167 ++++++++++++++++++- 4 files changed, 180 insertions(+), 7 deletions(-) diff --git a/opentelemetry-otlp/src/exporter/http/mod.rs b/opentelemetry-otlp/src/exporter/http/mod.rs index 2bad5292f1..1b858ae758 100644 --- a/opentelemetry-otlp/src/exporter/http/mod.rs +++ b/opentelemetry-otlp/src/exporter/http/mod.rs @@ -342,6 +342,7 @@ impl HttpExporterBuilder { pub fn build_metrics_exporter( mut self, temporality: opentelemetry_sdk::metrics::Temporality, + histogram_aggregation: opentelemetry_sdk::metrics::HistogramAggregation, ) -> Result { use crate::{ OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, @@ -356,7 +357,11 @@ impl HttpExporterBuilder { OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, )?; - Ok(crate::MetricExporter::from_http(client, temporality)) + Ok(crate::MetricExporter::from_http( + client, + temporality, + histogram_aggregation, + )) } } diff --git a/opentelemetry-otlp/src/exporter/tonic/mod.rs b/opentelemetry-otlp/src/exporter/tonic/mod.rs index 60336eaf85..cb15bf0f40 100644 --- a/opentelemetry-otlp/src/exporter/tonic/mod.rs +++ b/opentelemetry-otlp/src/exporter/tonic/mod.rs @@ -367,6 +367,7 @@ impl TonicExporterBuilder { pub(crate) fn build_metrics_exporter( self, temporality: opentelemetry_sdk::metrics::Temporality, + histogram_aggregation: opentelemetry_sdk::metrics::HistogramAggregation, ) -> Result { use crate::MetricExporter; use metrics::TonicMetricsClient; @@ -382,7 +383,11 @@ impl TonicExporterBuilder { let client = TonicMetricsClient::new(channel, interceptor, compression, retry_policy); - Ok(MetricExporter::from_tonic(client, temporality)) + Ok(MetricExporter::from_tonic( + client, + temporality, + histogram_aggregation, + )) } /// Build a new tonic span exporter diff --git a/opentelemetry-otlp/src/lib.rs b/opentelemetry-otlp/src/lib.rs index 1ebfedf79d..474ea3df5e 100644 --- a/opentelemetry-otlp/src/lib.rs +++ b/opentelemetry-otlp/src/lib.rs @@ -243,6 +243,7 @@ //! | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | Signal-specific headers for metrics exports. | //! | `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` | Signal-specific compression for metrics exports. | //! | `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | Temporality preference for metrics. Valid values: `cumulative`, `delta`, `lowmemory` (case-insensitive). | `cumulative` | +//! | `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` | Default histogram aggregation. Valid values: `explicit_bucket_histogram`, `base2_exponential_bucket_histogram` (case-insensitive). | `explicit_bucket_histogram` | //! //! ## Logs //! @@ -659,8 +660,9 @@ pub use crate::span::{ #[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] pub use crate::metric::{ MetricExporter, MetricExporterBuilder, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, }; #[cfg(feature = "logs")] diff --git a/opentelemetry-otlp/src/metric.rs b/opentelemetry-otlp/src/metric.rs index 483bbc32d2..03b06312b8 100644 --- a/opentelemetry-otlp/src/metric.rs +++ b/opentelemetry-otlp/src/metric.rs @@ -24,7 +24,7 @@ use core::fmt; use opentelemetry_sdk::error::OTelSdkResult; use opentelemetry_sdk::metrics::{ - data::ResourceMetrics, exporter::PushMetricExporter, Temporality, + data::ResourceMetrics, exporter::PushMetricExporter, HistogramAggregation, Temporality, }; use std::fmt::{Debug, Formatter}; use std::time::Duration; @@ -45,12 +45,18 @@ pub const OTEL_EXPORTER_OTLP_METRICS_HEADERS: &str = "OTEL_EXPORTER_OTLP_METRICS /// Temporality preference for metrics, defaults to cumulative. pub const OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: &str = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; +/// Default histogram aggregation for metrics. +/// Valid values: `explicit_bucket_histogram`, `base2_exponential_bucket_histogram` (case-insensitive). +/// Defaults to `explicit_bucket_histogram`. +pub const OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: &str = + "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION"; /// A builder for creating a new [MetricExporter]. #[derive(Debug, Default, Clone)] pub struct MetricExporterBuilder { client: C, temporality: Option, + histogram_aggregation: Option, } impl MetricExporterBuilder { @@ -89,6 +95,7 @@ impl MetricExporterBuilder { MetricExporterBuilder { client: TonicExporterBuilderSet(TonicExporterBuilder::default()), temporality: self.temporality, + histogram_aggregation: self.histogram_aggregation, } } @@ -98,6 +105,7 @@ impl MetricExporterBuilder { MetricExporterBuilder { client: HttpExporterBuilderSet(HttpExporterBuilder::default()), temporality: self.temporality, + histogram_aggregation: self.histogram_aggregation, } } @@ -108,6 +116,25 @@ impl MetricExporterBuilder { MetricExporterBuilder { client: self.client, temporality: Some(temporality), + histogram_aggregation: self.histogram_aggregation, + } + } + + /// Set the default histogram aggregation for the metrics. + /// + /// Valid values: [HistogramAggregation::ExplicitBucketHistogram] (default), + /// [HistogramAggregation::Base2ExponentialBucketHistogram]. + /// + /// Note: Programmatically setting this will override any value set via the + /// `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` environment variable. + pub fn with_histogram_aggregation( + self, + histogram_aggregation: HistogramAggregation, + ) -> MetricExporterBuilder { + MetricExporterBuilder { + client: self.client, + temporality: self.temporality, + histogram_aggregation: Some(histogram_aggregation), } } } @@ -132,12 +159,40 @@ fn resolve_temporality(provided: Option) -> Result, +) -> Result { + if let Some(histogram_aggregation) = provided { + return Ok(histogram_aggregation); + } + if let Ok(val) = std::env::var(OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION) { + return val + .parse::() + .map_err(|_| ExporterBuildError::InvalidConfig { + name: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION.to_string(), + reason: format!( + "Invalid value '{val}'. Expected: explicit_bucket_histogram or base2_exponential_bucket_histogram" + ), + }); + } + Ok(HistogramAggregation::default()) +} + #[cfg(feature = "grpc-tonic")] impl MetricExporterBuilder { /// Build the [MetricExporter] with the gRPC Tonic transport. pub fn build(self) -> Result { let temporality = resolve_temporality(self.temporality)?; - let exporter = self.client.0.build_metrics_exporter(temporality)?; + let histogram_aggregation = resolve_histogram_aggregation(self.histogram_aggregation)?; + let exporter = self + .client + .0 + .build_metrics_exporter(temporality, histogram_aggregation)?; opentelemetry::otel_debug!(name: "MetricExporterBuilt"); Ok(exporter) } @@ -148,7 +203,11 @@ impl MetricExporterBuilder { /// Build the [MetricExporter] with the HTTP transport. pub fn build(self) -> Result { let temporality = resolve_temporality(self.temporality)?; - let exporter = self.client.0.build_metrics_exporter(temporality)?; + let histogram_aggregation = resolve_histogram_aggregation(self.histogram_aggregation)?; + let exporter = self + .client + .0 + .build_metrics_exporter(temporality, histogram_aggregation)?; Ok(exporter) } } @@ -194,6 +253,7 @@ pub(crate) trait MetricsClient: fmt::Debug + Send + Sync + 'static { pub struct MetricExporter { client: SupportedTransportClient, temporality: Temporality, + histogram_aggregation: HistogramAggregation, } #[derive(Debug)] @@ -241,6 +301,10 @@ impl PushMetricExporter for MetricExporter { fn temporality(&self) -> Temporality { self.temporality } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + self.histogram_aggregation + } } impl MetricExporter { @@ -253,10 +317,12 @@ impl MetricExporter { pub(crate) fn from_tonic( client: crate::exporter::tonic::metrics::TonicMetricsClient, temporality: Temporality, + histogram_aggregation: HistogramAggregation, ) -> Self { Self { client: SupportedTransportClient::Tonic(client), temporality, + histogram_aggregation, } } @@ -264,10 +330,12 @@ impl MetricExporter { pub(crate) fn from_http( client: crate::exporter::http::OtlpHttpClient, temporality: Temporality, + histogram_aggregation: HistogramAggregation, ) -> Self { Self { client: SupportedTransportClient::Http(client), temporality, + histogram_aggregation, } } } @@ -373,4 +441,97 @@ mod tests { assert_eq!(result, Temporality::Cumulative); }); } + + #[test] + fn histogram_aggregation_code_config_overrides_env_var() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "explicit_bucket_histogram", + )], + || { + let result = resolve_histogram_aggregation(Some( + HistogramAggregation::Base2ExponentialBucketHistogram, + )) + .unwrap(); + assert_eq!( + result, + HistogramAggregation::Base2ExponentialBucketHistogram + ); + }, + ); + } + + #[test] + fn histogram_aggregation_env_var_sets_explicit_bucket() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "explicit_bucket_histogram", + )], + || { + let result = resolve_histogram_aggregation(None).unwrap(); + assert_eq!(result, HistogramAggregation::ExplicitBucketHistogram); + }, + ); + } + + #[test] + fn histogram_aggregation_env_var_sets_base2_exponential() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "base2_exponential_bucket_histogram", + )], + || { + let result = resolve_histogram_aggregation(None).unwrap(); + assert_eq!( + result, + HistogramAggregation::Base2ExponentialBucketHistogram + ); + }, + ); + } + + #[test] + fn histogram_aggregation_env_var_case_insensitive() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "BASE2_EXPONENTIAL_BUCKET_HISTOGRAM", + )], + || { + let result = resolve_histogram_aggregation(None).unwrap(); + assert_eq!( + result, + HistogramAggregation::Base2ExponentialBucketHistogram + ); + }, + ); + } + + #[test] + fn histogram_aggregation_invalid_env_var_returns_error() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + "invalid", + )], + || { + let result = resolve_histogram_aggregation(None); + assert!(result.is_err()); + }, + ); + } + + #[test] + fn histogram_aggregation_default_when_nothing_set() { + temp_env::with_var_unset( + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, + || { + let result = resolve_histogram_aggregation(None).unwrap(); + assert_eq!(result, HistogramAggregation::ExplicitBucketHistogram); + }, + ); + } } From 8a440a47a41a2d6819d8be677609505184918fcb Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Mon, 23 Mar 2026 17:54:40 +0100 Subject: [PATCH 3/5] feat(opentelemetry-stdout): support histogram aggregation --- opentelemetry-stdout/src/metrics/exporter.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/opentelemetry-stdout/src/metrics/exporter.rs b/opentelemetry-stdout/src/metrics/exporter.rs index 570c5c2e1a..146bfb9165 100644 --- a/opentelemetry-stdout/src/metrics/exporter.rs +++ b/opentelemetry-stdout/src/metrics/exporter.rs @@ -10,6 +10,7 @@ use opentelemetry_sdk::{ HistogramDataPoint, ResourceMetrics, ScopeMetrics, Sum, SumDataPoint, }, exporter::PushMetricExporter, + HistogramAggregation, }, }; use std::fmt::Debug; @@ -20,6 +21,7 @@ use std::time::Duration; pub struct MetricExporter { is_shutdown: atomic::AtomicBool, temporality: Temporality, + histogram_aggregation: HistogramAggregation, } impl MetricExporter { @@ -77,6 +79,10 @@ impl PushMetricExporter for MetricExporter { fn temporality(&self) -> Temporality { self.temporality } + + fn default_histogram_aggregation(&self) -> HistogramAggregation { + self.histogram_aggregation + } } fn print_metrics<'a>(metrics: impl Iterator) { @@ -354,6 +360,7 @@ fn print_exponential_hist_data_points<'a, T: Debug + Copy + 'a>( #[derive(Default)] pub struct MetricExporterBuilder { temporality: Option, + histogram_aggregation: Option, } impl MetricExporterBuilder { @@ -363,10 +370,20 @@ impl MetricExporterBuilder { self } + /// Set the default histogram aggregation of the exporter. + pub fn with_histogram_aggregation( + mut self, + histogram_aggregation: HistogramAggregation, + ) -> Self { + self.histogram_aggregation = Some(histogram_aggregation); + self + } + /// Create a metrics exporter with the current configuration pub fn build(self) -> MetricExporter { MetricExporter { temporality: self.temporality.unwrap_or_default(), + histogram_aggregation: self.histogram_aggregation.unwrap_or_default(), is_shutdown: atomic::AtomicBool::new(false), } } From 0fe6b491e3798e0dd31299431dd9350355fb73bb Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Thu, 30 Apr 2026 22:24:17 +0200 Subject: [PATCH 4/5] feat: add default impl --- opentelemetry-sdk/src/metrics/exporter.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/metrics/exporter.rs b/opentelemetry-sdk/src/metrics/exporter.rs index 0b79850325..51e0ccd118 100644 --- a/opentelemetry-sdk/src/metrics/exporter.rs +++ b/opentelemetry-sdk/src/metrics/exporter.rs @@ -39,5 +39,7 @@ pub trait PushMetricExporter: Send + Sync + 'static { fn temporality(&self) -> Temporality; /// The default aggregation to use for histogram instruments. - fn default_histogram_aggregation(&self) -> HistogramAggregation; + fn default_histogram_aggregation(&self) -> HistogramAggregation { + HistogramAggregation::default() + } } From 0e049dae93746a7fb804abe99284c82c857fc047 Mon Sep 17 00:00:00 2001 From: Mike Beaumont Date: Thu, 30 Apr 2026 22:27:49 +0200 Subject: [PATCH 5/5] chore: add CHANGELOG --- opentelemetry-otlp/CHANGELOG.md | 1 + opentelemetry-sdk/CHANGELOG.md | 1 + opentelemetry-stdout/CHANGELOG.md | 1 + 3 files changed, 3 insertions(+) diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md index 541a98219f..79473bcdaf 100644 --- a/opentelemetry-otlp/CHANGELOG.md +++ b/opentelemetry-otlp/CHANGELOG.md @@ -29,6 +29,7 @@ - Fixed [#2777](https://github.com/open-telemetry/opentelemetry rust/issues/2777) to properly handle `shutdown_with_timeout()` when using `grpc-tonic`. - Deprecate `tls` feature in favor of explicit `tls-ring` and `tls-aws-lc` features. **Migration**: Replace `tls` with `tls-ring` (or `tls-aws-lc`). Users of `tls-roots` or `tls-webpki-roots` must now also enable one of these. +- Support `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` ([#3433][3433]). ## 0.31.0 diff --git a/opentelemetry-sdk/CHANGELOG.md b/opentelemetry-sdk/CHANGELOG.md index 3f89175434..699c240124 100644 --- a/opentelemetry-sdk/CHANGELOG.md +++ b/opentelemetry-sdk/CHANGELOG.md @@ -26,6 +26,7 @@ - Fix panic when `SpanProcessor::on_end` calls `Context::current()` ([#3262][3262]). - Updated `SpanProcessor::on_end` documentation to clarify that `Context::current()` returns the parent context, not the span's context - Fix `traceparent` headers with unknown flags (e.g. W3C random-trace-id flag `0x02`) being incorrectly rejected. Unknown flags are now accepted and zeroed out as required by the W3C trace-context spec. [#3435][3435] +- Support `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` ([#3433][3433]). [3227]: https://github.com/open-telemetry/opentelemetry-rust/pull/3227 [3277]: https://github.com/open-telemetry/opentelemetry-rust/pull/3277 diff --git a/opentelemetry-stdout/CHANGELOG.md b/opentelemetry-stdout/CHANGELOG.md index a32f2b1c8b..2d84b833af 100644 --- a/opentelemetry-stdout/CHANGELOG.md +++ b/opentelemetry-stdout/CHANGELOG.md @@ -3,6 +3,7 @@ ## vNext - ExponentialHistogram supported in stdout +- Support `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` ([#3433][3433]). ## 0.31.0