Skip to content

Commit c8136d9

Browse files
utpillacijothomas
andauthored
Add with_boundaries hint API for explicit bucket histograms (#2135)
Co-authored-by: Cijo Thomas <[email protected]>
1 parent b244673 commit c8136d9

File tree

6 files changed

+116
-6
lines changed

6 files changed

+116
-6
lines changed

examples/metrics-basic/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use opentelemetry::KeyValue;
33
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
44
use opentelemetry_sdk::{runtime, Resource};
55
use std::error::Error;
6+
use std::vec;
67

78
fn init_meter_provider() -> opentelemetry_sdk::metrics::SdkMeterProvider {
89
let exporter = opentelemetry_stdout::MetricsExporterBuilder::default()
@@ -90,6 +91,9 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
9091
let histogram = meter
9192
.f64_histogram("my_histogram")
9293
.with_description("My histogram example description")
94+
// Setting boundaries is optional. By default, the boundaries are set to
95+
// [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]
96+
.with_boundaries(vec![0.0, 5.0, 10.0, 15.0, 20.0, 25.0])
9397
.init();
9498

9599
// Record measurements using the histogram instrument.

opentelemetry-sdk/src/metrics/meter.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ impl InstrumentProvider for SdkMeter {
8484
builder.name,
8585
builder.description,
8686
builder.unit,
87+
None,
8788
)
8889
.map(|i| Counter::new(Arc::new(i)))
8990
}
@@ -96,6 +97,7 @@ impl InstrumentProvider for SdkMeter {
9697
builder.name,
9798
builder.description,
9899
builder.unit,
100+
None,
99101
)
100102
.map(|i| Counter::new(Arc::new(i)))
101103
}
@@ -111,6 +113,7 @@ impl InstrumentProvider for SdkMeter {
111113
builder.name,
112114
builder.description,
113115
builder.unit,
116+
None,
114117
)?;
115118
if ms.is_empty() {
116119
return Ok(ObservableCounter::new(Arc::new(NoopAsyncInstrument::new())));
@@ -138,6 +141,7 @@ impl InstrumentProvider for SdkMeter {
138141
builder.name,
139142
builder.description,
140143
builder.unit,
144+
None,
141145
)?;
142146
if ms.is_empty() {
143147
return Ok(ObservableCounter::new(Arc::new(NoopAsyncInstrument::new())));
@@ -164,6 +168,7 @@ impl InstrumentProvider for SdkMeter {
164168
builder.name,
165169
builder.description,
166170
builder.unit,
171+
None,
167172
)
168173
.map(|i| UpDownCounter::new(Arc::new(i)))
169174
}
@@ -179,6 +184,7 @@ impl InstrumentProvider for SdkMeter {
179184
builder.name,
180185
builder.description,
181186
builder.unit,
187+
None,
182188
)
183189
.map(|i| UpDownCounter::new(Arc::new(i)))
184190
}
@@ -194,6 +200,7 @@ impl InstrumentProvider for SdkMeter {
194200
builder.name,
195201
builder.description,
196202
builder.unit,
203+
None,
197204
)?;
198205
if ms.is_empty() {
199206
return Ok(ObservableUpDownCounter::new(Arc::new(
@@ -223,6 +230,7 @@ impl InstrumentProvider for SdkMeter {
223230
builder.name,
224231
builder.description,
225232
builder.unit,
233+
None,
226234
)?;
227235
if ms.is_empty() {
228236
return Ok(ObservableUpDownCounter::new(Arc::new(
@@ -249,6 +257,7 @@ impl InstrumentProvider for SdkMeter {
249257
builder.name,
250258
builder.description,
251259
builder.unit,
260+
None,
252261
)
253262
.map(|i| Gauge::new(Arc::new(i)))
254263
}
@@ -261,6 +270,7 @@ impl InstrumentProvider for SdkMeter {
261270
builder.name,
262271
builder.description,
263272
builder.unit,
273+
None,
264274
)
265275
.map(|i| Gauge::new(Arc::new(i)))
266276
}
@@ -273,6 +283,7 @@ impl InstrumentProvider for SdkMeter {
273283
builder.name,
274284
builder.description,
275285
builder.unit,
286+
None,
276287
)
277288
.map(|i| Gauge::new(Arc::new(i)))
278289
}
@@ -288,6 +299,7 @@ impl InstrumentProvider for SdkMeter {
288299
builder.name,
289300
builder.description,
290301
builder.unit,
302+
None,
291303
)?;
292304
if ms.is_empty() {
293305
return Ok(ObservableGauge::new(Arc::new(NoopAsyncInstrument::new())));
@@ -315,6 +327,7 @@ impl InstrumentProvider for SdkMeter {
315327
builder.name,
316328
builder.description,
317329
builder.unit,
330+
None,
318331
)?;
319332
if ms.is_empty() {
320333
return Ok(ObservableGauge::new(Arc::new(NoopAsyncInstrument::new())));
@@ -342,6 +355,7 @@ impl InstrumentProvider for SdkMeter {
342355
builder.name,
343356
builder.description,
344357
builder.unit,
358+
None,
345359
)?;
346360
if ms.is_empty() {
347361
return Ok(ObservableGauge::new(Arc::new(NoopAsyncInstrument::new())));
@@ -366,6 +380,7 @@ impl InstrumentProvider for SdkMeter {
366380
builder.name,
367381
builder.description,
368382
builder.unit,
383+
builder.boundaries,
369384
)
370385
.map(|i| Histogram::new(Arc::new(i)))
371386
}
@@ -378,6 +393,7 @@ impl InstrumentProvider for SdkMeter {
378393
builder.name,
379394
builder.description,
380395
builder.unit,
396+
builder.boundaries,
381397
)
382398
.map(|i| Histogram::new(Arc::new(i)))
383399
}
@@ -479,8 +495,9 @@ where
479495
name: Cow<'static, str>,
480496
description: Option<Cow<'static, str>>,
481497
unit: Option<Cow<'static, str>>,
498+
boundaries: Option<Vec<f64>>,
482499
) -> Result<ResolvedMeasures<T>> {
483-
let aggregators = self.measures(kind, name, description, unit)?;
500+
let aggregators = self.measures(kind, name, description, unit, boundaries)?;
484501
Ok(ResolvedMeasures {
485502
measures: aggregators,
486503
})
@@ -492,6 +509,7 @@ where
492509
name: Cow<'static, str>,
493510
description: Option<Cow<'static, str>>,
494511
unit: Option<Cow<'static, str>>,
512+
boundaries: Option<Vec<f64>>,
495513
) -> Result<Vec<Arc<dyn internal::Measure<T>>>> {
496514
let inst = Instrument {
497515
name,
@@ -501,7 +519,7 @@ where
501519
scope: self.meter.scope.clone(),
502520
};
503521

504-
self.resolve.measures(inst)
522+
self.resolve.measures(inst, boundaries)
505523
}
506524
}
507525

opentelemetry-sdk/src/metrics/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,14 @@ mod tests {
259259
histogram_aggregation_helper(Temporality::Delta);
260260
}
261261

262+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
263+
async fn histogram_aggregation_with_custom_bounds() {
264+
// Run this test with stdout enabled to see output.
265+
// cargo test histogram_aggregation_with_custom_bounds --features=testing -- --nocapture
266+
histogram_aggregation_with_custom_bounds_helper(Temporality::Delta);
267+
histogram_aggregation_with_custom_bounds_helper(Temporality::Cumulative);
268+
}
269+
262270
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
263271
async fn updown_counter_aggregation_cumulative() {
264272
// Run this test with stdout enabled to see output.
@@ -1790,6 +1798,57 @@ mod tests {
17901798
}
17911799
}
17921800

1801+
fn histogram_aggregation_with_custom_bounds_helper(temporality: Temporality) {
1802+
let mut test_context = TestContext::new(temporality);
1803+
let histogram = test_context
1804+
.meter()
1805+
.u64_histogram("test_histogram")
1806+
.with_boundaries(vec![1.0, 2.5, 5.5])
1807+
.init();
1808+
histogram.record(1, &[KeyValue::new("key1", "value1")]);
1809+
histogram.record(2, &[KeyValue::new("key1", "value1")]);
1810+
histogram.record(3, &[KeyValue::new("key1", "value1")]);
1811+
histogram.record(4, &[KeyValue::new("key1", "value1")]);
1812+
histogram.record(5, &[KeyValue::new("key1", "value1")]);
1813+
1814+
test_context.flush_metrics();
1815+
1816+
// Assert
1817+
let histogram_data =
1818+
test_context.get_aggregation::<data::Histogram<u64>>("test_histogram", None);
1819+
// Expecting 2 time-series.
1820+
assert_eq!(histogram_data.data_points.len(), 1);
1821+
if let Temporality::Cumulative = temporality {
1822+
assert_eq!(
1823+
histogram_data.temporality,
1824+
Temporality::Cumulative,
1825+
"Should produce cumulative"
1826+
);
1827+
} else {
1828+
assert_eq!(
1829+
histogram_data.temporality,
1830+
Temporality::Delta,
1831+
"Should produce delta"
1832+
);
1833+
}
1834+
1835+
// find and validate key1=value1 datapoint
1836+
let data_point =
1837+
find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value1")
1838+
.expect("datapoint with key1=value1 expected");
1839+
1840+
assert_eq!(data_point.count, 5);
1841+
assert_eq!(data_point.sum, 15);
1842+
1843+
// Check the bucket counts
1844+
// -∞ to 1.0: 1
1845+
// 1.0 to 2.5: 1
1846+
// 2.5 to 5.5: 3
1847+
// 5.5 to +∞: 0
1848+
1849+
assert_eq!(vec![1.0, 2.5, 5.5], data_point.bounds);
1850+
assert_eq!(vec![1, 1, 3, 0], data_point.bucket_counts);
1851+
}
17931852
fn gauge_aggregation_helper(temporality: Temporality) {
17941853
// Arrange
17951854
let mut test_context = TestContext::new(temporality);

opentelemetry-sdk/src/metrics/pipeline.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,11 @@ where
244244
///
245245
/// If an instrument is determined to use a [aggregation::Aggregation::Drop],
246246
/// that instrument is not inserted nor returned.
247-
fn instrument(&self, inst: Instrument) -> Result<Vec<Arc<dyn internal::Measure<T>>>> {
247+
fn instrument(
248+
&self,
249+
inst: Instrument,
250+
boundaries: Option<&[f64]>,
251+
) -> Result<Vec<Arc<dyn internal::Measure<T>>>> {
248252
let mut matched = false;
249253
let mut measures = vec![];
250254
let mut errs = vec![];
@@ -288,14 +292,22 @@ where
288292
}
289293

290294
// Apply implicit default view if no explicit matched.
291-
let stream = Stream {
295+
let mut stream = Stream {
292296
name: inst.name,
293297
description: inst.description,
294298
unit: inst.unit,
295299
aggregation: None,
296300
allowed_attribute_keys: None,
297301
};
298302

303+
// Override default histogram boundaries if provided.
304+
if let Some(boundaries) = boundaries {
305+
stream.aggregation = Some(Aggregation::ExplicitBucketHistogram {
306+
boundaries: boundaries.to_vec(),
307+
record_min_max: true,
308+
});
309+
}
310+
299311
match self.cached_aggregator(&inst.scope, kind, stream) {
300312
Ok(agg) => {
301313
if errs.is_empty() {
@@ -682,11 +694,15 @@ where
682694
}
683695

684696
/// The measures that must be updated by the instrument defined by key.
685-
pub(crate) fn measures(&self, id: Instrument) -> Result<Vec<Arc<dyn internal::Measure<T>>>> {
697+
pub(crate) fn measures(
698+
&self,
699+
id: Instrument,
700+
boundaries: Option<Vec<f64>>,
701+
) -> Result<Vec<Arc<dyn internal::Measure<T>>>> {
686702
let (mut measures, mut errs) = (vec![], vec![]);
687703

688704
for inserter in &self.inserters {
689-
match inserter.instrument(id.clone()) {
705+
match inserter.instrument(id.clone(), boundaries.as_deref()) {
690706
Ok(ms) => measures.extend(ms),
691707
Err(err) => errs.push(err),
692708
}

opentelemetry/CHANGELOG.md

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

1010
- **Modified**: `MeterProvider.meter()` and `MeterProvider.versioned_meter()` argument types have been updated to `&'static str` instead of `impl Into<Cow<'static, str>>>` [#2112](https://github.com/open-telemetry/opentelemetry-rust/pull/2112). These APIs were modified to enforce the Meter `name`, `version`, and `schema_url` to be `&'static str`.
1111

12+
- Added `with_boundaries` API to allow users to provide custom bounds for Histogram instruments. [#2135](https://github.com/open-telemetry/opentelemetry-rust/pull/2135)
13+
1214
## v0.25.0
1315

1416
- **BREAKING** [#1993](https://github.com/open-telemetry/opentelemetry-rust/pull/1993) Box complex types in AnyValue enum

opentelemetry/src/metrics/instruments/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ pub struct HistogramBuilder<'a, T> {
3939
/// Unit of the Histogram.
4040
pub unit: Option<Cow<'static, str>>,
4141

42+
/// Bucket boundaries for the histogram.
43+
pub boundaries: Option<Vec<f64>>,
44+
4245
// boundaries: Vec<T>,
4346
_marker: marker::PhantomData<T>,
4447
}
@@ -51,6 +54,7 @@ impl<'a, T> HistogramBuilder<'a, T> {
5154
name,
5255
description: None,
5356
unit: None,
57+
boundaries: None,
5458
_marker: marker::PhantomData,
5559
}
5660
}
@@ -72,6 +76,12 @@ impl<'a, T> HistogramBuilder<'a, T> {
7276
self.unit = Some(unit.into());
7377
self
7478
}
79+
80+
/// Set the boundaries for this histogram.
81+
pub fn with_boundaries(mut self, boundaries: Vec<f64>) -> Self {
82+
self.boundaries = Some(boundaries);
83+
self
84+
}
7585
}
7686

7787
impl<'a> HistogramBuilder<'a, f64> {
@@ -198,6 +208,7 @@ impl<T> fmt::Debug for HistogramBuilder<'_, T> {
198208
.field("name", &self.name)
199209
.field("description", &self.description)
200210
.field("unit", &self.unit)
211+
.field("boundaries", &self.boundaries)
201212
.field(
202213
"kind",
203214
&format!("Histogram<{}>", &std::any::type_name::<T>()),

0 commit comments

Comments
 (0)