skip to content

Logging & Observability

Logging frameworks and metrics facades across the JVM, .NET, Node and Go ecosystems. Interviewers ask because structured output and a stable facade are what make logs and metrics usable once you run more than one service.

on this pageshow

explore

questions

12

In Logback, walk through what happens between a call to logger.info(...) and a line appearing in a file: how the event reaches appenders, what additivity does, and how an encoder differs from a layout.

level: juniorimportance: must knowfreq 60%

answer

  1. effective level = own or nearest ancestor; root always set
  2. event carries MDC copy + lazy caller data
  3. additivity walks up to root — duplicates
  4. filter ACCEPT/DENY/NEUTRAL per appender
  5. encoder owns bytes/charset; layout is legacy

basics

~20 s

The logger checks its effective level, builds a LoggingEvent, then walks up the logger hierarchy appending to every appender attached along the way (additivity). Each appender runs its filters, then an encoder turns the event into bytes and writes them.

solid answer

~50 s

The call first hits a level check against the logger's **effective level** — its own level, or the nearest ancestor's if unset; root always has one, so the check is always defined. If it passes, Logback builds a `LoggingEvent` carrying level, logger name, message pattern, argument array, thread, timestamp, a copy of the MDC and an optional throwable. The event is then given to the appenders attached to that logger and — because **additivity** defaults to true — to appenders on every ancestor up to root. That is why adding a FILE appender to `com.acme` while root holds CONSOLE yields both; `additivity="false"` stops the walk at that logger and is the standard fix for duplicated lines. Inside an appender, attached filters vote, then the **encoder** converts the event to bytes and writes to the output stream. `Layout` (event → String) is the older abstraction; the encoder owns bytes, charset and line separator, which is why `PatternLayoutEncoder` (or a JSON encoder) is what you configure today.

code

text · 8 lines
text
logger com.acme.billing  -> [FILE]      additivity=true
logger ROOT              -> [CONSOLE]

info("paid") on com.acme.billing.Invoice
  -> FILE     (from com.acme.billing)
  -> CONSOLE  (from ROOT, via additivity)

set additivity=false on com.acme.billing -> FILE only

go deeper

for a junior

Be able to state the sequence: level check, event, appenders up the hierarchy, filters, encoder writes. Know that additivity explains duplicate lines.

for a middle

Add the effective-level resolution rule, the eager MDC copy versus lazy caller data, and why the parameterised call form matters.

for a senior

Discuss the cost model — immediateFlush, caller-data converters, appender locking — and use additivity deliberately to route audit or access logs to a single destination.

for a principal

Treat the appender graph as a routing topology: shared appender instances, per-destination filters and structured encoders as the contract with downstream log processing, and set org-wide conventions rather than per-service patterns.

