Skip to main content
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:
Aggregation::Default

Sum Aggregation

Sum all values:
Aggregation::Sum

LastValue Aggregation

Keep only the last value:
Aggregation::LastValue

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:
Aggregation::Drop

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

  1. Return None for non-matching instruments: This allows other views to apply
  2. Use .ok() for graceful errors: Treats invalid configs as non-matching
  3. Order views by specificity: More specific views first, general views last
  4. Test view configurations: Ensure views apply as expected
  5. 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

Build docs developers (and LLMs) love