Logback lets you suppress a log event with a logger level, a TurboFilter on the context, or a Filter attached to an appender. Explain where each one runs, what it can see, and how you would use them to keep one noisy MDC-tagged request at DEBUG while everything else stays at WARN.
answer
- order: TurboFilter → level → appender filter
- TurboFilter is context-wide and pre-level; only ACCEPT can override level
- appender filter sees the built event, affects one destination
- ACCEPT short-circuits the chain, DENY drops, NEUTRAL continues
- MDC/Marker turbo filter = per-request DEBUG at WARN root
basics
~20 sLevel checks are cheapest and happen first, before any event object exists. TurboFilters run next, context-wide, on the raw arguments and MDC, and can force acceptance regardless of level. Appender filters run last, per destination, on the fully built event.
solid answer
~50 sThree gates run in order. 1. **Logger level** — an integer comparison against the effective level. No `LoggingEvent` is allocated and arguments are never formatted, so this is the only gate with essentially zero cost. 2. **TurboFilter** — registered on the `LoggerContext`, so it applies to every logger. It runs *before* the level check and sees the logger, level, raw message, argument array, throwable and the MDC. Returning `ACCEPT` overrides the level entirely; `DENY` drops; `NEUTRAL` defers to the normal level check. 3. **Appender Filter** — attached to one appender, evaluated after the event is fully built, so it can inspect formatted state. Same ACCEPT/DENY/NEUTRAL vote, but it only affects that destination. For per-request debug, put a marker or MDC key on the request and use `MDCFilter`/`MDCValueLevelPair` (the `TurboFilter` family) so matching threads are accepted at DEBUG while root stays WARN. Doing it with an appender filter would not work: the level check would already have discarded the event.
code
text · 6 linesturboFilter: MDCFilter { MDCKey=traceDebug, Value=true, OnMatch=ACCEPT }
root level: WARN
appender CONSOLE: ThresholdFilter WARN # forced DEBUG not shown here
appender FILE: (no filter) # forced DEBUG lands here
edge sets MDC traceDebug=true for sampled requests onlygo deeper
Know that the logger level is the first and cheapest gate and that appender filters narrow output per destination.
Explain the three-stage ordering, the ACCEPT/DENY/NEUTRAL contract, and that only a TurboFilter can override a level.
Design the conditional-verbose-logging pattern end to end: where the MDC key is set, sampling, which destination receives the forced lines, and the per-call cost of the filter.
Weigh dynamic verbosity against log-volume cost and abuse potential, decide whether level control belongs in config, in a TurboFilter or in an external control plane, and set the policy once for the fleet.
## Why three mechanisms exist They answer different questions. *Should this call cost anything at all?* is the level. *Should this particular request or user be treated differently, globally?* is a TurboFilter. *Should this destination receive this event?* is an appender filter. Confusing them produces both performance surprises and configurations that quietly do nothing. ## Level: the cheap gate The effective level check is a comparison of two ints on the fast path of `isDebugEnabled`-style logic. Crucially it happens before the event object exists, so a disabled call never allocates and never calls `toString()` on arguments — provided the parameterised form is used. Every other mechanism costs strictly more, which is why the level remains the primary throughput control. ## TurboFilter: global, pre-level, sees raw inputs A `TurboFilter` is added to the context (`<turboFilter class="..."/>`) rather than to a logger or an appender. It is invoked for **every** logging call in the application, before the level test, and receives the un-built inputs: `Marker`, `Logger`, `Level`, the message pattern, the argument array and the `Throwable`. It has full access to the MDC because the MDC lives on the thread. That position gives it the one power nothing else has: `FilterReply.ACCEPT` **forces** the event through even though the logger's level would have rejected it. This is the basis of *conditional verbose logging*: root stays at WARN, but a request whose MDC carries `traceDebug=true` (set at the edge for a specific tenant, user or sampled request) emits DEBUG for that thread only. The built-in `MDCFilter` and `MarkerFilter` do exactly this; `DuplicateMessageFilter` is another built-in that suppresses repeats of the same message pattern. The cost is symmetric with the power: this code runs on every call in the process, including the ones the level would have discarded for free. Keep it to a map lookup — a regex over the formatted message, or anything allocating, turns your cheapest path into your most expensive one. ## Appender filters: per-destination, post-build Filters attached to an `<appender>` run after the event exists and after it has been routed there by the additivity walk. They vote: - `DENY` — drop immediately, no later filter consulted. - `ACCEPT` — write immediately, skipping remaining filters. Note this does *not* resurrect an event the level already killed; it only short-circuits the rest of this chain. - `NEUTRAL` — continue; if the whole chain is neutral, the event is written. Canonical uses: `LevelFilter` with `onMatch=ACCEPT`/`onMismatch=DENY` to send exactly ERROR to a separate file; `ThresholdFilter` to keep console terse while the file keeps everything; `EvaluatorFilter` wrapping a `JaninoEventEvaluator` for an expression over the event, or a `OnMarkerEvaluator` for marker-based routing. A useful mental rule: **filters restrict, they cannot expand** — except for a TurboFilter's ACCEPT, which is the only upward override in the system. ## Putting it together for per-request DEBUG At the trust boundary, set an MDC key when a request qualifies (an internal header, a sampling decision, a tenant allow-list — never a client-controlled flag taken on faith, or you have handed an attacker a log-volume amplifier). Configure a TurboFilter matching that key/value with `onMatch=ACCEPT`. Leave root at WARN. Optionally add a `ThresholdFilter` on the console appender so the forced DEBUG lines land only in the file. Because the MDC is copied into the event at creation, the lines carry the correlation key, and the whole request is retrievable downstream. The two failure modes to name: forgetting that the MDC is thread-bound (an async handoff loses it unless the value is propagated explicitly), and putting the rule on an appender filter, where the level check has already discarded the event and nothing appears at all.
- Why can an appender filter's ACCEPT not resurrect an event that the logger level rejected?Because the level check happens earlier in the pipeline and no LoggingEvent is ever constructed for a rejected call, so the appender is never reached. ACCEPT in an appender filter only means stop evaluating the rest of that appender's filter chain and write. The only mechanism that can override a level is a TurboFilter returning ACCEPT, because it runs before the level check.
- What is the performance risk of a TurboFilter?It executes on every single logging call in the process, including all the calls the level check would otherwise have discarded for free, and it runs before any event is built. If it does anything more than a map or MDC lookup — regex matching, formatting, allocation, synchronisation — you have converted the cheapest path in logging into the most expensive one, and the cost scales with the volume of suppressed calls, which is usually the largest volume you have.
The level is a locked door, the TurboFilter is a doorman with a guest list who can wave someone through the locked door, and the appender filter is a bouncer inside one particular room.
saying these in an interview costs you the question
- Putting a per-request DEBUG rule on an appender filter and wondering why nothing appears
- Thinking appender-filter ACCEPT can override the logger level
- Believing TurboFilters apply only to the logger they are declared under — they are context-wide
- Setting MDC-driven verbose logging from a client-supplied header without any allow-list or sampling
- Assuming MDC survives a hand-off to another thread or executor automatically