## The pieces Logback consists of a `LoggerContext` (one configured world per application), `Logger` objects named by dot-separated strings and arranged in a hierarchy from that naming, `Appender`s that are output destinations, and `Encoder`s that serialise an event. `LoggerFactory.getLogger("com.acme.billing.Invoice")` returns a logger whose ancestors are `com.acme.billing`, `com.acme`, `com` and `ROOT`. The hierarchy is purely lexical — nothing scans classes. ## Level check first Every logger has an optional assigned level and an **effective level**: the assigned one, else the nearest ancestor's. `ROOT` is always assigned (DEBUG by default), so resolution terminates. The check is a cheap integer comparison and is why the parameterised form `log.debug("user {} failed", id)` matters: with a disabled level, no string concatenation and no `toString()` happen, because the arguments are only formatted after the check passes. ## Building the event A passing call materialises a `LoggingEvent`. Two fields deserve attention. The **MDC** is copied into the event at creation time, so a value put on the thread after the call is not visible in that line, and an asynchronous handoff later still carries the values captured here. **Caller data** (class, method, file, line) is *not* captured eagerly — deriving it requires walking the stack, so Logback computes it lazily only if the pattern asks (`%class`, `%method`, `%line`, `%caller`). Those converters are the classic reason a hot logging path is slow. ## Appender walk and additivity The event is passed to `callAppenders`, which iterates the current logger's appenders, then repeats on the parent, and so on up to root — unless a logger has `additivity="false"`, which ends the walk after its own appenders run. Consequences worth stating in an interview: - **Duplicate lines** almost always mean an appender is attached to both a specific logger and root with additivity left on. - Additivity is not a level. A logger with `additivity="false"` and no appenders of its own drops the event entirely, which is a common accidental silencing. - The same appender instance can be referenced from several loggers; it is shared, so its filters and its output ordering apply to all of them. ## Filters, then encoder Each appender may carry filters returning `ACCEPT` (write immediately, skipping remaining filters), `DENY` (drop) or `NEUTRAL` (keep evaluating; if all are neutral the event is written). This is a per-destination decision, distinct from the logger level, so one appender can be noisier than another for the same event stream. The surviving event goes to the encoder. Historically `Layout` produced a `String` and the appender decided how to write it; since Logback 0.9.19 `Encoder` owns the event-to-bytes step, including charset, the trailing separator and any header/footer. `PatternLayoutEncoder` is a bridge that wraps a `PatternLayout`; structured logging simply swaps in a JSON encoder without touching the appender. Practically: configure `<encoder>`, not `<layout>`. ## Where writes actually land `OutputStreamAppender` (the parent of console and file appenders) writes under a lock and, by default, flushes each event — safe but syscall-heavy. `immediateFlush=false` buffers and multiplies throughput at the cost of losing the tail on a hard kill. That trade-off, not the pattern string, is what usually explains logging cost in a benchmark.

  • Why is log.debug("id={}", id) preferable to log.debug("id=" + id)?
    With the placeholder form, the argument array is passed by reference and formatting happens only after the level check passes, so a disabled DEBUG level costs one integer comparison. String concatenation is evaluated by the caller before the method is even entered, so it pays for concatenation and any toString() regardless of level. It also keeps the message pattern constant, which structured encoders and log aggregators can group on.
  • A team sees every line twice in the console. What do you check?
    Almost always additivity: an appender attached to an application logger while the same appender or an equivalent console appender is also on root, so the event is written on the way up. Fix by setting additivity="false" on the specific logger, or by removing the duplicate appender reference rather than the logger. Also confirm two logging backends are not both on the classpath, which produces a similar symptom for a different reason.

Additivity is a memo dropped into a mail chute: it passes every floor's inbox on the way down to the lobby unless someone seals the chute on their floor.

saying these in an interview costs you the question

  • Thinking additivity is a level or a filter rather than the parent-appender walk
  • Claiming a logger with no level configured logs nothing — it inherits its ancestor's
  • Believing caller data (%line, %method) is free because it appears in a pattern
  • Saying the MDC is read when the line is written, so late puts show up
  • Configuring <layout> and assuming charset and line-separator handling come with it

context

open as a page

Micrometer offers Counter, Gauge, Timer and DistributionSummary. Explain how you choose between them for a given measurement, and why a Micrometer Gauge must be registered against an object it observes rather than being set imperatively.

level: juniorimportance: must knowfreq 58%

basics

~20 s

Counter is a monotonically increasing total you consume as a rate. Gauge is a value sampled at publish time that can go up or down. Timer records durations plus a count. DistributionSummary records non-time distributions. A Gauge holds the observed object weakly and reads it on publish, so instrumentation never keeps it alive.

open as a page

Why do SLF4J log calls use a message with `{}` placeholders and separate arguments instead of building the message with string concatenation, and when do you still need an `isDebugEnabled()` guard?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Placeholders defer formatting: if the level is disabled, the arguments are never converted to strings and no message is built. Concatenation pays that cost on every call. You only need an enabled-check when computing an argument is itself expensive.

open as a page

How does Logback find and load its configuration at startup, what is the precedence between logback-test.xml, logback.xml and the logback.configurationFile system property, and how do you debug a configuration that appears to be ignored?

