Skip to content

Commit 0e221c1

Browse files
cijothomasutpilla
andauthored
Bugfix - add validation for custom buckets provided for Histograms (#2351)
Co-authored-by: Utkarsh Umesan Pillai <[email protected]>
1 parent fa6e6cd commit 0e221c1

File tree

4 files changed

+89
-1
lines changed

4 files changed

+89
-1
lines changed

opentelemetry-sdk/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@
3232
- These methods are intended for log appenders. Keep the clone of the provider handle, instead of depending on above methods.
3333

3434

35+
- **Bug Fix:** Validates the `with_boundaries` bucket boundaries used in
36+
Histograms. The boundaries provided by the user must not contain `f64::NAN`,
37+
`f64::INFINITY` or `f64::NEG_INFINITY` and must be sorted in strictly
38+
increasing order, and contain no duplicates. Instruments will not record
39+
measurements if the boundaries are invalid.
40+
[#2351](https://github.com/open-telemetry/opentelemetry-rust/pull/2351)
41+
3542
## 0.27.0
3643

3744
Released 2024-Nov-11

opentelemetry-sdk/src/metrics/meter.rs

+39
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,23 @@ impl SdkMeter {
394394
return Histogram::new(Arc::new(NoopSyncInstrument::new()));
395395
}
396396

397+
if let Some(ref boundaries) = builder.boundaries {
398+
let validation_result = validate_bucket_boundaries(boundaries);
399+
if let Err(err) = validation_result {
400+
// TODO: Include the buckets too in the error message.
401+
// TODO: This validation is not done when Views are used to
402+
// provide boundaries, and that should be fixed.
403+
otel_error!(
404+
name: "InstrumentCreationFailed",
405+
meter_name = self.scope.name(),
406+
instrument_name = builder.name.as_ref(),
407+
message = "Measurements from this Histogram will be ignored.",
408+
reason = format!("{}", err)
409+
);
410+
return Histogram::new(Arc::new(NoopSyncInstrument::new()));
411+
}
412+
}
413+
397414
match resolver
398415
.lookup(
399416
InstrumentKind::Histogram,
@@ -533,6 +550,28 @@ fn validate_instrument_config(name: &str, unit: &Option<Cow<'static, str>>) -> M
533550
validate_instrument_name(name).and_then(|_| validate_instrument_unit(unit))
534551
}
535552

553+
fn validate_bucket_boundaries(boundaries: &[f64]) -> MetricResult<()> {
554+
// Validate boundaries do not contain f64::NAN, f64::INFINITY, or f64::NEG_INFINITY
555+
for boundary in boundaries {
556+
if boundary.is_nan() || boundary.is_infinite() {
557+
return Err(MetricError::InvalidInstrumentConfiguration(
558+
"Bucket boundaries must not contain NaN, +Inf, or -Inf",
559+
));
560+
}
561+
}
562+
563+
// validate that buckets are sorted and non-duplicate
564+
for i in 1..boundaries.len() {
565+
if boundaries[i] <= boundaries[i - 1] {
566+
return Err(MetricError::InvalidInstrumentConfiguration(
567+
"Bucket boundaries must be sorted and non-duplicate",
568+
));
569+
}
570+
}
571+
572+
Ok(())
573+
}
574+
536575
fn validate_instrument_name(name: &str) -> MetricResult<()> {
537576
if name.is_empty() {
538577
return Err(MetricError::InvalidInstrumentConfiguration(

opentelemetry-sdk/src/metrics/mod.rs

+23
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,29 @@ mod tests {
178178
// As instrument name is invalid, no metrics should be exported
179179
test_context.check_no_metrics();
180180
}
181+
182+
let invalid_bucket_boundaries = vec![
183+
vec![1.0, 1.0], // duplicate boundaries
184+
vec![1.0, 2.0, 3.0, 2.0], // duplicate non consequent boundaries
185+
vec![1.0, 2.0, 3.0, 4.0, 2.5], // unsorted boundaries
186+
vec![1.0, 2.0, 3.0, f64::INFINITY, 4.0], // boundaries with positive infinity
187+
vec![1.0, 2.0, 3.0, f64::NAN], // boundaries with NaNs
188+
vec![f64::NEG_INFINITY, 2.0, 3.0], // boundaries with negative infinity
189+
];
190+
for bucket_boundaries in invalid_bucket_boundaries {
191+
let test_context = TestContext::new(Temporality::Cumulative);
192+
let histogram = test_context
193+
.meter()
194+
.f64_histogram("test")
195+
.with_boundaries(bucket_boundaries)
196+
.build();
197+
histogram.record(1.9, &[]);
198+
test_context.flush_metrics();
199+
200+
// As bucket boundaires provided via advisory params are invalid, no
201+
// metrics should be exported
202+
test_context.check_no_metrics();
203+
}
181204
}
182205

183206
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]

opentelemetry/src/metrics/instruments/mod.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,26 @@ impl<'a, T> HistogramBuilder<'a, T> {
8787
///
8888
/// Setting boundaries is optional. By default, the boundaries are set to:
8989
///
90-
/// `[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]`
90+
/// `[0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0,
91+
/// 2500.0, 5000.0, 7500.0, 10000.0]`
92+
///
93+
/// # Notes
94+
/// - Boundaries must not contain `f64::NAN`, `f64::INFINITY` or
95+
/// `f64::NEG_INFINITY`
96+
/// - Values must be in strictly increasing order (e.g., each value must be
97+
/// greater than the previous).
98+
/// - Boundaries must not contain duplicate values.
99+
///
100+
/// If invalid boundaries are provided, the instrument will not report
101+
/// measurements.
102+
/// Providing an empty `vec![]` means no bucket information will be
103+
/// calculated.
104+
///
105+
/// # Warning
106+
/// Using more buckets can improve the accuracy of percentile calculations in backends.
107+
/// However, this comes at a cost, including increased memory, CPU, and network usage.
108+
/// Choose the number of buckets carefully, considering your application's performance
109+
/// and resource requirements.
91110
pub fn with_boundaries(mut self, boundaries: Vec<f64>) -> Self {
92111
self.boundaries = Some(boundaries);
93112
self

0 commit comments

Comments
 (0)