linkerd / linkerd/linkerd2

Linkerd proxy /metrics inconsistent format

Open
#14,249 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area/proxy area/viz bug pinned
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.