Chapter 8 — Metrics Pipeline: Bridging OpenTelemetry and Prometheus
Learning Objectives
Configure the OpenTelemetry SDK to emit metrics that Prometheus can scrape, or that the Collector can forward to a Prometheus-compatible backend.
Compare cumulative versus delta aggregation temporality and pick the right one for each backend in a multi-destination pipeline.
Translate OpenTelemetry attributes to Prometheus labels — including metric name and unit transformations — without losing semantic meaning.
Decide between the SDK Prometheus exporter, the Collector prometheus exporter, the prometheusremotewrite exporter, and Prometheus' native OTLP receiver.
Section 1 — OpenTelemetry Metrics Data Model
Pre-Reading Check — Sections 1 & 2
1. Which OpenTelemetry instrument type is best suited for measuring the number of in-flight HTTP requests at any moment?
A) Counter
B) UpDownCounter
C) Histogram
D) ObservableCounter
2. What is the primary advantage of an exponential histogram over a classic explicit-bucket histogram?
A) It uses no memory at all.
B) It automatically picks bucket boundaries via a scale parameter and downscales to stay bounded.
C) It supports more attribute keys than other histograms.
D) It works without a Meter.
3. In OpenTelemetry, what is a View?
A) A dashboard widget for visualizing metrics.
B) An SDK configuration that intercepts measurements and changes aggregation, name, or attributes before export.
C) A query language for Prometheus.
D) A type of OTel resource attribute.
4. Which aggregation temporality is the natural fit for Prometheus?
5. If a single OTLP export carrying delta points never arrives at the Collector, what happens?
A) The next cumulative sample makes the data recoverable.
B) The events in that interval are lost forever.
C) Prometheus automatically retries delivery.
D) The SDK retransmits all data since process start.
OpenTelemetry's metrics data model is intentionally richer than the Prometheus text exposition format. That richness — six instrument types, configurable aggregations via Views, exponential histograms, attributes-plus-resource-attributes — is precisely why the bridge is nontrivial.
1.1 The Six Core Instruments
Instruments are the API surface your code calls. They split along two axes: synchronous vs. observable (does the app push, or does the SDK pull a callback?) and monotonic vs. non-monotonic (can the value ever decrease?).
Instrument
Sync?
Monotonic?
Typical use
Counter
Yes
Yes
Requests served, bytes sent
UpDownCounter
Yes
No
In-flight requests, queue depth
Histogram
Yes
n/a
Request duration, payload size
ObservableCounter
No (callback)
Yes
CPU seconds, GC bytes
ObservableUpDownCounter
No (callback)
No
Memory in use, thread pool size
ObservableGauge
No (callback)
n/a
Temperature, load average
Think of synchronous instruments as ringing a bell on every event and observable instruments as a thermostat the SDK reads on a schedule. Both produce time series; the cost models differ.
1.2 Meter to Exporter — the OTel pipeline
flowchart TD
Meter[Meter]
Meter --> Sync[Synchronous Instruments]
Meter --> Obs[Observable Instruments]
Sync --> C[Counter]
Sync --> UDC[UpDownCounter]
Sync --> H[Histogram]
Obs --> OC[ObservableCounter]
Obs --> OUDC[ObservableUpDownCounter]
Obs --> OG[ObservableGauge]
C --> View["View rename, filter, change aggregation"]
UDC --> View
H --> View
OC --> View
OUDC --> View
OG --> View
View --> Agg["Aggregation Sum / LastValue / Histogram / ExpHistogram"]
Agg --> DP["Data Point value + attributes + timestamp + temporality"]
DP --> Exp["Exporter OTLP / Prometheus / stdout"]
Figure 8.1 above shows the SDK pipeline: an instrument emits a measurement; a View may rewrite or re-aggregate it; the aggregation builds a data point with a temporality stamp; an exporter pushes or exposes that point.
1.3 Views — the SDK's escape hatch
A View intercepts measurements before export. Views let you:
Rename a metric (http.server.request.duration → request_latency).
Drop high-cardinality attributes (e.g., strip user_id off a counter).
Change the aggregation type (e.g., explicit-bucket → exponential histogram).
Override bucket boundaries on a histogram.
Views are how you serve two backends from one instrument: an explicit-bucket histogram on the Prometheus scrape path, and an exponential histogram on the OTLP path.
// Go SDK: switch a histogram to exponential
sdkmetric.NewView(
sdkmetric.Instrument{Name: "request_duration_seconds"},
sdkmetric.Stream{
Aggregation: sdkmetric.AggregationExponentialHistogram{
MaxSize: 160,
MaxScale: 10,
},
},
)
1.4 Exponential histograms
OTel's ExponentialHistogram uses base-2 buckets controlled by a scales: buckets approximate [2^(i/2^s), 2^((i+1)/2^s)). Higher s = more buckets per power of two = more resolution.
Three properties make exponential histograms much better than fixed-bucket histograms:
Automatic dynamic range — no need to pre-pick bucket boundaries.
Bounded memory — when bucket count would exceed MaxSize, the aggregator downscales, merging neighbors. Memory stays constant; precision degrades gracefully.
Separate positive, negative, and zero buckets — supports signed observations.
Contrast with a classic histogram: when latency suddenly grows past your last bucket, every observation piles into the +Inf overflow bucket and your quantiles become meaningless.
Key Points — Section 1
OTel exposes six instrument types orthogonal to aggregation type and temporality.
Views are the SDK-level knob for renaming, filtering, and changing aggregation before export.
Exponential histograms auto-pick bucket boundaries and downscale to stay memory-bounded.
Synchronous vs. observable is about who triggers the measurement, not about what the metric means.
Section 2 — Aggregation Temporality
Aggregation temporality is the single most common source of OTel-to-Prometheus bugs. It is the difference between a counter that grows forever and one that resets every export — and Prometheus' query language assumes the former.
2.1 Cumulative vs. delta — what each means
Cumulative: each point is the total since a fixed start (usually process start).
Delta: each point is the change since the previous export.
Temporality applies to sums (counters) and histograms. Gauges are instantaneous and ignore temporality. Crucially, temporality is a property of the exported time series, not the instrument: the same Counter can be exported as cumulative to one backend and delta to another.
Animation: Cumulative vs Delta — same events, two shapes
Cumulative (Prometheus-friendly)
Prometheus expects this
Delta (per-interval increment)
Causes negative rate() in Prom
Both panels show the same underlying event stream (100, then 120, then 130 events per interval). The cumulative series totals 100 → 220 → 350; the delta series reports each interval's increment independently. Prometheus' rate() and increase() assume cumulative.
2.2 What happens on restart and missed exports
Aspect
Cumulative
Delta
Value at time T
Total since start
Change since last export
Lost export
Backend computes a longer-window rate
Data for that interval is lost forever
Process restart
Backend must detect reset
Each export is independent — no reset concept
Aligns with Prometheus?
Natural fit
Not supported natively
Typical OTLP push guidance
Supported, less common
Often favored
2.3 The Collector as temporality translator
In real pipelines, you rarely want to pick one temporality and force every backend to live with it. The dominant 2025 pattern is to let the OpenTelemetry Collector convert temporality per exporter:
App SDK exports OTLP with AggregationTemporality = DELTA.
Collector receives delta points.
Collector forwards delta to an OTLP vendor backend that prefers deltas.
Collector accumulates delta into cumulative for prometheusremotewrite or the /metrics endpoint Prometheus scrapes.
The Prometheus exporters inside the Collector maintain per-series state to sum deltas into a running cumulative total. This is what makes the “delta-from-apps, cumulative-to-Prometheus” pattern work end to end.
2.4 Symptoms of getting it wrong
Misconfigured temporality produces distinctive symptoms in Prometheus:
sum(rate(my_requests_total[5m])) returns negative values — your “counter” is actually delta, so each scrape sees a smaller value than the last.
Raw series shows a counter that drops between scrapes — same root cause.
increase() over a long window returns a value much smaller than the true event count — Prometheus interpreted drops as resets and discarded data.
The fix is always the same: ensure the exporter that feeds Prometheus emits cumulative, regardless of what the SDK and intermediate Collectors use.
Key Points — Section 2
Cumulative = total since start; delta = change since last export. Prometheus needs cumulative.
Lost delta exports cannot be reconstructed. Lost cumulative exports are recoverable from the next sample.
The Collector is the right place to convert delta to cumulative just before the Prometheus boundary.
Negative rate() in Prometheus is almost always delta leaking into the scrape path.
Post-Reading Quiz — Sections 1 & 2
1. Which OpenTelemetry instrument type is best suited for measuring the number of in-flight HTTP requests at any moment?
A) Counter
B) UpDownCounter
C) Histogram
D) ObservableCounter
2. What is the primary advantage of an exponential histogram over a classic explicit-bucket histogram?
A) It uses no memory at all.
B) It automatically picks bucket boundaries via a scale parameter and downscales to stay bounded.
C) It supports more attribute keys than other histograms.
D) It works without a Meter.
3. In OpenTelemetry, what is a View?
A) A dashboard widget for visualizing metrics.
B) An SDK configuration that intercepts measurements and changes aggregation, name, or attributes before export.
C) A query language for Prometheus.
D) A type of OTel resource attribute.
4. Which aggregation temporality is the natural fit for Prometheus?
Animation: Three bridge paths — one app, three destinations
Three sequential metric flows: (1) SDK exposes /metrics and Prometheus scrapes directly; (2) app pushes OTLP to a Collector that exposes /metrics for Prometheus scrape; (3) app pushes OTLP to a Collector that pushes remote-write to a horizontally scalable backend.
3.1 Option 1 — SDK Prometheus exporter
Attach a Prometheus exporter directly to the OTel SDK inside your app. The SDK accumulates measurements internally and exposes them in Prometheus text format on an HTTP endpoint.
Pros: familiar; no Collector required. Cons: resource attributes flatten into labels (or are lost), exponential histograms are down-converted to explicit-bucket, every app couples to Prometheus' wire format.
App pushes OTLP to a Collector; the Collector hosts a Prometheus exporter on a port; Prometheus scrapes the Collector. You get OTLP push from apps, Prometheus pull at the storage boundary, and a Collector chokepoint to manage temporality, naming, and cardinality.
The Collector pushes metrics over the Prometheus remote-write protocol. The dominant pattern for shipping into Cortex, Mimir, Thanos Receive, and VictoriaMetrics. Cannot target vanilla Prometheus — vanilla Prometheus is a remote-write client, not a receiver.
This path can preserve OTel exponential histograms by mapping them to Prometheus native histograms over the wire, making it the highest-fidelity option in 2025.
3.4 Option 4 — Prometheus OTLP receiver
Recent Prometheus versions (2.47+, more complete in 3.x) include an OTLP receiver that accepts pushed OTLP metrics directly into the TSDB. Collapses the pipeline to two components, but you lose service-discovery and OTLP-specific semantics map to Prometheus equivalents with varying maturity.
Four bridges exist; for most teams, App → OTLP → Collector → prometheus → scrape wins.
prometheusremotewrite is for remote-write-capable backends (Mimir/Cortex/Thanos/VictoriaMetrics), not vanilla Prometheus.
The Collector path is the only one that gives you a single place to fix temporality, naming, and cardinality across all apps.
Prometheus' OTLP receiver is the simplest topology but the least mature on attribute and histogram mapping.
Section 4 — Naming and Label Mapping
Once the wire path is sorted out, the next failure mode is semantic. OTel uses dotted, case-sensitive names plus explicit units. Prometheus uses underscored names with base-unit suffixes and the _total convention. A faithful bridge has to translate without quietly losing meaning.
4.1 Name conversion — the deterministic transform
Replace . with _ — http.server.request.duration → http_server_request_duration.
Replace other invalid characters (hyphens, slashes) with _.
Convert the value to the Prometheus base unit (e.g., ms → seconds), append the unit suffix.
If the instrument is a monotonic counter, append _total.
Animation: http.server.request.duration (ms, Histogram) → Prometheus name
0OTel name:http.server.request.durationunit: ms · type: Histogram
1Replace . with _→ http_server_request_duration
2Sanitize other invalid chars → http_server_request_duration (none here)
3Unit ms → base unit seconds; values × 0.001; append _seconds
4http_server_request_duration_secondsHistogram → no _total
Four-step deterministic transform. For a monotonic Counter, the exporter would also append _total. Setting the wrong unit at the instrument is a common silent bug — values land in Prometheus off by 1,000×.
OTel name
Unit
Instrument
Prometheus name
http.server.request.duration
s
Histogram
http_server_request_duration_seconds
http.server.active_requests
{request}
UpDownCounter
http_server_active_requests
http.client.request.body.size
By
Histogram
http_client_request_body_size_bytes
process.cpu.time
s
ObservableCounter
process_cpu_time_seconds_total
system.memory.usage
By
ObservableUpDownCounter
system_memory_usage_bytes
4.2 Unit suffixes
OTel uses UCUM unit codes; Prometheus uses base-unit suffixes. The exporter does the conversion if you set the OTel unit correctly.
OTel unit
Prom suffix
Value conversion
s
_seconds
None
ms
_seconds
× 0.001
us / µs
_seconds
× 0.000001
By
_bytes
None
KiBy
_bytes
× 1024
1, {request}
(none)
None
4.3 Attributes → labels
OTel attributes (per-measurement) and resource attributes (per-SDK) both become Prometheus labels. Attribute keys go through the same dot-to-underscore conversion. The Prom exporter typically promotes service.name → job and service.instance.id → instance to mirror Prometheus' service-discovery model.
OTel attribute
Prometheus label
service.name
job (and/or service_name)
service.instance.id
instance
http.response.status_code
http_response_status_code
k8s.namespace.name
k8s_namespace_name
net.peer.name
net_peer_name
4.4 UTF-8 names in Prometheus 3.x
Prometheus 3.x supports UTF-8 metric and label names through the OpenMetrics/native protocols, which means the OTel dotted form can in principle be preserved end-to-end by quoting the metric name. In practice, dashboards, alerting rules, and recording rules predating 2024 still expect underscored names. Treat UTF-8 names as forward-looking; expect translation back to underscored names anywhere a tool was written before 2024.
4.5 Common pitfalls
Forgetting to set the unit. Histogram values land in Prometheus without _seconds or _bytes — dashboards misinterpret them silently.
High-cardinality resource attributes.host.id or k8s.pod.uid can produce one series per host/pod, times every metric. Use Views to drop attributes you do not need.
Casing. Use http_status_code, not httpStatusCode.
Reserved labels. Do not produce attributes starting with __ (Prometheus uses these internally).
Bucket labels. The le label is added by the exporter — you cannot produce it as an OTel attribute.
Key Points — Section 4
Dots → underscores; UCUM units → base-unit suffixes; monotonic counters get _total.
Set the unit correctly at the instrument; the exporter does the value conversion and suffix.
Resource attributes become labels by default — cardinality discipline matters.
UTF-8 names are valid in Prometheus 3.x but most tooling still expects the underscored convention.
Post-Reading Quiz — Sections 3 & 4
6. Which exporter pushes metrics into Cortex, Mimir, Thanos Receive, or VictoriaMetrics?
A) The SDK Prometheus exporter.
B) The Collector prometheus exporter.
C) The Collector prometheusremotewrite exporter.
D) The Prometheus OTLP receiver.
7. Can you point the prometheusremotewrite exporter at a vanilla Prometheus server?
A) Yes — all Prometheus servers accept remote-write by default.
B) No — vanilla Prometheus is a remote-write client, not a receiver.
C) Only after enabling --web.enable-remote-write-receiver.
D) Only over HTTPS.
8. According to the chapter, what is the recommended 2025 bridging pattern for a Prometheus shop adopting OTel?
A) SDK Prometheus exporter on every app, no Collector.