A Histogram is a synchronous instrument that records a distribution of values. Histograms are ideal for understanding the statistical distribution of measurements like request duration, response sizes, or temperatures.
When to Use Histogram
Use a Histogram when:
- You need to calculate percentiles (p50, p95, p99)
- You want to understand the distribution of values
- Min, max, sum, and count are all valuable
- Values represent measurements or observations
Common examples:
- HTTP request duration
- Response payload size
- Database query latency
- Temperature readings
- Request processing time
Histograms are more expensive than counters because they record individual measurements and calculate statistics. Use them only when you need distribution data.
API Reference
pub struct Histogram<T>(Arc<dyn SyncInstrument<T> + Send + Sync>);
impl<T> Histogram<T> {
pub fn record(&self, value: T, attributes: &[KeyValue]);
}
The record method adds a value to the distribution.
Creating a Histogram
Histograms support u64 and f64 data types:
use opentelemetry::global;
let meter = global::meter("my-app");
// f64 histogram (most common)
let duration_histogram = meter.f64_histogram("http_request_duration")
.with_description("HTTP request duration in seconds")
.with_unit("s")
.build();
// u64 histogram
let size_histogram = meter.u64_histogram("response_size")
.with_description("HTTP response size in bytes")
.with_unit("By")
.build();
Recording Measurements
Use the record method to add values to the histogram:
use opentelemetry::KeyValue;
// Record a single value
duration_histogram.record(0.123, &[]);
// Record with attributes
duration_histogram.record(
0.456,
&[
KeyValue::new("method", "GET"),
KeyValue::new("endpoint", "/api/users"),
KeyValue::new("status", "200"),
],
);
// Record response sizes
size_histogram.record(1024, &[KeyValue::new("content_type", "application/json")]);
Configuring Bucket Boundaries
Histogram accuracy depends on bucket boundaries. You can customize them when creating the histogram:
let histogram = meter.f64_histogram("request_duration")
.with_description("Request duration in seconds")
.with_unit("s")
.with_boundaries(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0
])
.build();
Default Boundaries
If you don’t specify boundaries, the default is:
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]
Boundary Requirements
Boundaries must:
- Not contain
f64::NAN, f64::INFINITY, or f64::NEG_INFINITY
- Be in strictly increasing order
- Not contain duplicate values
Invalid boundaries will cause the instrument to not report measurements.
Choosing Boundaries
Choose boundaries based on your expected value range:
// For request duration in seconds (most requests < 1s)
let latency = meter.f64_histogram("latency")
.with_boundaries(vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0])
.build();
// For response size in bytes
let size = meter.u64_histogram("response_size")
.with_boundaries(vec![100.0, 500.0, 1000.0, 5000.0, 10000.0, 50000.0, 100000.0])
.build();
// For fine-grained millisecond measurements
let ms_latency = meter.f64_histogram("db_query_duration")
.with_unit("ms")
.with_boundaries(vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, 1000.0])
.build();
Use more buckets for ranges where you expect most values. This improves percentile accuracy but increases memory and network usage.
Complete Example
use opentelemetry::{global, KeyValue};
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::Resource;
use std::time::Instant;
fn init_meter_provider() -> SdkMeterProvider {
let exporter = opentelemetry_stdout::MetricExporterBuilder::default().build();
let provider = SdkMeterProvider::builder()
.with_periodic_exporter(exporter)
.with_resource(
Resource::builder()
.with_service_name("my-service")
.build(),
)
.build();
global::set_meter_provider(provider.clone());
provider
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let meter_provider = init_meter_provider();
let meter = global::meter("http-server");
// Create a histogram for request duration
let histogram = meter
.f64_histogram("http_request_duration")
.with_description("HTTP request duration in seconds")
.with_unit("s")
.with_boundaries(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0
])
.build();
// Simulate handling a request
let start = Instant::now();
// ... process request ...
let duration = start.elapsed().as_secs_f64();
histogram.record(
duration,
&[
KeyValue::new("method", "GET"),
KeyValue::new("endpoint", "/api/users"),
KeyValue::new("status", "200"),
],
);
meter_provider.shutdown()?;
Ok(())
}
Example from Source
From the metrics-basic example:
// Create a Histogram Instrument
let histogram = meter
.f64_histogram("my_histogram")
.with_description("My histogram example description")
.with_boundaries(vec![0.0, 5.0, 10.0, 15.0, 20.0, 25.0])
.build();
// Record measurements
histogram.record(
10.5,
&[
KeyValue::new("mykey1", "myvalue1"),
KeyValue::new("mykey2", "myvalue2"),
],
);
Exponential Histograms
For unpredictable value ranges, use exponential histograms via Views. They automatically adjust bucket widths:
use opentelemetry_sdk::metrics::{Aggregation, Instrument, Stream};
let exponential_histogram_view = |i: &Instrument| {
if i.name() == "my_histogram" {
Stream::builder()
.with_aggregation(Aggregation::Base2ExponentialHistogram {
max_size: 160,
max_scale: 20,
record_min_max: true,
})
.build()
.ok()
} else {
None
}
};
let provider = SdkMeterProvider::builder()
.with_view(exponential_histogram_view)
.build();
See Views for more details on customizing aggregation.
Attributes and Cardinality
Like counters, histograms support attributes:
histogram.record(
0.123,
&[
KeyValue::new("method", "GET"),
KeyValue::new("status", "200"),
],
);
Each unique attribute combination creates a separate histogram. Keep cardinality low to avoid memory issues.
Cloning Histograms
Histograms can be cloned to share across your application:
let histogram = meter.f64_histogram("latency").build();
let histogram_clone = histogram.clone();
// Both record to the same underlying histogram
histogram.record(0.1, &[]);
histogram_clone.record(0.2, &[]);
Clone histograms instead of creating duplicates. Multiple histograms with the same name can lower SDK performance.
Histogram vs. Gauge
// Use for distributions - captures min, max, percentiles
let histogram = meter.f64_histogram("request_duration").build();
histogram.record(0.123, &[]);
histogram.record(0.456, &[]);
histogram.record(0.789, &[]);
// Results: min, max, p50, p95, p99, count, sum
Best Practices
- Choose appropriate boundaries: Match your expected value range
- Use consistent units: Seconds for duration, bytes for size
- Keep cardinality low: Limit unique attribute combinations
- Clone when sharing: Don’t create duplicate histograms
- Use f64 for most cases: Better precision for measurements
- Consider exponential histograms: For unpredictable ranges
Next Steps
Counter
Record monotonically increasing values
Gauge
Record independent point-in-time values
Views
Customize histogram aggregation and boundaries
Observable Instruments
Use callbacks to report measurements