LinkMesh

Search docs, blog and changelog

ENDE

Snippets · Processors

Reduce metric cardinality (aggregate away per-request attributes)

One attribute with a value per user or per request turns a metric into thousands of series, and most backends bill per series. aggregate_on_attributes keeps only the attributes you name and sums every data point that becomes identical, so a request counter per route stays a request counter per route.

Signals
Metrics
Category
Reduction
Source
OTLP
Components
otlpbatchmemory_limitertransformotlphttp
Destination
OTLP/HTTP endpoint

OpenTelemetry Collector

OpenTelemetry Collector YAML
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  # Keeps only the attributes dashboards group by, and sums the data points
  # that become identical once the per-request attributes are gone.
  transform/reduce-cardinality:
    error_mode: ignore
    metric_statements:
      - context: metric
        statements:
          - aggregate_on_attributes("sum", ["http.request.method", "http.route", "http.response.status_code"]) where name == "http.server.request.count"
  batch:
    timeout: 5s

exporters:
  otlphttp:
    endpoint: https://your-backend:4318

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, transform/reduce-cardinality, batch]
      exporters: [otlphttp]

LinkMesh

LinkMesh has no built-in template for this job. Add it as a custom processor in raw mode, with transform as the component type and this body - the same lines as under transform/reduce-cardinality in the collector config:

Custom processor body (transform)
error_mode: ignore
metric_statements:
  - context: metric
    statements:
      - aggregate_on_attributes("sum", ["http.request.method", "http.route", "http.response.status_code"]) where name == "http.server.request.count"

How custom components are validated and rolled out: create a custom processor.

Download config.yamlValidated with otelcol-contrib v0.161.0 on 2026-10-26

Complete config files, each checked with the collector's own validate command (otelcol-contrib v0.161.0). Replace https://your-backend:4318 with your own OTLP endpoint before you run it.

Before and after

Example telemetry in, and what this exact config sent out when we ran it in otelcol-contrib v0.161.0. CI re-runs it on every change and fails if the output differs. Changed values are highlighted.

1 metric in, 1 metric out (4 data points in, 2 out)

  1. Before

    metric
    http.server.request.count
    point 1
    5 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":200,"user.id":"u-1"}
    point 2
    3 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":200,"user.id":"u-2"}
    point 3
    7 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":200,"user.id":"u-3"}
    point 4
    1 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":404,"user.id":"u-1"}
    resource.service.name
    checkout

    After

    metric
    http.server.request.count
    point 1
    15 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":200}
    point 2
    1 {"http.request.method":"GET","http.route":"/orders/{id}","http.response.status_code":404}
    resource.service.name
    checkout

The gotcha

Deleting the attribute is not enough. In our test delete_key(attributes, "user.id") left all four data points in place, three of them now with identical attributes: duplicates a backend either rejects or overwrites, so the count is wrong either way. aggregate_on_attributes merged them instead: 4 points in, 2 out, 5 + 3 + 7 = 15.

Manage this across a fleet

LinkMesh manages collector configuration across a fleet from one self-hosted control plane, so a change like this is made once rather than host by host. The first 25 Collectors are free after a no-card registration in the OpenSight Customer Portal (5 without one).

Doing this in LinkMesh: Create a custom processor