Configuring Views and Aggregation in OpenTelemetry Metrics
A metric is recorded in one place and consumed in another, and the two rarely agree about bucket boundaries or which attributes matter. Views are the mechanism for resolving that without editing the recording code — which matters most for instruments a library created and you cannot change. This page covers the four things Views actually do. It builds on the OpenTelemetry metrics SDK, part of the Python metrics and instrumentation section.
Prerequisites
pip install "opentelemetry-sdk>=1.27.0,<2.0.0" \
"opentelemetry-exporter-otlp-proto-grpc>=1.27.0,<2.0.0"
export OTEL_SERVICE_NAME=orders-api
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_METRIC_EXPORT_INTERVAL=15000
Implementation
Step 1 — Set explicit buckets on a latency instrument. The SDK's default ladder is general-purpose; an SLO needs a boundary on the objective.
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.view import View, ExplicitBucketHistogramAggregation
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
latency_view = View(
instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(
boundaries=[0.010, 0.025, 0.050, 0.100, 0.150, 0.250, 0.400, 0.600, 1.0, 2.5],
),
)
provider = MeterProvider(
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter(insecure=True))],
views=[latency_view],
)
The 0.250 boundary is the SLO threshold, for the reasons in choosing histogram buckets for latency SLOs.
Step 2 — Drop attributes you cannot stop being recorded. attribute_keys is an allowlist: everything not listed is discarded before aggregation.
from opentelemetry.sdk.metrics.view import View
# A library records http.url in full. Keep the route, drop the rest.
bound_cardinality = View(
instrument_name="http.client.request.duration",
attribute_keys={"http.request.method", "server.address", "http.response.status_code"},
)
The attribute never becomes a series, which means the cardinality is bounded at the SDK rather than at the Collector — cheaper, and closer to the code that has the problem.
Step 3 — Rename an awkward stream. Useful when a library's metric name does not match your naming convention and your dashboards already exist.
rename = View(
instrument_name="requests.duration", # what the library calls it
name="http.client.request.duration", # what your dashboards expect
description="Outbound HTTP request duration",
)
Step 4 — Disable an instrument entirely. DropAggregation produces no stream at all.
from opentelemetry.sdk.metrics.view import View, DropAggregation
silence = View(
instrument_name="db.client.connections.usage", # noisy, and not yours to remove
aggregation=DropAggregation(),
)
Prefer this to filtering later: a dropped instrument costs nothing to aggregate, nothing to export and nothing to store, while a Collector-side filter has already paid for all three.
Step 5 — Register everything on the provider. Views are applied at stream creation, so they must exist before the instruments do.
provider = MeterProvider(
resource=RESOURCE,
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter(insecure=True))],
views=[latency_view, bound_cardinality, rename, silence],
)
metrics.set_meter_provider(provider)
A View added after an instrument has been created has no effect on it, and nothing warns about that — which is why the whole set belongs in the same startup function.
Configuration options
| Selector | Purpose | Notes |
|---|---|---|
instrument_name |
match one instrument | supports a * wildcard |
instrument_type |
match a whole class | for example every histogram |
meter_name |
match an instrumentation scope | the way to target one library |
| Transformation | ||
aggregation |
change the shape | ExplicitBucketHistogramAggregation, DropAggregation, SumAggregation |
attribute_keys |
allowlist | everything unlisted is dropped |
name |
rename the stream | give distinct names when two Views match |
description |
document it | appears in the exported metadata |
Verification
from opentelemetry import metrics
meter = metrics.get_meter("verify")
hist = meter.create_histogram("http.server.request.duration", unit="s")
for value in (0.02, 0.2, 0.24, 0.3, 1.2):
hist.record(value, {"http.route": "/orders/{id}", "user.id": "u-9f3c"})
Expected Output (Collector debug exporter):
Histogram #0
-> http.route: Str(/orders/{id})
Count: 5
Sum: 1.960
ExplicitBounds: [0.01, 0.025, 0.05, 0.1, 0.15, 0.25, 0.4, 0.6, 1, 2.5]
Buckets: [0, 1, 0, 0, 0, 2, 1, 0, 0, 1, 0]
Two things confirm the Views are active: the boundaries are yours rather than the SDK default, and user.id is absent although the recording code passed it — dropped by the attribute allowlist before aggregation.
Then assert it, so a later refactor cannot silently reintroduce the attribute:
def test_user_id_never_reaches_the_exporter():
metrics_data = reader.get_metrics_data()
for rm in metrics_data.resource_metrics:
for sm in rm.scope_metrics:
for metric in sm.metrics:
for point in metric.data.data_points:
assert "user.id" not in point.attributes
Common mistakes
The View has no effect
Error signature: the default buckets are still exported despite a View being defined.
Root cause: the View was created after the MeterProvider, or the selector does not match — most often because the instrument name differs by a dot or a suffix.
Remediation: pass Views to the constructor, and print the instrument names the SDK actually sees before writing the selector.
Two Views produce one stream
Error signature: a fine-grained and a coarse stream were configured, and only one appears.
Root cause: both Views produce a stream with the same name, so they collide at the exporter.
Remediation: give each View an explicit distinct name.
Cardinality is fixed in the Collector instead
Error signature: the Collector's memory limiter engages regularly under normal load. Root cause: the series were created, aggregated, serialised and sent before being dropped. Remediation: move the permanent drops into Views and keep the Collector filter for experiments.
Selectors, and how specific to be
A View's selector decides which instruments it applies to, and the choice between the available criteria is mostly about how much future behaviour you want to constrain.
By instrument name is the most common and the most precise. It applies to exactly one stream, which makes the intent obvious to a reader and means an instrument renamed by a dependency upgrade silently stops matching — a failure mode worth knowing about, and one that a startup assertion catches.
By instrument type applies to a whole class: every histogram, every counter. This is the right selector for a policy that is genuinely about the shape rather than about a specific metric — "every histogram in this service uses these boundaries" is a reasonable default that individual Views can then override.
By meter name applies to everything created by one instrumentation scope, which is the way to target a single library's instruments without naming each one. When a dependency emits a dozen metrics and you want the same attribute allowlist on all of them, this is the selector that expresses that in one entry.
With a wildcard on the name, which is useful for a family — http.client.* — and worth using sparingly, because a wildcard that grows to match something unintended is harder to notice than a name that stops matching.
# every histogram gets a sensible default ladder
View(instrument_type=Histogram,
aggregation=ExplicitBucketHistogramAggregation(boundaries=DEFAULT_LADDER))
# and one specific metric overrides it
View(instrument_name="http.server.request.duration",
aggregation=ExplicitBucketHistogramAggregation(boundaries=SLO_LADDER))
| Selector | Scope | Best for |
|---|---|---|
instrument_name |
one stream | a specific decision |
instrument_name with * |
a family | consistent treatment of related metrics |
instrument_type |
a class | a service-wide default |
meter_name |
one library | policy for a dependency's metrics |
Temporality is a reader concern, not a View one
A related setting that often gets confused with Views: aggregation temporality — cumulative or delta — is configured on the metric reader or the exporter, not on a View. A View changes the shape of a stream; temporality changes how successive exports relate to each other.
The distinction matters when debugging, because a counter behaving oddly across restarts is a temporality question and no View will affect it. Cumulative counters restart at zero and the query layer compensates; delta counters lose only the interval in flight. Set it explicitly for the destination rather than relying on a default, as covered in bridging Prometheus metrics into OpenTelemetry.
One practical note on ordering: Views are evaluated against each instrument as its stream is created, and where several match, each produces its own stream. That makes a broad default plus a narrow override a matter of naming the streams distinctly rather than of precedence — there is no first-match-wins rule to rely on.
Keeping Views reviewable
Views encode policy that is invisible from the call site: someone reading histogram.record(elapsed, {"user.id": uid}) has no way to know the attribute is dropped before export. That asymmetry is the price of being able to fix instrumentation you do not own, and two habits keep it from becoming confusing.
Keep every View in one module next to the provider construction, so the whole policy is readable at once. And comment each one with why rather than what — "url.full is unbounded; kept on the span instead" explains a decision that the code alone cannot, and it is the note that stops someone removing the View six months later because it looked redundant.
A startup assertion is the third habit worth having: check that each View's selector matches at least one instrument, and log a warning when it does not. A View that matches nothing is silent, and it is exactly what a renamed instrument produces.
Related
- The OpenTelemetry metrics SDK in Python — the parent guide: providers, readers and instruments.
- Recording counters and histograms with OpenTelemetry — the instruments a View reshapes.
- Choosing histogram buckets for latency SLOs — how to pick the boundaries a View applies.
- Controlling label cardinality in Prometheus — the problem
attribute_keyssolves. - Exporting OTLP metrics to the collector — where the reshaped stream goes.
Frequently Asked Questions
What is a View for?
Changing how an instrument's measurements become a metric stream, without changing the code that records them. That covers four things in practice: custom histogram buckets, dropping attributes to reduce cardinality, renaming a stream, and disabling an instrument entirely. The common thread is that all four are decisions the consumer of the metric should be able to make, and the instrumented code — especially a library's — is often not yours to edit.
Can I add a View after the MeterProvider is created?
No. Views are supplied to the provider's constructor and are applied when a stream is created for an instrument, so a View registered later does not affect instruments that already exist. In practice this means Views belong in the same startup function that builds the provider, which is also where they are easiest to review.
How do I drop an attribute without touching the recording code?
Give the View an attribute_keys set containing only the attributes you want to keep. Everything else is dropped before aggregation, so the series count collapses to the combinations of the keys you listed. That is the standard remedy when a library records something useful for tracing and ruinous for metrics — a user id, a full URL — and you cannot change it.
What happens when two Views match the same instrument?
You get two streams, which is a feature rather than a conflict: a fine-grained stream with all attributes for a short-retention system and a coarse one with attributes dropped for long-term storage. Give each View a distinct name, or the two streams collide in the exporter and the result depends on which arrived last.
Do Views work for the Prometheus exporter too?
Yes — Views operate at the SDK level, before any exporter sees the data, so they apply identically whether you export over OTLP or expose a Prometheus endpoint through the OpenTelemetry Prometheus exporter. That is one of the practical arguments for the OpenTelemetry SDK: the reshaping rules live in one place regardless of where the data ends up.