Views allow you to customize how metrics are aggregated and exported by the SDK. They provide powerful control over metric behavior without modifying your instrumentation code.
What are Views?
Views are functions that match instruments and return customized Stream configurations. They enable you to:
- Rename metrics
- Change units
- Customize aggregation
- Filter attributes
- Set cardinality limits
- Drop unwanted metrics
View Trait
pub(crate) trait View: Send + Sync + 'static {
fn match_inst(&self, inst: &Instrument) -> Option<Stream>;
}
impl<T> View for T
where
T: Fn(&Instrument) -> Option<Stream> + Send + Sync + 'static,
{
fn match_inst(&self, inst: &Instrument) -> Option<Stream> {
self(inst)
}
}
Views are implemented for any function that takes an &Instrument and returns Option<Stream>.
Instrument Reference
The Instrument type provides information for matching:
pub struct Instrument {
pub(crate) name: Cow<'static, str>,
pub(crate) description: Cow<'static, str>,
pub(crate) kind: InstrumentKind,
pub(crate) unit: Cow<'static, str>,
pub(crate) scope: InstrumentationScope,
}
impl Instrument {
pub fn name(&self) -> &str;
pub fn kind(&self) -> InstrumentKind;
pub fn unit(&self) -> &str;
pub fn scope(&self) -> &InstrumentationScope;
}
Stream Configuration
The Stream type defines how an instrument’s data should be collected:
pub struct Stream {
name: Option<Cow<'static, str>>,
description: Option<Cow<'static, str>>,
unit: Option<Cow<'static, str>>,
aggregation: Option<Aggregation>,
allowed_attribute_keys: Option<Arc<HashSet<Key>>>,
cardinality_limit: Option<usize>,
}
Create streams using the builder pattern:
Stream::builder()
.with_name("custom_name")
.with_description("Custom description")
.with_unit("ms")
.with_aggregation(Aggregation::Sum)
.with_cardinality_limit(100)
.build()
Registering Views
Register views when building the MeterProvider:
use opentelemetry_sdk::metrics::{Instrument, SdkMeterProvider, Stream};
let my_view = |i: &Instrument| {
if i.name() == "my_histogram" {
Some(
Stream::builder()
.with_name("renamed_histogram")
.build()
.unwrap(),
)
} else {
None
}
};
let provider = SdkMeterProvider::builder()
.with_view(my_view)
.build();
Common View Patterns
Renaming Metrics
Change metric names without modifying code:
let rename_view = |i: &Instrument| {
if i.name() == "my_histogram" {
Some(
Stream::builder()
.with_name("my_histogram_renamed")
.with_unit("milliseconds")
.build()
.unwrap(),
)
} else {
None
}
};
let provider = SdkMeterProvider::builder()
.with_view(rename_view)
.build();
Changing Aggregation
Customize how metrics are aggregated:
use opentelemetry_sdk::metrics::Aggregation;
let exponential_histogram_view = |i: &Instrument| {
if i.name() == "my_third_histogram" {
Stream::builder()
.with_aggregation(Aggregation::Base2ExponentialHistogram {
max_size: 160,
max_scale: 20,
record_min_max: true,
})
.build()
.ok()
} else {
None
}
};
Setting Cardinality Limits
Limit the number of unique attribute combinations:
let cardinality_limit_view = |i: &Instrument| {
if i.name() == "my_second_histogram" {
Stream::builder()
.with_cardinality_limit(2)
.build()
.ok()
} else {
None
}
};
When the limit is reached, additional attribute combinations are aggregated into an overflow bucket.
Dropping Metrics
Exclude specific metrics from export:
let drop_view = |i: &Instrument| {
if i.name() == "unwanted_metric" {
Stream::builder()
.with_aggregation(Aggregation::Drop)
.build()
.ok()
} else {
None
}
};
Complete Example
From the metrics-advanced example:
use opentelemetry::global;
use opentelemetry_sdk::metrics::{
Aggregation, Instrument, SdkMeterProvider, Stream, Temporality
};
use opentelemetry_sdk::Resource;
fn init_meter_provider() -> SdkMeterProvider {
// View 1: Rename and change unit
let my_view_rename_and_unit = |i: &Instrument| {
if i.name() == "my_histogram" {
Some(
Stream::builder()
.with_name("my_histogram_renamed")
.with_unit("milliseconds")
.build()
.unwrap(),
)
} else {
None
}
};
// View 2: Change cardinality limit
let my_view_change_cardinality = |i: &Instrument| {
if i.name() == "my_second_histogram" {
Stream::builder()
.with_cardinality_limit(2)
.build()
.ok()
} else {
None
}
};
// View 3: Use exponential histogram
let my_view_change_aggregation = |i: &Instrument| {
if i.name() == "my_third_histogram" {
Stream::builder()
.with_aggregation(Aggregation::Base2ExponentialHistogram {
max_size: 160,
max_scale: 20,
record_min_max: true,
})
.build()
.ok()
} else {
None
}
};
let exporter = opentelemetry_stdout::MetricExporterBuilder::default()
.with_temporality(Temporality::Delta)
.build();
let provider = SdkMeterProvider::builder()
.with_periodic_exporter(exporter)
.with_resource(
Resource::builder()
.with_service_name("metrics-advanced-example")
.build(),
)
.with_view(my_view_rename_and_unit)
.with_view(my_view_change_cardinality)
.with_view(my_view_change_aggregation)
.build();
global::set_meter_provider(provider.clone());
provider
}
Aggregation Types
Views can specify different aggregation strategies:
Default Aggregation
Use the instrument’s default aggregation:
Sum Aggregation
Sum all values:
LastValue Aggregation
Keep only the last value:
Explicit Bucket Histogram
Histogram with custom boundaries:
Aggregation::ExplicitBucketHistogram {
boundaries: vec![0.0, 5.0, 10.0, 25.0, 50.0, 100.0],
record_min_max: true,
}
Exponential Histogram
Histogram with exponentially growing buckets:
Aggregation::Base2ExponentialHistogram {
max_size: 160, // Maximum number of buckets
max_scale: 20, // Maximum resolution scale (-10 to 20)
record_min_max: true,
}
Exponential histograms automatically adjust bucket widths, making them ideal for unpredictable value ranges (e.g., network latency with occasional outliers).
Drop Aggregation
Discard all data:
Exponential Histogram Details
Exponential histograms are useful when value ranges are unpredictable:
let exponential_view = |i: &Instrument| {
if i.name() == "network_latency" {
Stream::builder()
.with_aggregation(Aggregation::Base2ExponentialHistogram {
max_size: 160, // More buckets = more precision
max_scale: 20, // Higher scale = finer resolution
record_min_max: true,
})
.build()
.ok()
} else {
None
}
};
Parameters:
max_size: Maximum number of buckets (higher = more memory, better precision)
max_scale: Resolution scale from -10 to 20 (higher = finer buckets)
record_min_max: Whether to track min and max values
Use exponential histograms for metrics like network round-trip time, where most values are small but occasional spikes occur.
Pattern Matching
Match instruments by different criteria:
By Name
let view = |i: &Instrument| {
if i.name() == "specific_metric" {
Some(Stream::builder().with_name("renamed").build().unwrap())
} else {
None
}
};
By Name Pattern
let view = |i: &Instrument| {
if i.name().starts_with("http_") {
Some(
Stream::builder()
.with_aggregation(Aggregation::ExplicitBucketHistogram {
boundaries: vec![0.01, 0.1, 1.0, 10.0],
record_min_max: true,
})
.build()
.unwrap(),
)
} else {
None
}
};
By Instrument Kind
use opentelemetry_sdk::metrics::InstrumentKind;
let view = |i: &Instrument| {
if i.kind() == InstrumentKind::Histogram {
Some(
Stream::builder()
.with_cardinality_limit(100)
.build()
.unwrap(),
)
} else {
None
}
};
By Scope
let view = |i: &Instrument| {
if i.scope().name() == "problematic-library" {
Some(
Stream::builder()
.with_aggregation(Aggregation::Drop)
.build()
.unwrap(),
)
} else {
None
}
};
Multiple Views
Register multiple views in order of precedence:
let provider = SdkMeterProvider::builder()
.with_view(view1) // Checked first
.with_view(view2) // Checked second
.with_view(view3) // Checked third
.build();
The first matching view is applied.
Error Handling
Handle invalid stream configurations gracefully:
let view = |i: &Instrument| {
if i.name() == "my_metric" {
// Using .ok() treats errors as non-matching
Stream::builder()
.with_cardinality_limit(2)
.build()
.ok()
} else {
None
}
};
Alternatively, handle errors explicitly:
let view = |i: &Instrument| {
if i.name() == "my_metric" {
match Stream::builder().with_cardinality_limit(2).build() {
Ok(stream) => Some(stream),
Err(e) => {
eprintln!("Failed to create stream: {}", e);
None
}
}
} else {
None
}
};
Use Cases
Reduce Cardinality
Limit high-cardinality metrics:
let limit_cardinality = |i: &Instrument| {
if i.name().starts_with("user_") {
Stream::builder()
.with_cardinality_limit(100)
.build()
.ok()
} else {
None
}
};
Standardize Units
Convert all duration metrics to seconds:
let standardize_units = |i: &Instrument| {
if i.unit() == "ms" {
Some(
Stream::builder()
.with_unit("s")
.build()
.unwrap(),
)
} else {
None
}
};
Filter Noisy Metrics
Drop verbose debug metrics in production:
let filter_debug = |i: &Instrument| {
if i.name().starts_with("debug_") {
Stream::builder()
.with_aggregation(Aggregation::Drop)
.build()
.ok()
} else {
None
}
};
Best Practices
- Return
None for non-matching instruments: This allows other views to apply
- Use
.ok() for graceful errors: Treats invalid configs as non-matching
- Order views by specificity: More specific views first, general views last
- Test view configurations: Ensure views apply as expected
- Document view purposes: Explain why each view exists
Next Steps
Instruments
Learn about different instrument types
Histogram
Understand histogram configuration
Meters
Create and configure Meters
Counter
Use Counter instruments