Last updated: September 8, 2026
Mastering the OpenTelemetry Filter Processor
Collecting more telemetry doesn't automatically improve observability. Health checks, noisy debug logs, synthetic traffic, and unused metrics can add cost and clutter without helping you understand how your systems are behaving.
The OpenTelemetry Collector's filter processor gives you a way to remove such data before it reaches the next stage of your pipeline. You describe what should be dropped using the OpenTelemetry Transformation Language (OTTL), and the processor evaluates those conditions against traces, metrics, logs, and profiles.
When a filter matches, the affected telemetry doesn't continue downstream. This makes filtering useful for deterministic noise removal, but it also means a poorly designed rule can permanently remove data you later discover you needed.
Note: The configuration in this guide applies to OpenTelemetry Collector v0.146.0 and later. Earlier releases used a different configuration structure that is now deprecated.
Prerequisites
You should already be familiar with the basic structure of an OpenTelemetry Collector configuration, particularly receivers, processors, exporters, and service pipelines.
Most examples also use OTTL expressions for filtering. While you don't need extensive OTTL knowledge to follow along, reading our introductory guide will help make them more understandable.
What the OpenTelemetry filter processor does
The filter processor evaluates OTTL conditions and removes telemetry whenever a
condition evaluates to true. For example, the following processor drops spans
generated for two health-check endpoints:
123456# otelcol.yamlprocessors:filter/drop_health_checks:trace_conditions:- span.attributes["url.path"] == "/healthz"- span.attributes["url.path"] == "/readyz"
Each item in trace_conditions is an independent condition, and conditions in
the same list are combined using OR logic, so that a span whose url.path
matches either path is dropped while everything else continues to the exporter.
The processor can currently filter four telemetry signals:
| Signal | Configuration | Available contexts |
|---|---|---|
| Traces | trace_conditions | resource, scope, span, span event |
| Metrics | metric_conditions | resource, scope, metric, data point |
| Logs | log_conditions | resource, scope, log |
| Profiles | profile_conditions | resource, scope, profile |
This signal-level configuration is one of the major differences between the current processor and its older syntax (pre v0.146.0).
When you should use the filter processor
Filtering makes the most sense when you can identify telemetry that should never continue downstream based on a deterministic property.
Typical examples include:
- Health and readiness spans that add little diagnostic value,
- Telemetry from an environment you deliberately don't ingest,
- Known synthetic or test traffic,
- Debug logs that shouldn't leave a production environment,
- Metrics emitted by an unwanted instrumentation library,
- Specific span events or metric data points known to be useless, and
- Temporary containment of malformed or unexpectedly expensive telemetry.
The filter processor is less appropriate when your goal is to retain a representative subset of useful telemetry. In that situation, sampling is the appropriate technique.
It's also worth addressing unwanted telemetry at its source whenever practical. If an instrumentation library can be configured not to produce something, that's usually preferable to creating the data, transporting it to a Collector, and then discarding it.
Understanding filter conditions and context inference
The filter processor has four top-level condition lists:
123456processors:filter:trace_conditions:metric_conditions:log_conditions:profile_conditions:
Inside each list, you refer to telemetry fields using their OTTL context prefix. For example:
12345678trace_conditions:- span.name == "GET /healthz"metric_conditions:- metric.name == "go.goroutine.count"log_conditions:- log.severity_number <= SEVERITY_NUMBER_DEBUG4
The prefixes tell OTTL what type of object you're referring to. They also allow the filter processor to infer the context in which an expression needs to run.
How context inference works
The filter processor determines the evaluation context from the paths used in each condition. Consider the following:
12metric_conditions:- metric.name == "k8s.pod.status.phase" and datapoint.value_int == 4
This condition references both metric and datapoint fields. Because data
points are nested beneath metrics, the processor evaluates the expression in the
data point context, where both paths are available.
The same principle applies across other telemetry hierarchies. For example:
1234log_conditions:- >resource.attributes["deployment.environment.name"] == "production" andlog.severity_number <= SEVERITY_NUMBER_DEBUG4
This expression combines a resource attribute with a log record field. The processor infers the appropriate context and evaluates both parts together.
In most cases, you only need to reference the fields you care about and the filter processor will use those paths to determine where the condition should run.
Filtering follows the telemetry hierarchy
Conditions are evaluated from broader contexts toward more specific ones. For traces, that hierarchy is:
1234resource└── scope└── span└── span event
For metrics:
1234resource└── scope└── metric└── data point
For logs:
123resource└── scope└── log record
If telemetry at a higher level is dropped, the processor doesn't continue evaluating lower-level conditions underneath it.
Metrics have one additional behavior worth knowing: if all data points belonging to a metric are removed, the empty metric is also dropped.
Where to place the filter processor
Processors run in the order they appear in a pipeline, so filter placement affects both what the condition can match and how much work the Collector does before data is discarded.
In general, filter as early as possible after any processor that adds fields your conditions depend on:
123456789101112131415processors:filter/drop_collector_logs:error_mode: ignorelog_conditions:- >resource.attributes["k8s.namespace.name"] == "observability" andresource.attributes["k8s.container.name"] == "otel-collector"service:pipelines:logs:receivers: [filelog]processors:[memory_limiter, k8sattributes, filter/drop_collector_logs, transform]exporters: [otlp]
Here, k8sattributes must run before the filter because it populates the
k8s.namespace.name and k8s.container.name resource attributes used by the
condition. If the filter ran first, those attributes would still be nil and
the condition wouldn't match.
When tail sampling is in the same pipeline, ensure you don't filter individual spans that the sampler may need to make its decision, or you risk changing the sampling outcome and retaining incomplete traces.
Finally, remember that a processor only runs in pipelines that reference it. If a filter defines trace, metric, and log conditions but is only added to the traces pipeline, its metric and log conditions are never evaluated.
Writing OTTL filter conditions
Most useful filters rely on a small set of OTTL patterns: comparisons, Boolean
operators, existence checks, and built-in
converter functions
such as IsMatch().
1. Matching an exact value
For an exact attribute match, compare the field directly:
12trace_conditions:- span.attributes["url.path"] == "/healthz"
Remember that a matching condition means the telemetry gets dropped, not kept.
2. Combining multiple requirements with and
Use and when several facts must all be true before data is removed:
1234log_conditions:- >resource.attributes["deployment.environment.name"] == "production" andlog.severity_number <= SEVERITY_NUMBER_DEBUG4
This condition drops low-severity logs only when the resource identifies the deployment environment as production.
3. Matching either of several possibilities
Separate entries in the condition list are already ORed together:
123trace_conditions:- span.attributes["url.path"] == "/healthz"- span.attributes["url.path"] == "/readyz"
You can also use or inside one expression when that makes the relationship
clearer:
1234trace_conditions:- >span.attributes["url.path"] == "/healthz" or span.attributes["url.path"] =="/readyz"
Both configurations drop the same two sets of spans.
4. Checking whether a field exists
OTTL lets you compare a path with nil for an existence check. For example,
this removes spans without an HTTP request method:
12trace_conditions:- span.attributes["http.request.method"] == nil
The inverse removes HTTP spans:
12trace_conditions:- span.attributes["http.request.method"] != nil
The filter processor's own documentation uses these forms to distinguish between HTTP and non-HTTP spans.
5. Matching text with IsMatch()
The IsMatch() function is useful when you need a regular expression rather
than exact equality:
12trace_conditions:- IsMatch(span.name, "^(GET|HEAD) /internal/")
However, note that regex evaluation is more difficult to reason about and can add unnecessary work to a high-volume pipeline.
Some common filtering use cases
The filter processor is most useful when you can identify a class of telemetry that you've deliberately decided not to retain. In practice, that often means removing repetitive infrastructure traffic, excluding telemetry that doesn't belong in a particular pipeline, or protecting a backend from data you know is malformed.
Dropping health checks and other routine probe traffic
Health and readiness probes can generate large numbers of nearly identical spans without adding much diagnostic value during normal operation.
If you don't need those spans in your tracing backend, you can remove them using
a stable request attribute such as url.path or http.route, depending on what
your instrumentation emits.
1234567processors:filter/drop_probes:error_mode: ignoretrace_conditions:- span.attributes["url.path"] == "/healthz"- span.attributes["url.path"] == "/readyz"- span.attributes["url.path"] == "/metrics"
If part of your fleet still uses older HTTP semantic conventions, inspect the
actual spans before writing the rule. Older instrumentation may expose the
request target through attributes such as http.target, so mixed-version
environments may need to match both forms during a migration.
Be careful not to remove telemetry you actually use to diagnose probe failures. If failed health checks are operationally important, make the condition narrower so that only successful requests are dropped.
Excluding telemetry from services or environments you don't ingest
A shared Collector may receive telemetry from workloads that shouldn't all reach the same backend or pipeline.
For example, you might receive development telemetry alongside production data but only want to export the production signal:
123456789101112processors:filter/drop_development:error_mode: ignoretrace_conditions:- >resource.attributes["deployment.environment.name"] == "development"metric_conditions:- >resource.attributes["deployment.environment.name"] == "development"log_conditions:- >resource.attributes["deployment.environment.name"] == "development"
The same pattern works with attributes such as service.name,
service.namespace, or k8s.namespace.name.
If the exclusion can happen earlier, such as by configuring the receiver not to collect the workload at all, that's always preferable because the unwanted data never enters the pipeline.
Dropping low-severity logs from production
Debug logs are often useful during development but too noisy to retain from every instance of a busy production service.
You can remove them based on the log record's severity:
12345678processors:filter/drop_debug_logs:error_mode: ignorelog_conditions:- >resource.attributes["deployment.environment.name"] == "production" andlog.severity_number >= SEVERITY_NUMBER_TRACE and log.severity_number <=SEVERITY_NUMBER_DEBUG4
The lower bound matters because OpenTelemetry uses severity number 0 for
SEVERITY_NUMBER_UNSPECIFIED, which means the original severity wasn't
available or couldn't be mapped.
A condition that checks only log.severity_number <= SEVERITY_NUMBER_DEBUG4
would therefore drop both debug logs and records with an unspecified severity
which can remove much more data than intended, especially when ingesting logs
from sources where severity parsing isn't reliable.
By bounding the condition between TRACE and DEBUG4, you drop only records
that OpenTelemetry has actually classified as trace or debug severity.
Preventing telemetry feedback loops
Collectors that read container logs can accidentally ingest their own output, especially when a log exporter writes back to standard output. That can create a feedback loop where the Collector repeatedly re-ingests telemetry it just exported.
The best fix is excluding the Collector's own logs at the receiver end so they never enter the pipeline in the first place. If those logs are already reaching a shared gateway, you can still filter them, but only if you have a reliable attribute that identifies them. For example, in a Kubernetes pipeline where container metadata has already been added:
123456processors:filter/drop_collector_logs:error_mode: ignorelog_conditions:- >resource.attributes["k8s.container.name"] == "opentelemetry-collector"
Temporarily contain a metric cardinality problem
A high-cardinality metric attribute can create a large number of time series very quickly. This happens when an unbounded value such as a user ID, request ID, or session ID is accidentally used as a metric dimension.
If checkout.duration unexpectedly contains a user.id attribute, you can
temporarily drop the affected data points before they reach your backend while
you fix the instrumentation at the source:
1234567processors:filter/contain_cardinality:error_mode: ignoremetric_conditions:- >metric.name == "checkout.duration" and datapoint.attributes["user.id"]!= nil
This should not become the permanent solution because the filtered measurements disappear entirely, which means queries against the metric will undercount real traffic.
The actual fix is to remove or normalize the unbounded attribute in the instrumentation or with a transformation before export.
Removing metrics you deliberately don't use
Instrumentation libraries and infrastructure receivers often produce more metrics than is required. If you've established that a particular metric serves no dashboards, alerts, or investigations, you can remove it completely:
12345processors:filter/drop_unused_metrics:error_mode: ignoremetric_conditions:- metric.name == "process.runtime.go.mem.heap_objects"
Error handling in the filter processor
An OTTL expression can return an error while the processor evaluates telemetry,
so you must carefully control what happens next through the filter processor's
error_mode:
123processors:filter:error_mode: ignore
The available values are:
| Mode | Behavior |
|---|---|
ignore | Log the error and continue processing conditions |
silent | Ignore the error without logging it |
propagate | Return the error to the pipeline |
ignore is the default and the recommended option as it prevents a bad
condition from causing otherwise valid telemetry to be dropped. Set it
explicitly to make the intended failure behavior clear to anyone reading the
configuration.
Use silent only when evaluation errors are expected and don't need to appear
in the Collector logs, but be careful with propagate, since an evaluation
error can cause the affected payload to be dropped.
Advanced configuration and explicit contexts
For most filters, the simple condition lists shown so far are all you need. The
processor can infer the correct OTTL context from paths such as span.name,
metric.name, datapoint.attributes[...], or resource.attributes[...].
There are cases, however, where you need more control over how a condition is
evaluated. The filter processor supports an advanced form that lets you group
conditions, set the evaluation context explicitly, and override error_mode for
a particular group.
For example:
123456789processors:filter/advanced:error_mode: ignoretrace_conditions:- context: spanconditions:- IsRootSpan()- conditions:- span.status.code == STATUS_CODE_ERROR
The first group sets context: span explicitly because IsRootSpan() doesn't
contain a path such as span.name that the processor can use to infer the
context. Without that hint, it cannot know which telemetry object the function
should be evaluated against.
The second group doesn't need an explicit context. The span.status.code path
is enough for the processor to infer that the condition belongs in span context.
You can also set error_mode on an individual group when one set of conditions
needs different error handling from the processor-wide default:
123456789processors:filter/advanced:error_mode: ignoremetric_conditions:- conditions:- metric.name == "checkout.duration"- error_mode: propagateconditions:- datapoint.attributes["tenant.id"] != nil
In practice, explicit contexts are mainly useful when a condition consists of functions or expressions that don't contain enough path information for context inference. If the processor can infer the context from the expression, it's usually clearer to let it do so.
That keeps most configurations compact while still giving you a way to handle the few cases where inference isn't possible or where one group needs different evaluation behavior.
Best practices when filtering telemetry data
A filter can be perfectly valid OTTL and still leave you with worse observability.
Before you deploy one, consider what the remaining telemetry will represent once matching data has been removed. The goal isn't simply to reduce volume, but to do so without distorting the data you rely on, or hiding problems that should be fixed elsewhere.
1. Be careful when dropping individual spans
Spans are part of a larger trace, so removing one can affect the usefulness of everything around it.
If you drop a parent span but keep its children, those child spans may reach your backend without the expected parent. Logs can also retain trace and span identifiers that refer to spans you've already removed.
Filtering health-check requests is easier to reason about because the entire operation is being treated as noise. Removing arbitrary internal spans from otherwise useful traces requires much more care.
2. Don't remove your healthy baseline
A filter that keeps only slow or failed spans may reduce storage, but it also changes the population you're observing.
Once normal, successful requests have been removed, the retained traces no longer represent typical application behavior. That makes it harder to compare failures against healthy traffic or understand what normal latency looks like.
If your main goal is reducing trace volume while keeping a representative set of traces, use sampling instead.
3. Use filtering to contain instrumentation problems, not hide them
If a metric contains a high-cardinality attribute that shouldn't be there, a filter can protect your backend while you correct the instrumentation, but it shouldn't become the permanent fix.
Dropping the affected data points also removes valid measurements, which can leave dashboards and alerts working from incomplete data. Fix the attribute at the source, or transform it before export, then remove the temporary filter once the corrected telemetry is flowing.
4. Keep conditions as narrow as possible
Broad conditions are easy to write and easy to regret. For example:
12log_conditions:- IsMatch(log.body, "debug")
This drops any log whose body happens to contain the word debug, regardless of
its severity or where it came from, so a stack trace mentioning a debug flag
disappears alongside the records you actually meant to remove.
There's a second problem that's easier to miss. IsMath() expects a string-like
target, so a structured body is either coerced into a serialized form, which
means the regex starts matching against field names you never intended to
inspect.
If you do need to match against a body, guard the type first:
12log_conditions:- IsString(log.body) and IsMatch(log.body, "debug")
That at least makes the string assumption explicit rather than leaving it to the error mode. A condition based on structured fields is still easier to predict:
12345log_conditions:- >resource.attributes["deployment.environment.name"] == "production" andlog.severity_number >= SEVERITY_NUMBER_TRACE and log.severity_number <=SEVERITY_NUMBER_DEBUG4
Whenever possible, use the most specific structured fields available to identify the telemetry data you actually intend to discard.
Testing and troubleshooting filter conditions
Filtering permanently removes matching telemetry, so test new conditions before you rely on them in production.
At minimum, verify both sides of the rule: telemetry that should match must be removed, while telemetry that shouldn't match must continue through the pipeline.
Enabling debug logging
When a condition isn't behaving as expected, enable debug logging for the Collector:
1234service:telemetry:logs:level: debug
At debug level, OTTL logs details about condition evaluation, including whether
the condition matched and the TransformContext it was evaluated against. This
lets you see the telemetry fields and values OTTL actually received instead of
guessing why an expression returned an unexpected result.
For example, a matching log condition might produce debug output similar to:
1234567891011121314151617181920212223242526272829303132{"level": "debug","caller": "ottl@v0.160.0/parser.go:518","msg": "condition evaluation result","otelcol.component.id": "filter/test","otelcol.component.kind": "processor","otelcol.pipeline.id": "logs","otelcol.signal": "logs","condition": "log.attributes[\"demo.drop\"] == true","match": true,"TransformContext": {"resource": {"attributes": { "service.name": "telemetrygen" },"dropped_attribute_count": 0},"scope": { "attributes": {}, "dropped_attribute_count": 0, "name": "", "version": "" },"log_record": {"attributes": { "app": "server", "demo.drop": true },"body": "the message","dropped_attribute_count": 1,"flags": 0,"observed_time_unix_nano": 0,"severity_number": 9,"severity_text": "Info","span_id": "0000000000000000","time_unix_nano": 1788887046499785069,"trace_id": "00000000000000000000000000000000","event_name": ""},"cache": {}}}
Here, match: true confirms that the record satisfies the filter condition,
while TransformContext shows the telemetry values that OTTL evaluated. The
name field identifies which processor instance ran the condition, which
matters when a pipeline contains several named filters and you need to work out
which one removed a particular record.
Monitor how much telemetry the filter processor drops
The filter processor exposes internal metrics that count how much telemetry it removes. When the Collector exports its own telemetry over OTLP, these metrics use names such as:
1234otelcol_processor_filter_spans.filteredotelcol_processor_filter_datapoints.filteredotelcol_processor_filter_logs.filteredotelcol_processor_filter_profiles.filtered
These counters are useful both for troubleshooting and for monitoring filters after deployment. If telemetry suddenly disappears, check whether the relevant filter counter increased at the same time.
You can also alert on unexpected changes in the drop rate. A filter can remain healthy from the Collector's perspective while still matching far more telemetry than you intended.
Filtering vs other telemetry reduction techniques
Filtering is the right choice only when you want the matching telemetry to disappear completely. If you still need part of the data, or want to retain a representative subset, another Collector processor may be a better fit:
-
Use tail sampling when you want to decide which complete traces are worth keeping based on properties such as latency, errors, or attributes.
-
Use the log deduplication processor when repeated logs contain useful information but don't need to be stored individually. It groups identical log records over a time interval and emits one record with a count of how many occurrences were combined.
-
Use the redaction processor when you want to keep the telemetry but remove sensitive fields. Dropping an entire log record because one attribute contains a secret is often unnecessary if that field can be redacted instead.
-
Use the transform processor when the telemetry needs to be changed rather than removed. For example, you can delete or normalize a problematic metric attribute while preserving the measurement itself.
Filter when the whole telemetry item has no downstream value, sample when choosing which traces to retain, redact when removing sensitive fields, and transform when correcting or reshaping data.
Final thoughts
Keeping every piece of telemetry you can collect doesn't make your observability better. What helps is retaining data that's useful, that you trust, and that's proportionate to the questions you actually need to answer.
The filter processor gives you a way to enforce that judgment in the Collector. When used well, it helps reduce noise and cost, but if used poorly, it can hide important behavior, distort what remains, or turn an instrumentation problem into a data-loss problem.
If you'd rather manage filtering without maintaining Collector rules yourself, Dash0's SignalControl lets you drop noisy, low-value telemetry at ingestion before you're charged for it. You can define filters for logs, metrics, spans, and web events, then see how much data each rule keeps or drops.
Sign up today for a free 14-day trial to give it a try!
