Linkerd proxy /metrics inconsistent format
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 11.5k
- Forks
- 1.4k
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 53
Description
### What is the issue?
We've observed **inconsistencies in the metrics reported by the Linkerd proxy**, particularly in how they comply with the [Prometheus](https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example) or [OpenMetrics](https://github.com/prometheus/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1) standards. We originally reported this to Buoyant about **8 months ago**, and the issue was acknowledged as a **Request For Enhancement**. An improvement was expected to land in **Linkerd 2.18**, but we’re still seeing the same format inconsistencies.
```
# HELP outbound_http_route_request_statuses Completed request-response streams.
# TYPE outbound_http_route_request_statuses counter
outbound_http_route_request_statuses_total{parent_group="core",parent_kind="Service",parent_namespace="product-default",parent_name="hydration-api",parent_port="80",parent_section_name="",route_group="policy.linkerd.io",route_kind="HTTPRoute",route_namespace="product-default",route_name="product-default-hydration-api-pv-5b8f",http_status="200",error=""} 127
```
This example is from the proxy's `:4191/metrics` endpoint while testing HTTPRoute metrics.
According to [Linkerd docs](https://linkerd.io/2-edge/reference/proxy-metrics/), this endpoint is expected to emit metrics in **Prometheus format**, which is also reflected by the `content-type: text/plain` response header.
However, the format of some counter metrics—like `outbound_http_route_request_statuses`—raises questions. Per [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/#basic-info), counters must follow this pattern:
```
# HELP outbound_http_route_request_statuses_total Description
# TYPE outbound_http_route_request_statuses_total counter
outbound_http_route_request_statuses_total{...} 127
```
_(i.e., the `_total` suffix should appear **consistently** across help, type, and sample lines)._
By contrast, the [OpenMetrics format](https://github.com/prometheus/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1) expects the `_total` suffix **only on the sample name**, not the `HELP` or `TYPE` declarations.
This discrepancy causes **issues for metrics consumers**. Some systems—like **Datadog's Prometheus scraper**—silently fail to ingest improperly formatted metrics, assuming they conform strictly to Prometheus standards. See: [datadog/integrations-core#14772](https://github.com/DataDog/integrations-core/issues/14772)
In our case, the **Datadog integration** treats Linkerd metrics as Prometheus due to the `text/plain` header. When it encounters metrics using **OpenMetrics-like syntax**, it skips them entirely 🤯.
Even this issue [Prometheus Go client](https://github.com/prometheus/client_golang/issues/725) reinforces that counters must consistently use the `_total` suffix across all declarations.
We also found other examples of this issue:
```
# HELP process_uptime_seconds Total time since the process started (in seconds)
# TYPE process_uptime_seconds counter
# UNIT process_uptime_seconds seconds
process_uptime_seconds_total 5422.808476493
```
Only the sample adds `_total`, while `HELP` and `TYPE` do omit the expected suffix.
On the other hand, metrics like the following are correctly formatted and **are** successfully processed by Datadog:
```
# HELP request_total Total count of HTTP requests.
# TYPE request_total counter
request_total{direction="inbound",target_addr="0.0.0.0:4191",...} 213
request_total{direction="inbound",target_addr="0.0.0.0:4191",...} 1273
```
The official [Prometheus examples](https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example) also confirm that `_total` must appear in **every** line of a counter declaration:
```
# HELP http_requests_total The total number of HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="post",code="200"} 1027 1395066363000
http_requests_total{method="post",code="400"} 3 1395066363000
```
---
### Why This Matters
Scrapers like [Datadog’s OpenMetrics integration](https://github.com/DataDog/integrations-core/tree/master/openmetrics) expect metrics to conform to **either Prometheus or OpenMetrics**, based on the endpoint’s `Content-Type` header:
- `"application/openmetrics-text"` → OpenMetrics format
- `"text/plain"` → Prometheus format
Since Linkerd's `:4191/metrics` endpoint responds with `text/plain`, the **entire payload is interpreted as in Prometheus format**.
See also: [Datadog openmetrics parser caveats](https://github.com/DataDog/integrations-core/tree/master/openmetrics#errors-parsing-the-openmetrics-payload-with-agent-746)
### How can it be reproduced?
Simply query the :4191/metrics endpoint on a Linkerd proxy and examine the response payload along with its headers.
### Logs, error output, etc
```
# HELP process_uptime_seconds Total time since the process started (in seconds)
# TYPE process_uptime_seconds counter
# UNIT process_uptime_seconds seconds
process_uptime_seconds_total 5422.808476493
```
Only the sample adds `_total`, while `HELP` and `TYPE` do omit the expected suffix.
Per [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/#basic-info), `_total` must appear in **every** line of a counter declaration.
### output of `linkerd check -o short`
```
Status check results are √
```
### Environment
- Linkerd version: `edge-25.5.5`
### Possible solution
The intention behind supporting both Prometheus and OpenMetrics formats may be to preserve backward compatibility. However, if that’s the case, wouldn’t it be cleaner to expose **separate endpoints**—each dedicated to a specific metrics format—and consistently set the `Content-Type` header accordingly?
A more flexible approach could be to serve the appropriate format **from the same :4191/metrics endpoint**, based on the `Accept` header sent by the client.
Given that the current setup mixes Prometheus and OpenMetrics formats in a single endpoint, a more standards-aligned behavior might look like this:
• **Default behavior**: Return all metrics as of currently with `Content-Type: text/plain` **when no Accept header is provided** by the client.
• **OpenMetrics support**: Return only OpenMetrics-compliant metrics with `Content-Type: application/openmetrics-text` when the client **sets an Accept header like**:
`application/openmetrics-text;version=1.0.0,application/openmetrics-text;version=0.0.1;q=0.75,text/plain;version=0.0.4;q=0.5,*/*;q=0.1`
• **Prometheus-only request**: Return Prometheus-compliant metrics with `Content-Type: text/plain` **when the client explicitly sets**: `Accept: text/plain`
This separation would improve interoperability for consumers like Datadog or custom scrapers that strictly adhere to either Prometheus or OpenMetrics specifications.
### Additional context
_No response_
### Would you like to work on fixing this bug?
None
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by querying the Linkerd proxy's :4191/metrics endpoint and comparing the response body with its text/plain header against the Prometheus and OpenMetrics specifications. Trace the metrics endpoint implementation and its existing tests; done means the emitted counter declarations and content type are consistent with the selected format.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- prometheus
- Domain
- observability-sre
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100