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.
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 nameresource.attributes["linkmesh.source.name"] == "nginx-access"
# Everything except one sourceresource.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 renameresource.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 numbersseverity_number >= 17
# Body contains a substring (regex)IsMatch(body, ".*timeout.*")
# One serviceresource.attributes["service.name"] == "checkout"
# Records that carry a given attribute at allattributes["http.route"] != nil
# Numeric threshold — only on numeric attributesattributes["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
| Field | Meaning | Signals |
|---|---|---|
body | The log record body | logs |
severity_number | OTel severity (ERROR = 17, WARN = 13, INFO = 9) | logs |
attributes["key"] | Record / span / datapoint attribute | all |
resource.attributes["key"] | Resource-level attribute | all |
| Operator / function | Use |
|---|---|
== != | Equality / inequality |
> < >= <= | Numeric comparison |
IsMatch(field, "regex") | Regex test against a string field |
!= nil == nil | Attribute 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.
-
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-accessrecord first; nothing downstream sees them. -
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.
Related
- Route — the route entity: priority, is-final, pipeline, destinations.
- Source — where
linkmesh.source.name/.idcome 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.