level: middleimportance: must knowfreq 55%

basics

~20 s

On first use of a logger, Logback auto-configures: the logback.configurationFile system property wins, else logback-test.xml, else logback.xml on the classpath; with none, it falls back to a console appender at DEBUG. Its own parse errors go to an internal status list, printable via a status listener.

open as a page

In Micrometer, a meter is identified by its name plus a set of tags (key/value dimensions). What makes a tag safe or unsafe to add, and what exactly breaks — in the application process, at export/scrape time, and in the metrics backend — when tag cardinality is unbounded?

level: middleimportance: must knowfreq 52%

basics

~20 s

A tag is safe when its value set is bounded and decided by your code, not by callers. Each distinct tag combination is a permanent time series: registry memory that is never reclaimed, a larger export payload, backend index growth, and slower queries.

open as a page

What problem does the SLF4J logging facade solve on the JVM, and how does an application end up bound to one concrete logging backend at runtime?

level: middleimportance: must knowfreq 70%

basics

~20 s

SLF4J is an API-only jar that libraries compile against. At startup it discovers one provider on the classpath and delegates every call to it. The application, not the library, picks the backend; with no provider, logging becomes a no-op.

open as a page

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.

level: middleimportance: should knowfreq 42%

basics

~20 s

Level 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.

open as a page

What is SLF4J's Mapped Diagnostic Context (MDC), and why does putting a correlation identifier into it often produce missing or wrong values in a service that uses thread pools and asynchronous work?

level: middleimportance: should knowfreq 55%

basics

~20 s

MDC is a per-thread key-value map that the layout can print on every line, so you set a correlation id once instead of passing it to every log call. It is thread-local: work handed to another thread does not inherit it, and a pooled thread keeps stale entries unless cleared.

open as a page

You wrap a Logback file appender in AsyncAppender to cut latency. Explain the queue model, what queueSize, discardingThreshold, neverBlock and includeCallerData actually do, and which log lines you can silently lose.

level: seniorimportance: should knowfreq 45%

basics

~20 s

AsyncAppender puts events on a bounded blocking queue (default 256) drained by one worker thread. When the queue is 80% full it silently drops TRACE/DEBUG/INFO events by default; when full it blocks the caller unless neverBlock=true, which drops instead. Caller data is not captured unless enabled.

open as a page

In Micrometer, the same counter can be registered from several places without creating duplicate series, a counter on a step-based export registry can read as zero moments after you incremented it, and an export that fails can lose a whole interval. Explain the registry mechanics behind all three: how Micrometer decides that two registrations refer to the same meter, what accumulate-and-reset means for step meters and which interval a step meter's reported value refers to, and what a step-based registry does when a publish fails.

level: seniorimportance: should knowfreq 35%

basics

~20 s

Registration is idempotent: the same name plus tag set is the same Meter.Id, so you get the existing meter back. Step meters report the last completed interval and then reset. Publishing runs on that same step interval, and a failed batch is dropped, not retried.

open as a page

Third-party dependencies in a JVM service log through java.util.logging, Apache Commons Logging and Log4j 1.x, yet operations wants a single log stream. How do the SLF4J bridge jars solve this, and what must you avoid when installing them?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Bridge jars re-implement each legacy API on top of SLF4J so those calls land in your one backend. Rules: never ship both a bridge and the binding that routes back to the same framework (infinite loop), and exclude the original legacy jar.

open as a page

SLF4J 2.0 added a fluent logging API entered through methods such as `atInfo()` and `atDebug()`. What does it offer over the classic `info(String, Object...)` calls, and how do its key-value pairs differ from Mapped Diagnostic Context entries and from Markers?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

The fluent API builds an event step by step: message, arguments, key-value pairs, a marker, a cause, then log. Its key-value pairs are per-event structured fields; MDC entries are per-thread ambient context; markers are labels for routing and filtering, not data.

open as a page