Skip to content

Commit 4203559

Browse files
feat(opentelemetry-otlp): support histogram aggregation configuration
1 parent ec0c6c7 commit 4203559

4 files changed

Lines changed: 180 additions & 7 deletions

File tree

opentelemetry-otlp/src/exporter/http/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ impl HttpExporterBuilder {
342342
pub fn build_metrics_exporter(
343343
mut self,
344344
temporality: opentelemetry_sdk::metrics::Temporality,
345+
histogram_aggregation: opentelemetry_sdk::metrics::HistogramAggregation,
345346
) -> Result<crate::MetricExporter, ExporterBuildError> {
346347
use crate::{
347348
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
@@ -356,7 +357,11 @@ impl HttpExporterBuilder {
356357
OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
357358
)?;
358359

359-
Ok(crate::MetricExporter::from_http(client, temporality))
360+
Ok(crate::MetricExporter::from_http(
361+
client,
362+
temporality,
363+
histogram_aggregation,
364+
))
360365
}
361366
}
362367

opentelemetry-otlp/src/exporter/tonic/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ impl TonicExporterBuilder {
367367
pub(crate) fn build_metrics_exporter(
368368
self,
369369
temporality: opentelemetry_sdk::metrics::Temporality,
370+
histogram_aggregation: opentelemetry_sdk::metrics::HistogramAggregation,
370371
) -> Result<crate::MetricExporter, ExporterBuildError> {
371372
use crate::MetricExporter;
372373
use metrics::TonicMetricsClient;
@@ -382,7 +383,11 @@ impl TonicExporterBuilder {
382383

383384
let client = TonicMetricsClient::new(channel, interceptor, compression, retry_policy);
384385

385-
Ok(MetricExporter::from_tonic(client, temporality))
386+
Ok(MetricExporter::from_tonic(
387+
client,
388+
temporality,
389+
histogram_aggregation,
390+
))
386391
}
387392

388393
/// Build a new tonic span exporter

opentelemetry-otlp/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
//! | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | Signal-specific headers for metrics exports. |
244244
//! | `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` | Signal-specific compression for metrics exports. |
245245
//! | `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | Temporality preference for metrics. Valid values: `cumulative`, `delta`, `lowmemory` (case-insensitive). | `cumulative` |
246+
//! | `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` | Default histogram aggregation. Valid values: `explicit_bucket_histogram`, `base2_exponential_bucket_histogram` (case-insensitive). | `explicit_bucket_histogram` |
246247
//!
247248
//! ## Logs
248249
//!
@@ -659,8 +660,9 @@ pub use crate::span::{
659660
#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))]
660661
pub use crate::metric::{
661662
MetricExporter, MetricExporterBuilder, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION,
662-
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS,
663-
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
663+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
664+
OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE,
665+
OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
664666
};
665667

666668
#[cfg(feature = "logs")]

opentelemetry-otlp/src/metric.rs

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use core::fmt;
2424
use opentelemetry_sdk::error::OTelSdkResult;
2525

2626
use opentelemetry_sdk::metrics::{
27-
data::ResourceMetrics, exporter::PushMetricExporter, Temporality,
27+
data::ResourceMetrics, exporter::PushMetricExporter, HistogramAggregation, Temporality,
2828
};
2929
use std::fmt::{Debug, Formatter};
3030
use std::time::Duration;
@@ -45,12 +45,18 @@ pub const OTEL_EXPORTER_OTLP_METRICS_HEADERS: &str = "OTEL_EXPORTER_OTLP_METRICS
4545
/// Temporality preference for metrics, defaults to cumulative.
4646
pub const OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: &str =
4747
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE";
48+
/// Default histogram aggregation for metrics.
49+
/// Valid values: `explicit_bucket_histogram`, `base2_exponential_bucket_histogram` (case-insensitive).
50+
/// Defaults to `explicit_bucket_histogram`.
51+
pub const OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: &str =
52+
"OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION";
4853

4954
/// A builder for creating a new [MetricExporter].
5055
#[derive(Debug, Default, Clone)]
5156
pub struct MetricExporterBuilder<C> {
5257
client: C,
5358
temporality: Option<Temporality>,
59+
histogram_aggregation: Option<HistogramAggregation>,
5460
}
5561

5662
impl MetricExporterBuilder<NoExporterBuilderSet> {
@@ -89,6 +95,7 @@ impl<C> MetricExporterBuilder<C> {
8995
MetricExporterBuilder {
9096
client: TonicExporterBuilderSet(TonicExporterBuilder::default()),
9197
temporality: self.temporality,
98+
histogram_aggregation: self.histogram_aggregation,
9299
}
93100
}
94101

@@ -98,6 +105,7 @@ impl<C> MetricExporterBuilder<C> {
98105
MetricExporterBuilder {
99106
client: HttpExporterBuilderSet(HttpExporterBuilder::default()),
100107
temporality: self.temporality,
108+
histogram_aggregation: self.histogram_aggregation,
101109
}
102110
}
103111

@@ -108,6 +116,25 @@ impl<C> MetricExporterBuilder<C> {
108116
MetricExporterBuilder {
109117
client: self.client,
110118
temporality: Some(temporality),
119+
histogram_aggregation: self.histogram_aggregation,
120+
}
121+
}
122+
123+
/// Set the default histogram aggregation for the metrics.
124+
///
125+
/// Valid values: [HistogramAggregation::ExplicitBucketHistogram] (default),
126+
/// [HistogramAggregation::Base2ExponentialBucketHistogram].
127+
///
128+
/// Note: Programmatically setting this will override any value set via the
129+
/// `OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION` environment variable.
130+
pub fn with_histogram_aggregation(
131+
self,
132+
histogram_aggregation: HistogramAggregation,
133+
) -> MetricExporterBuilder<C> {
134+
MetricExporterBuilder {
135+
client: self.client,
136+
temporality: self.temporality,
137+
histogram_aggregation: Some(histogram_aggregation),
111138
}
112139
}
113140
}
@@ -132,12 +159,40 @@ fn resolve_temporality(provided: Option<Temporality>) -> Result<Temporality, Exp
132159
Ok(Temporality::default())
133160
}
134161

162+
/// Resolve histogram aggregation with priority:
163+
/// 1. Provided config value
164+
/// 2. OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION environment variable
165+
/// 3. Default (ExplicitBucketHistogram)
166+
#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))]
167+
fn resolve_histogram_aggregation(
168+
provided: Option<HistogramAggregation>,
169+
) -> Result<HistogramAggregation, ExporterBuildError> {
170+
if let Some(histogram_aggregation) = provided {
171+
return Ok(histogram_aggregation);
172+
}
173+
if let Ok(val) = std::env::var(OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION) {
174+
return val
175+
.parse::<HistogramAggregation>()
176+
.map_err(|_| ExporterBuildError::InvalidConfig {
177+
name: OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION.to_string(),
178+
reason: format!(
179+
"Invalid value '{val}'. Expected: explicit_bucket_histogram or base2_exponential_bucket_histogram"
180+
),
181+
});
182+
}
183+
Ok(HistogramAggregation::default())
184+
}
185+
135186
#[cfg(feature = "grpc-tonic")]
136187
impl MetricExporterBuilder<TonicExporterBuilderSet> {
137188
/// Build the [MetricExporter] with the gRPC Tonic transport.
138189
pub fn build(self) -> Result<MetricExporter, ExporterBuildError> {
139190
let temporality = resolve_temporality(self.temporality)?;
140-
let exporter = self.client.0.build_metrics_exporter(temporality)?;
191+
let histogram_aggregation = resolve_histogram_aggregation(self.histogram_aggregation)?;
192+
let exporter = self
193+
.client
194+
.0
195+
.build_metrics_exporter(temporality, histogram_aggregation)?;
141196
opentelemetry::otel_debug!(name: "MetricExporterBuilt");
142197
Ok(exporter)
143198
}
@@ -148,7 +203,11 @@ impl MetricExporterBuilder<HttpExporterBuilderSet> {
148203
/// Build the [MetricExporter] with the HTTP transport.
149204
pub fn build(self) -> Result<MetricExporter, ExporterBuildError> {
150205
let temporality = resolve_temporality(self.temporality)?;
151-
let exporter = self.client.0.build_metrics_exporter(temporality)?;
206+
let histogram_aggregation = resolve_histogram_aggregation(self.histogram_aggregation)?;
207+
let exporter = self
208+
.client
209+
.0
210+
.build_metrics_exporter(temporality, histogram_aggregation)?;
152211
Ok(exporter)
153212
}
154213
}
@@ -194,6 +253,7 @@ pub(crate) trait MetricsClient: fmt::Debug + Send + Sync + 'static {
194253
pub struct MetricExporter {
195254
client: SupportedTransportClient,
196255
temporality: Temporality,
256+
histogram_aggregation: HistogramAggregation,
197257
}
198258

199259
#[derive(Debug)]
@@ -241,6 +301,10 @@ impl PushMetricExporter for MetricExporter {
241301
fn temporality(&self) -> Temporality {
242302
self.temporality
243303
}
304+
305+
fn default_histogram_aggregation(&self) -> HistogramAggregation {
306+
self.histogram_aggregation
307+
}
244308
}
245309

246310
impl MetricExporter {
@@ -253,21 +317,25 @@ impl MetricExporter {
253317
pub(crate) fn from_tonic(
254318
client: crate::exporter::tonic::metrics::TonicMetricsClient,
255319
temporality: Temporality,
320+
histogram_aggregation: HistogramAggregation,
256321
) -> Self {
257322
Self {
258323
client: SupportedTransportClient::Tonic(client),
259324
temporality,
325+
histogram_aggregation,
260326
}
261327
}
262328

263329
#[cfg(any(feature = "http-proto", feature = "http-json"))]
264330
pub(crate) fn from_http(
265331
client: crate::exporter::http::OtlpHttpClient,
266332
temporality: Temporality,
333+
histogram_aggregation: HistogramAggregation,
267334
) -> Self {
268335
Self {
269336
client: SupportedTransportClient::Http(client),
270337
temporality,
338+
histogram_aggregation,
271339
}
272340
}
273341
}
@@ -373,4 +441,97 @@ mod tests {
373441
assert_eq!(result, Temporality::Cumulative);
374442
});
375443
}
444+
445+
#[test]
446+
fn histogram_aggregation_code_config_overrides_env_var() {
447+
run_env_test(
448+
vec![(
449+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
450+
"explicit_bucket_histogram",
451+
)],
452+
|| {
453+
let result = resolve_histogram_aggregation(Some(
454+
HistogramAggregation::Base2ExponentialBucketHistogram,
455+
))
456+
.unwrap();
457+
assert_eq!(
458+
result,
459+
HistogramAggregation::Base2ExponentialBucketHistogram
460+
);
461+
},
462+
);
463+
}
464+
465+
#[test]
466+
fn histogram_aggregation_env_var_sets_explicit_bucket() {
467+
run_env_test(
468+
vec![(
469+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
470+
"explicit_bucket_histogram",
471+
)],
472+
|| {
473+
let result = resolve_histogram_aggregation(None).unwrap();
474+
assert_eq!(result, HistogramAggregation::ExplicitBucketHistogram);
475+
},
476+
);
477+
}
478+
479+
#[test]
480+
fn histogram_aggregation_env_var_sets_base2_exponential() {
481+
run_env_test(
482+
vec![(
483+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
484+
"base2_exponential_bucket_histogram",
485+
)],
486+
|| {
487+
let result = resolve_histogram_aggregation(None).unwrap();
488+
assert_eq!(
489+
result,
490+
HistogramAggregation::Base2ExponentialBucketHistogram
491+
);
492+
},
493+
);
494+
}
495+
496+
#[test]
497+
fn histogram_aggregation_env_var_case_insensitive() {
498+
run_env_test(
499+
vec![(
500+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
501+
"BASE2_EXPONENTIAL_BUCKET_HISTOGRAM",
502+
)],
503+
|| {
504+
let result = resolve_histogram_aggregation(None).unwrap();
505+
assert_eq!(
506+
result,
507+
HistogramAggregation::Base2ExponentialBucketHistogram
508+
);
509+
},
510+
);
511+
}
512+
513+
#[test]
514+
fn histogram_aggregation_invalid_env_var_returns_error() {
515+
run_env_test(
516+
vec![(
517+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
518+
"invalid",
519+
)],
520+
|| {
521+
let result = resolve_histogram_aggregation(None);
522+
assert!(result.is_err());
523+
},
524+
);
525+
}
526+
527+
#[test]
528+
fn histogram_aggregation_default_when_nothing_set() {
529+
temp_env::with_var_unset(
530+
OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION,
531+
|| {
532+
let result = resolve_histogram_aggregation(None).unwrap();
533+
assert_eq!(result, HistogramAggregation::ExplicitBucketHistogram);
534+
},
535+
);
536+
}
376537
}

0 commit comments

Comments
 (0)