OpenTelemetry Metrics: A Complete Guide for Engineers in 2026
With OpenTelemetry (OTel), you can use OTel metrics specification support to get request rate and error ratio out of your code without committing to a vendor’s agent. You can also get p99 latency.
This guide covers the full metric instrument set, aggregation and temporality, and exemplars that connect measurements to traces. It also explains Views, cardinality limits, and the pipeline from your application to a backend.
Before you start instrumenting, one thing worth knowing is that the metrics API your code calls is stable across languages, and the SDK behind it has a metrics specification status that varies by language. That gap is why an OpenTelemetry Protocol (OTLP) exporter or exemplar feature you rely on in Java may behave differently in Python or JavaScript.
What Are OpenTelemetry Metrics?
Metrics are one of the three signals in OpenTelemetry’s telemetry data model, alongside traces and logs. Each capture is a metric event with a value, a timestamp, and associated metadata. Logs record discrete events with full context, and metrics trade per-event detail for cheap, always-on aggregates that still keep selected attributes and optional exemplars.
Distributed tracing follows one request across services, and metrics roll observations into periodic points carried by the OTLP, so your cost scales with the number of distinct attribute combinations rather than the number of requests.
Why Metrics Matter for Observability
Metrics are one of the cheapest observability signals to query because the SDK pre-aggregates them before export. Your service emits roughly the same number of observations per minute whether it handles 100 requests or 100,000, so you keep operational trends without paying to store every request as its own event.
That same property is what makes metrics the right signal for burn-rate alerting against a service level objective (SLO). Rate calculations depend on a complete count of requests and errors over the alert window, and metrics preserve that count because they are aggregated at the source rather than sampled away.
Core Components of the OpenTelemetry Metrics Pipeline
Every metric passes through the same metrics SDK pipeline on its way from your code to a backend, and the six components below cover every stage of that path in order. The Meter Provider, metric readers, and exporters are the three you wire once at startup. Your application code then creates Meters and calls Add or Record on instruments per request, and an optional Collector sits between the SDK and the backend when you need to reshape data in flight.
Meter Provider: The One Object You Configure at Startup
The Meter Provider is the factory you initialize once. It owns the resource attributes, Views, exemplar filter, readers, and exporters. You can change buckets or exporters as a startup edit rather than rewriting instrumentation calls.
Meter: Where Instrumentation Scope Comes From
A Meter comes from the Meter Provider and creates the instruments your code calls. The metrics API specification defines its behavior. Its instrumentation scope names the library and version. This keeps two libraries that emit requests.total apart.
Metric Instruments: Synchronous and Asynchronous
Synchronous instruments run inline with application or business processing logic, and those measurements can carry trace context. Asynchronous instruments register a callback that the SDK invokes during each collection. Your code initiates a synchronous measurement, and the SDK’s collection cycle initiates an asynchronous one.
Metric Reader: The Collection Trigger Between Instruments and Exporters
A metric reader schedules when the SDK collects aggregated points from every instrument and hands them to a paired exporter. The PeriodicExportingMetricReader runs on a configurable interval for push exporters like OTLP, while a pull reader collects on demand for scrape-based paths like Prometheus. You configure one reader per exporter at startup, and its collection cycle is also what invokes your asynchronous instrument callbacks.
Metric Exporter: OTLP, Prometheus, or Console
Exporters move aggregated data from a reader to a Collector or backend. Your options include OTLP and a Prometheus exporter serving /metrics. A console exporter is also available. The language support matrix marks OTLP metrics exporters as stable in Java, Go, JavaScript, and .NET.
OpenTelemetry Collector: The Optional Middle Hop
The Collector’s processors change data in flight through filtering and enrichment. The Collector processor documentation covers batching points, dropping series, converting temporality, and attaching pod and node attributes. Teams use these operations to wire Kubernetes observability pipelines. The Open Agent Management Protocol (OpAMP) gives you a vendor-agnostic way to manage the fleet through the OpAMP specification.
OpenTelemetry Metric Instrument Types
Seven instruments exist, four of them synchronous: Counter, UpDownCounter, Gauge, and Histogram. The other three are asynchronous variants prefixed Observable. Each instrument has a default aggregation that a View can override.
Counter: Monotonic Totals a Backend Can Rate
A Counter supports non-negative increments, so the total only climbs, and its monotonic Sum default lets your backend compute a rate over any window. A synchronous call lets the measurement capture active trace context for an exemplar. For example, an exemplar on a request-count increase can point you to one request span from that interval.
The Python instrument example below uses two low-cardinality attributes. It records one completed GET request. The Counter increases by one for that attribute combination.
request_counter = meter.create_counter(
name="requests.total",
unit="1",
description="Total number of requests completed",
)
request_counter.add(1, {
"http.request.method": "GET", "http.response.status_code": "200"
})
Each distinct combination of http.request.method and http.response.status_code becomes its own time series, which the SDK sums until the next export. The attributes let you compare request rates by method and response code. The exemplar retains a trace reference without turning trace IDs into metric attributes.
UpDownCounter: Values That Go Both Ways
An UpDownCounter takes increments and decrements, so you can track active requests or queue depth. The metric semantic conventions require increments and decrements to use identical attributes or they split into separate series. Synchronous calls also let an exemplar connect an active-request increase or decrease to the span that changed it.
Gauge: The Most Recent Value
A synchronous Gauge records non-additive values as they change, such as background noise level. Its Last Value default keeps the most recent measurement and its timestamp. When you record the Gauge inside an active span, an exemplar can associate a sudden value change with that trace context.
Histogram: Percentiles You Can Merge Across Pods
A Histogram records the distribution of values rather than a total. The default histogram aggregation uses upper boundaries [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000] and keeps count, sum, and optional min and max, so a backend derives p50, p95, and p99 at query time through the metrics data model. Backends can add bucket counts across instances, but they cannot merge the pre-computed quantiles that a Summary carries. The aligned histogram reservoir keeps one exemplar per bucket, so a slow-request exemplar in the highest bucket can take you from a p99 spike to a representative trace.
Observable Instruments: Callback Versions of Counter, UpDownCounter, and Gauge
Counter, UpDownCounter, and Gauge each have an asynchronous twin that the SDK collects through your callback. The callback runs during SDK collection rather than application processing. Each twin preserves the corresponding instrument’s core semantics:
- ObservableCounter: Keeps monotonic semantics and suits totals the system already holds, such as process CPU time. Because the callback measurement has no active Context, Go’s default trace-based filter drops its exemplars rather than attaching an unrelated trace.
- ObservableUpDownCounter: Suits additive snapshots such as current heap allocation, which a backend can sum across processes. Its callback has the same Context limitation, so a heap snapshot doesn’t receive a default trace-linked exemplar in Go.
- ObservableGauge: Fits when the value is already available or cheap to snapshot, as with CPU utilization; summing utilization across CPUs is meaningless, hence the Last Value default. Its callback-based utilization snapshot also lacks active Context, so Go’s default filter doesn’t retain a trace exemplar.
All three read state during collection. Individual changes between collections are not recorded. You should use synchronous instruments when trace-linked exemplars are important to the investigation path. This distinction connects instrument timing directly to exemplar availability.
How to Choose the Right Instrument
The instrument selection guidelines base instrument choice on how the value changes and whether instances can add their values together. You should then determine whether you need the distribution rather than a single total or snapshot:
- How does the value change, and is it additive? Counting changes as they happen means a Counter, or an UpDownCounter if the total can fall; an absolute value such as heap bytes means the asynchronous family. A summable absolute value is an ObservableCounter if monotonic, an ObservableUpDownCounter if not; temperature or utilization is an ObservableGauge.
- Do you need a distribution? Percentiles, or “how many requests took under one second,” mean a Histogram whatever the other answers were.
These questions let you choose semantics before considering export format or backend behavior. If you need trace-linked exemplars, also account for whether your code records the value synchronously. Instrument timing can narrow the choice even when two instruments appear to represent the same value.
The OpenTelemetry Metrics Data Model
The metrics data model specification defines the metric event and metric data stream before describing the time series stored by a backend. Each layer represents a different stage between recording and storage. The distinctions explain how one instrument can produce many backend time series.
A metric event contains the value, timestamp, attributes, and, for synchronous instruments, the trace and span ID of the active span. Shipping every event raw is impractical at production volume, so the SDK aggregates first and exports data points instead. The OTLP metric data stream gathers events sharing a resource, scope, name, unit, and data point type (Sum, Gauge, Histogram, ExponentialHistogram), and includes temporality and monotonicity.
Your backend stores the time series, keyed by name, attributes, value type, and unit. This layering separates what your code records from what the SDK exports and your backend stores. It also explains why changing attributes can create new time series without changing the metric name. The event, stream, and stored-series layers can consequently have different volumes.
Aggregation in OpenTelemetry Metrics
Aggregation is how the SDK collapses many events into the small set of data points that eventually reach your backend. Each instrument has a default aggregation the SDK applies automatically, and the mapping is fixed across languages:
- Counter and UpDownCounter: Sum, so a backend can compute a rate over any window from the running total.
- Gauge: Last Value, which keeps the most recent measurement and its timestamp.
- Histogram: Explicit Bucket Histogram, which records count, sum, and per-bucket counts so a backend derives percentiles at query time.
- Drop: An opt-in aggregation for series you want the SDK to discard before export.
- Base-2 exponential bucket histogram: An opt-in alternative for distributions where the explicit boundaries fit the data poorly.
Views are the mechanism you use to change any of those defaults. A View can rename a metric, filter which attribute keys survive to export, cap cardinality per instrument, or replace the aggregation itself through the Java View configuration and its equivalents in other languages. The default histogram boundaries almost always need adjustment because they were chosen as a generic starting point rather than for any specific unit or workload.
For example, on http.server.request.duration, you can register a View that keeps http.route and http.request.method, removes user.id, and replaces the default boundaries with latency-specific ones. That configuration preserves the aggregation you want to query without letting a single user identifier spawn a new time series per user.
Temporality: Cumulative vs. Delta
Aggregation temporality decides whether each exported point covers the interval since process start or only since the last export. Cumulative points share a start timestamp, so a monotonic sum never decreases, and they cost the SDK memory proportional to cardinality. Delta points reset after each report and move that cost downstream.
The OTLP exporter uses cumulative temporality by default, and OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE accepts cumulative, delta, or lowmemory per instrument kind under the OTLP temporality preference. This means cumulative request-count exports of 10, 15, and 18 correspond to interval deltas of 10, 5, and 3.
Prometheus expects cumulative temporality. Prometheus drops delta OTLP unless you turn on the otlp-deltatocumulative feature in v3.2 or later.
Cardinality and How OpenTelemetry Manages It
Cardinality, the number of distinct attribute combinations an instrument produces, drives cost more than request volume does.
On an http.server.duration histogram, 10 services, four methods, 50 routes, and five status classes yield 10,000 time series; adding user.id for 100,000 users pushes that to one billion. You should treat every attribute as a multiplicative cost decision.
The SDK default caps each instrument at 2,000 distinct attribute sets among its active metric streams. Further combinations collapse into one synthetic point carrying otel.metric.overflow = true. How the limit applies across collection boundaries depends on aggregation temporality and the SDK implementation. Totals stay correct, while queries that filter or group by attributes can undercount.
OpenTelemetry Metrics Semantic Conventions
Semantic conventions are the OpenTelemetry rules that fix metric names, units, and instrument types across every service in your fleet, so that http.server.request.duration means the same thing whether a Go service or a Java service emits it. Sticking to these conventions is what lets your backend build dashboards and alerts that work across the whole environment without per-service translation.
The naming rules break down into three parts that apply to every metric you emit:
- Namespaces: Metric names use dot-separated lowercase segments (like http.server.request.duration) so related metrics group naturally in a backend browser.
- Pluralization: Only countable non-units get pluralized with braces, such as {fault} or {request}. Units and continuous values stay singular.
- Units: Values follow the Unified Code for Units of Measure (UCUM) standard, using symbols like s for seconds, By for bytes, or 1 for dimensionless counts. The unit belongs in the instrument’s unit field and never repeats in the metric name.
Each domain then defines its own required attributes and instrument types on top of those naming rules. The HTTP metric conventions specify http.server.request.duration as a stable Histogram measured in seconds with recommended bucket boundaries from 0.005 up to 10. That metric requires http.request.method and url.scheme on every data point, and http.route must stay low-cardinality so the series count remains bounded.
Connecting Metrics to Other Signals
A histogram tells you p99 latency doubled at 14:02, but not which request caused it. Exemplars provide that request-level link, and shared resource attributes give every metric, log, and span the same identity. Together, they let you move from an aggregate symptom to the telemetry for a representative request and service instance.
Exemplars
An exemplar attaches OpenTelemetry context to a metric event: the value, timestamp, trace_id, span_id, and dropped attributes. The default TraceBased filter samples only inside a sampled span, and the AlignedHistogramBucketExemplarReservoir keeps one exemplar per histogram bucket.
Go, Java, and .NET implement both reservoirs, while the SDK compliance matrix shows that Python and JavaScript lack SDK-wide exemplar filter configuration. Asynchronous measurements carry no Context, so Go drops exemplars for callback-based instruments under the default filter.
A high-latency histogram bucket can retain an exemplar for one slow request. Your backend can then use its trace_id to open the trace and inspect the spans that contributed to the delay. This link gives you detail without adding a trace ID to every metric series.
Resource Attributes and Metrics Enrichment
A Resource is the immutable attribute set describing the entity producing telemetry, so every metric from the Meter Provider carries the required service.name. When configured, metrics can also carry service.instance.id and cloud.region; deployment.environment.name is another resource attribute.
Cardinality limits do not apply to resource attributes, so they provide a reliable join key from a metric to the logs of the same pod. You can use that shared identity even when no exemplar is available.
OpenTelemetry Metrics Best Practices
Cardinality control is a design discipline more than a runtime setting. Five habits keep an instrument affordable. They combine up-front attribute design with ongoing pipeline maintenance:
- Design attributes as a budget: Multiply the distinct values of every attribute before shipping, and keep TraceId and SpanId out of attributes.
- Standardize on the semantic conventions: Adopt the published metric wherever one exists.
- Correlate signals: Derive Rate, Errors, and Duration (RED) metrics from spans in the Collector and leave exemplar sampling on.
- Cap cardinality in the SDK: Some SDKs support per-View limits, while others expose a global limit.
- Review the pipeline on a schedule: Drop the series nobody alerts on.
Two of those five habits run automatically once you configure them. The SDK enforces the cardinality cap on every instrument, and your Views apply their attribute filters on every export.
The other three habits stay in your hands. You still decide the attribute budget up front, choose which conventions to adopt, and set the schedule for pipeline reviews. Automation keeps the safeguards in place, and deliberate design keeps the metrics worth safeguarding.
Getting Started: Instrumenting Your Application
Instrumenting an application with OpenTelemetry comes down to three layers you configure together. The API is the vendor-neutral surface your code and libraries call, the SDK decides how those calls are aggregated and processed, and exporters carry the results to a backend.
Each layer is described in the sections below, along with the language and stability differences worth knowing before you settle on a setup.
The OpenTelemetry Metrics API
The API is the vendor-neutral interface set your code calls, with a mandatory no-op: “All language implementations of OpenTelemetry MUST provide a No-Op.” If you configure no SDK, every Add and Record does nothing. A library can instrument itself without forcing a backend on users.
The OpenTelemetry SDK
The SDK owns aggregation, Views, cardinality limits, exemplar reservoirs, and the reader that schedules export. OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES, temporality preference, and histogram aggregation are all environment variables. You can move one image from a scrape target to OTLP push without a rebuild when the image already includes and configures both exporter components.
Exporters and Observability Backends
OTLP is the right default whenever your backend accepts it because it carries the most metric context intact. Exemplars, exponential histograms, and resource attributes all survive the trip from your SDK to the backend without translation.
Older exposition formats trade some of that context for compatibility. The legacy Prometheus text exposition format drops exemplars entirely, and OpenMetrics and other exemplar-capable paths can preserve them if the backend on the other end reads them. Your backend choice determines which pieces of metric context you can query later.
Putting OpenTelemetry Metrics to Work
The pipeline you have configured runs end to end. An instrument records a measurement, the SDK aggregates it and applies your Views, an exporter ships the result, and your backend stores and queries the series. What remains is picking a backend that accepts OTLP natively so none of that context gets stripped on the way in.
Coralogix accepts OTLP metrics, logs, and traces over gRPC Remote Procedure Calls (gRPC) or Hypertext Transfer Protocol (HTTP) through OpenTelemetry-native ingestion with no proprietary agent. A Prometheus Query Language (PromQL) endpoint runs on top of the same store, and DataPrime queries the logs and spans your exemplars point to from a single query language. Metrics price at $0.05 per GB, where one GB represents 1,000 active time series.
You can point your Collector’s OTLP exporter at the free 14-day Coralogix trial to validate your Views and temporality settings against a real backend.
Frequently Asked Questions About OpenTelemetry Metrics
What are the four types of metrics in OpenTelemetry?
The four synchronous instruments are Counter, UpDownCounter, Gauge, and Histogram. The full instrument set also includes an Observable variant of Counter, UpDownCounter, and Gauge that a callback collects.
Can you provide some examples of OpenTelemetry metrics?
requests.total is a Counter, active requests are an UpDownCounter, and http.server.request.duration is a Histogram in seconds. Process CPU time exposed as an absolute total during collection is an ObservableCounter. It records total CPU time consumed, with states providing the breakdown.
What is temporality in OpenTelemetry metrics?
Cumulative points cover the interval since process start, so successive points share one start timestamp. Delta points cover only the interval since the last export; the default behavior is described in the temporality section above.
How do OpenTelemetry metrics differ from Prometheus?
OpenTelemetry is an instrumentation standard: APIs, SDKs, and the OTLP transport for logs, metrics, and traces. Prometheus is a metrics backend with scraping, storage, and PromQL, and the two connect through the OTel Prometheus exporter’s /metrics endpoint or a configured Prometheus OTLP receiver. The OTLP receiver is not suitable for replacing ingestion through scraping.
What are the OpenTelemetry metrics standards?
The metrics API specification defines the instruments your code calls; the SDK specification defines aggregation, Views, cardinality limits, and export. Semantic conventions fix names and units, OTLP carries the result to a backend, and the API is stable while SDK support varies by language.