Dynamicsampling Processor
Supported Telemetry
Overview
The Dynamic Sampling Processor performs adaptive tail-based trace sampling using first-match rules-based routing to dynamic samplers. Each sampler produces a known sample rate which is encoded asot=th in W3C TraceState for correct downstream metric weighting.
How it works
- Spans are accumulated in memory, grouped by trace ID.
- A trace becomes ready for evaluation when either of two events fires (whichever comes first):
- a root span arrives (any span with an empty
ParentSpanID), or trace_timeoutelapses since the first span of the trace arrived. This timer is set on first-seen and is never extended by subsequent spans, ensuring a predictable upper bound on buffer occupancy.
- a root span arrives (any span with an empty
- After the trigger fires, the processor pauses for
decision_delayto let in-flight straggler spans land. The same delay applies regardless of which event fired the trigger. - Rules are then evaluated in order against the accumulated trace. The first rule whose conditions all match selects the sampler; once a rule is selected, its sampler’s keep/drop decision is final and no later rules are considered. A rule with no conditions is a catch-all.
- The matched sampler produces a sample rate (1-in-N).
- The keep/drop decision is made deterministically by comparing the sample rate’s threshold against the randomness of the trace ID, using the OTel consistent probability sampling algorithm.
- Sampled traces are forwarded with two annotations on every span:
otelcol.processor.dynamic_sampling.rule: the name of the matched rule- W3C TraceState
ot=th:<hex>: the threshold encoding the effective sample rate
- The decision (sampled or dropped) is recorded in a per-trace LRU cache so that any late-arriving spans for the same trace ID are handled consistently with the original decision (see Decision cache below).
Configuration
Rules
Rules are evaluated in order; the first whose conditions all match selects the sampler. A rule with no conditions is a catch-all. Once a rule is selected, its sampler’s decision is final: a drop stays dropped, the trace is not handed to any later rule.[!WARNING] A rule with no conditions (a catch-all) placed before another rule consumes every trace and renders the later rules unreachable. The processor logs a warning at startup when it detects this configuration so it shows up in collector logs.The intended pattern is specific-conditions rules first, catch-all last:
ema_dynamic. Flipping the order so default comes first would mean default swallows every trace (including errors) and keep-errors is never reached, which is what the startup warning flags.
Each condition is a simple expression. In this initial release the supported forms are:
| Expression | Meaning |
|---|---|
field == value | Any span (or its resource) has field set to value. |
field != value | No span has field set to value. |
status.code == N | Any span has status code N (0 = Unset, 1 = Ok, 2 = Error). |
status.code != N | No span has status code N. |
Samplers
| Type | Behaviour |
|---|---|
always_sample | Keep every matching trace (sample rate 1). |
deterministic | Fixed-rate probabilistic sampling using sampling_percentage (0, 100]. |
ema_dynamic | EMA-based adaptive sampling targeting an average sampling percentage across keys. |
ema_throughput | EMA-based adaptive sampling targeting a fixed events-per-second throughput across keys. |
windowed_throughput | Sliding-window throughput sampler with separate update and lookback frequencies. |
ema_dynamic, ema_throughput, windowed_throughput) are backed by dynsampler-go.
deterministic
ema_dynamic
Adapts the sample rate per traffic key over time, keeping a target average sampling percentage across all keys.
ema_throughput
Adjusts rates per key to hit a target sustained throughput in events per second.
windowed_throughput
Sliding-window throughput sampler that decouples how often rates are recalculated (update_frequency) from the historical window used for the calculation (lookback_frequency). This reacts to traffic shifts faster than the EMA samplers at the cost of being more sensitive to short-term spikes.
Sampling keys
For samplers that acceptkey_fields, the sampling key for a trace is built by collecting distinct values of each key_field (across resource and span attributes), sorting them, and joining with the • separator. Missing fields are replaced with <missing>.
Decision cache
When a trace’s decision is finalised, the trace ID and outcome are recorded in one of two LRU caches. Any later spans that arrive for that trace ID short-circuit the accumulation path and are handled consistently with the original decision:- Sampled cache (
decision_cache.sampled_cache_size): late spans are forwarded immediately, stamped with the same rule attribute andot=thTraceState as the original batch. - Not-sampled cache (
decision_cache.non_sampled_cache_size): late spans are silently dropped.
0 disables that side of the cache. Late spans for a
decision class whose cache is disabled fall through to the normal pending-trace
path and may produce a second (possibly inconsistent) decision; with both caches
disabled, every late span falls through. Operators tune the cache sizes against
observed late-span volume.
Buffer overflow vs decision cache
The in-memory accumulation buffer is sized bynum_traces. If the buffer is full
when a span for a brand-new trace arrives, the processor evicts an existing
pending trace to make room; the evicted trace is dropped without a sampling
decision and without ot=th annotations, and is counted on
processor_dynamic_sampling_traces_evicted. The decision cache
(decision_cache.*) is a separate structure that records the outcome of
completed decisions for late-arriving spans, it does not protect pending traces
from eviction. Operators sizing the processor should watch traces_evicted and
increase num_traces if it is non-zero in steady state.
Deployment considerations
The processor accumulates spans in memory, so all spans of a given trace must reach the same processor instance. Multi-instance deployments use the same two-tier pattern as thetail_sampling processor:
Relationship to processor/tail_sampling
This processor sits next to tail_sampling rather than extending it. The two are close in shape (both buffer traces and decide once a trigger fires) but use different evaluation models, and tail_sampling’s current model has several mechanical features that a rate-bearing sampler does not fit cleanly into.
Decision semantics
Intail_sampling’s policy loop only two outcomes short-circuit evaluation: any policy returning Dropped, and (when sample_on_first_match is enabled) the first policy to return Sampled. Every other outcome, including NotSampled, lets the loop continue to subsequent policies.
All common “this trace did not pass my check” votes from existing policies (probabilistic, status_code, rate_limiting, and_policy, etc.) return NotSampled, never Dropped. Dropped is reserved for the explicit drop policy, and the final-decision composition gives Dropped precedence over Sampled.
A rate-bearing sampler dropped into this model has to pick a return value for a probabilistically-dropped trace, and both options break correctness:
- Returning
NotSampledmatches the existing convention but does not stop the loop. A later policy votingSampledwould still cause the trace to be kept, and the rate-bearing sampler’s view of what it controlled diverges from reality. Its rate calculations drift over time. - Returning
Droppedstops the loop, but it also wins precedence over everySampledvote in the composition step. An operator pairing a rate-bearing sampler with an explicitkeep-errorspolicy would expect errors to always win; instead the rate-bearing sampler’s probabilistic drop would override the keep, inverting the configured intent.
No rate or threshold in the policy contract
TheEvaluator interface returns only a decision enum. There is no way for a policy to communicate the sample rate or threshold it applied, which means ot=th cannot be emitted from inside tail_sampling without expanding the contract. The processor has roughly twenty existing policies; widening the interface would touch all of them.
sample_on_first_match would become correctness-load-bearing for one policy type only
Today sample_on_first_match is an opt-in optimization. Adding rate-bearing samplers as policies would make it mandatory for configurations using them; without it, the OR composition above produces drift. Existing tail_sampling users adding a rate-bearing policy without flipping the flag would silently get wrong rates. Validation cannot easily reject this because the flag is fine in isolation, and there is no current marker on a policy type that says “this policy requires first-match for correctness.”
Rule attribution
This processor records the matched rule name on every span in a sampled trace, which is only meaningful under first-match semantics. Undertail_sampling’s multi-policy OR model multiple policies can vote Sampled and there is no defined “winner” to attribute the trace to.
Relationship to PR #48865
In-flight work on adding tracestate handling totail_sampling’s probabilistic policy is input-driven: it reads SDK-supplied ot=th to adjust the tail probabilistic threshold (equalizing mode). This processor is output-driven: it computes a tail-stage rate and emits ot=th. The two address different problems and can ship independently.
For these reasons dynamic_sampling is a separate processor. The tail_sampling users retain the existing multi-policy model unchanged, and dynamic_sampling keeps a single evaluation model (first-match with rate-bearing samplers) end to end.
Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
otelcol_processor_dynamic_sampling_traces_active | Gauge | Traces currently in the accumulation buffer. | |
otelcol_processor_dynamic_sampling_traces_sampled | Counter | rule | Traces kept, attributed to the rule that selected them. |
otelcol_processor_dynamic_sampling_traces_dropped | Counter | rule | Traces dropped, attributed to the rule that selected them. |
otelcol_processor_dynamic_sampling_decision_sample_rate | Histogram | rule | Distribution of effective sample rates produced per rule. |
otelcol_processor_dynamic_sampling_decision_triggers | Counter | trigger | Number of trace decisions made, labelled by which event triggered them (root_span, trace_timeout). |
otelcol_processor_dynamic_sampling_traces_evicted | Counter | Traces evicted from the buffer before a decision could be made. |
Output attributes
Every span in a sampled trace is annotated with:| Attribute | Type | Example | Description |
|---|---|---|---|
otelcol.processor.dynamic_sampling.rule | string | keep-errors | Name of the rule that selected this trace. |
ot=th:<hex> per the OTel consistent probability sampling spec. The spanmetrics connector (enable_metrics_sampling_method: true) reads this field to produce correctly weighted R.E.D metrics from sampled data.
Future work
- OTTL-based conditions
- Shared-storage backed scaling for single-tier deployments
- Rule and sampler hot-reload
Last generated: 2026-07-06