Skip to content

Filter records on a route

Every route has an input filter: an OTTL boolean expression that decides which incoming records the route accepts. This page is the practical guide to writing one — targeting a specific source, scoping to a signal type, matching on content, and reasoning about which route wins when several could match.

The model in one paragraph

Each source on a collector fans out to every route deployed there. A route’s input filter selects the records it accepts; matched records run through the route’s pipeline and go to its destinations. An empty filter — or the literal true — is a catch-all that accepts everything. You set it on the Collector (or Group) detail page → Routing tab → the route’s input-filter field.

A route expanded in the Routing tab — the input-filter matcher sits in the middle, between the route identity and its pipeline + destinations.

Priority and is-final decide who wins

Routes evaluate in priority order — lower numbers first (default 100). What happens on a match depends on is-final:

  • Final (the default): the first matching route consumes the record; lower-priority routes never see it. This is classic first-match-wins.
  • Not final: the record runs through this route and continues down the chain, so more than one route can act on it — the “also copy to a backup destination” pattern.

On the topology canvas, each route shows a badge per source indicating whether it statically matches that source: in (accepts), out (excluded), or ? when the match can’t be resolved without seeing live data. Only filters written against the injected source attributes (below) resolve to in/out; content and signal filters show ? — they still work at runtime, the canvas just can’t predict them.

Filter by source

Every source stamps two resource attributes onto its records at the receiver, so you can target a source by name or by its stable id:

# Exactly one source, by name
resource.attributes["linkmesh.source.name"] == "nginx-access"
# Everything except one source
resource.attributes["linkmesh.source.name"] != "debug-firehose"
# Sources whose name matches a pattern (regex)
IsMatch(resource.attributes["linkmesh.source.name"], "prod-.*")
# By stable id — survives a source rename
resource.attributes["linkmesh.source.id"] == "6a2efc8073848d5115d32d5d"

Filter by signal type

There is no signal == "logs" field — signal scoping is structural, not something you write in the filter expression. A route processes a source’s logs, metrics, or traces only where the referenced pipeline’s signal types overlap the source’s signals.

To make a route logs-only (or metrics-only / traces-only), point it at a pipeline whose signal types are scoped to that signal. If a route’s pipeline can’t process a signal the source emits, the canvas shows a signal-mismatch indicator on that edge — the record simply isn’t routed for that signal.

Filter by content — a cookbook

Each entry is a want → expression. The fields available are body, severity_number (logs only), attributes["key"] (record/span/datapoint attributes), and resource.attributes["key"] (resource-level attributes).

# Errors and above only (logs) — 17 is ERROR in OTel severity numbers
severity_number >= 17
# Body contains a substring (regex)
IsMatch(body, ".*timeout.*")
# One service
resource.attributes["service.name"] == "checkout"
# Records that carry a given attribute at all
attributes["http.route"] != nil
# Numeric threshold — only on numeric attributes
attributes["http.status_code"] >= 500
# Combine conditions (AND)
resource.attributes["service.name"] == "checkout" and severity_number >= 17
# Either/or (OR)
IsMatch(body, "ERROR") or IsMatch(body, "FATAL")

Field & operator reference

FieldMeaningSignals
bodyThe log record bodylogs
severity_numberOTel severity (ERROR = 17, WARN = 13, INFO = 9)logs
attributes["key"]Record / span / datapoint attributeall
resource.attributes["key"]Resource-level attributeall
Operator / functionUse
==   !=Equality / inequality
>   <   >=   <=Numeric comparison
IsMatch(field, "regex")Regex test against a string field
!= nil   == nilAttribute exists / is absent
and   or   not (...)Combine / negate conditions

Validate before you ship

Don’t guess. Open the pipeline attached to the route and use the dry-run panel to paste sample records and confirm the expression keeps exactly what you intend — especially for content and numeric filters, where a type mismatch silently fails to match. Then watch the route’s throughput on the canvas after saving to confirm live records flow as predicted.

Worked example — first match wins

Two sources land on a collector: nginx-access (chatty) and everything else. You want nginx access logs handled by a dedicated pipeline and the rest by a general one.

  1. Route A — priority 10, final. Input filter:

    resource.attributes["linkmesh.source.name"] == "nginx-access"

    Pipeline: your nginx-tuned recipe. Because it’s final and lowest-numbered, it grabs every nginx-access record first; nothing downstream sees them.

  2. Route B — priority 20, final. Input filter true (catch-all). It receives everything Route A didn’t consume — i.e. all non-nginx records.

On the canvas, Route A shows in for nginx-access and out for the other source; Route B shows in for both (it’s a catch-all), but at runtime only the leftovers reach it because Route A is final.

Now fan out instead of consume: flip Route A to not final. Its records still flow through Route A and continue to Route B, so nginx-access logs are processed by both pipelines — the “process normally, plus copy everything to a backup destination” pattern. The badges don’t change; the difference is purely whether Route A consumes or passes the record on.

  • Route — the route entity: priority, is-final, pipeline, destinations.
  • Source — where linkmesh.source.name / .id come from.
  • Drop noisy logs — dropping records inside a pipeline (opposite match semantics to a route filter).
  • Build your first pipeline — the pipeline a route hands matched records to.