Skip to content

Logging Interview Questions & Answers

16 questions Updated 2026-06-26 Share:

Spring Boot logging interview guide — the SLF4J + Logback default stack, log levels and groups, file output and rotation, logback-spring.xml profiles, structured JSON, runtime level changes via actuator, and MDC.

Read the in-depth guideSpring Boot Logging: Logback, Levels, Files, and Structured JSON(opens in new tab)
16 of 16

Out of the box Spring Boot logs through the SLF4J facade backed by the Logback implementation. spring-boot-starter-logging (pulled in transitively by every other starter) wires this up, so you get working console logging with zero configuration.

// SLF4J is the API you code against — Logback is what runs underneath
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
    // log.info(...) -> SLF4J -> Logback -> console
}

A facade like SLF4J lets you write code against one logging API while keeping the freedom to swap the underlying engine (Logback, Log4j2, java.util.logging) without touching application code.

Rule of thumb: Code against the SLF4J Logger, never against Logback directly — that's what keeps your logging implementation replaceable.

A facade decouples your code from the logging implementation. Your classes only ever import org.slf4j.Logger; whether the bytes end up in Logback, Log4j2, or JUL is a dependency-time decision, not a code change. It also unifies the dozens of logging APIs your transitive dependencies use.

<!-- Swapping engines is a build change, not a code change -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId> <!-- SLF4J + Logback -->
</dependency>

Spring Boot bridges legacy APIs (Jakarta Commons Logging, Log4j 1.x, JUL) onto SLF4J so libraries that use them still funnel through the same configuration and output as your own logs.

Rule of thumb: A facade buys you one config to rule them all and the freedom to change engines — that's why every serious Java app logs through SLF4J.

Spring Framework's internal code is written against Jakarta Commons Logging (JCL), but Spring Boot doesn't ship the real Commons Logging jar. Instead spring-jcl provides a compile-time replacement that routes JCL calls to SLF4J, so framework logs and your logs share one pipeline.

<!-- spring-jcl: a JCL API surface that delegates to SLF4J at runtime -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jcl</artifactId>
</dependency>

This is why you can set logging.level.org.springframework=DEBUG and actually see Spring's internals — they end up in the same Logback configuration as everything else. The same bridging trick handles Log4j 1.x and java.util.logging calls from other libraries.

Rule of thumb: Don't add a separate commons-logging dependency — Boot's spring-jcl already bridges it to SLF4J, and adding the real one causes duplicate-binding conflicts.

Set them as properties — no XML required. Use logging.level.<package-or-class> for a specific area and logging.level.root for the global default. Levels are case-insensitive.

logging:
  level:
    root: INFO                              # global default
    org.springframework.web: DEBUG          # whole package
    com.example.OrderService: TRACE         # a single class
    org.hibernate.SQL: DEBUG                # see executed SQL

Levels are hierarchical: a setting on a package applies to everything beneath it unless overridden by a more specific entry. You can also pass them on the command line (--logging.level.com.example=DEBUG) or via environment variables for quick, no-rebuild tweaks.

Rule of thumb: Keep root at INFO in production and raise levels narrowly on the package you're debugging — never flip the whole app to DEBUG.

A logging group maps one name to several packages so you can set their level in a single shot. Spring Boot ships two built-in groups — web and sql — and you can define your own under logging.group.*.

logging:
  group:
    payments: com.example.billing,com.example.gateway   # custom group
  level:
    web: DEBUG       # built-in: Spring web, codecs, HTTP client, etc.
    sql: DEBUG       # built-in: JDBC, Hibernate SQL, JdbcTemplate
    payments: TRACE  # flips both packages above at once

Without groups you'd repeat several logging.level.* lines and risk forgetting one. The built-in web and sql groups are especially handy for turning on request or SQL tracing during an incident.

Rule of thumb: Bundle the packages you debug together into a named group so you can raise their level with one toggle instead of hunting down every package.

By default Boot logs only to the console. Enable a file by setting logging.file.name (an exact path/filename) or logging.file.path (a directory, where Boot writes spring.log). Set one, not both.

logging:
  file:
    name: /var/log/myapp/application.log   # exact file
    # path: /var/log/myapp                 # OR a directory -> /var/log/myapp/spring.log

Once a file is configured, Boot logs to both the console and the file. The file output uses a plain (uncolored) pattern by default, and rolling is enabled automatically.

Rule of thumb: In containers, prefer console-only logging (let the platform collect stdout); use logging.file.name when you run on a VM and need a local file to tail.

Override logging.pattern.console and logging.pattern.file with Logback pattern syntax. Spring Boot exposes convenient placeholders like %clr for ANSI color and a LOG_LEVEL_PATTERN for MDC.

# Timestamp, colored level, thread, logger (40 chars), message + newline
logging.pattern.console=%d{HH:mm:ss.SSS} %clr(%-5level) [%thread] %logger{40} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n

Console output is colored by default when the terminal supports ANSI; control it with spring.output.ansi.enabled (detect/always/never). The file pattern is intentionally plain so log files stay free of escape codes.

Rule of thumb: Tweak patterns with logging.pattern.* for simple needs; if you need real structure, jump to structured JSON rather than hand-crafting an elaborate text pattern.

When file logging is on, Logback's rolling policy is active. Boot surfaces it through logging.logback.rollingpolicy.* so you can tune it without touching XML — files roll by size and/or day and old archives are pruned.

logging:
  file:
    name: /var/log/myapp/app.log
  logback:
    rollingpolicy:
      max-file-size: 50MB        # roll when a file hits this size
      max-history: 14            # keep 14 days of archives
      total-size-cap: 2GB        # hard ceiling on all archives combined
      file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz   # gzip rolled files

Without limits a log file grows until it fills the disk — a classic 3am outage. max-history and total-size-cap bound retention; max-file-size keeps individual files tail-able.

Rule of thumb: Always set max-history and total-size-cap in production so logs can never eat the disk.

Exclude the default logging starter and add spring-boot-starter-log4j2. Boot detects Log4j2 on the classpath and configures it automatically. The usual reason is performance — Log4j2's asynchronous loggers (via the LMAX Disruptor) offer very high throughput and low latency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions><exclusion>                               <!-- drop Logback -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </exclusion></exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>   <!-- add Log4j2 -->
</dependency>

You then configure it with log4j2-spring.xml instead of logback-spring.xml. For most apps Logback is perfectly fine; switch only when you've measured a logging bottleneck or your org standardizes on Log4j2.

Rule of thumb: Stay on the default Logback unless async throughput is a proven need — switching engines is a maintenance cost you should pay deliberately.

Logback reads logback.xml very early, before Spring's environment exists. Spring Boot instead recommends logback-spring.xml, which Boot loads itself — and that timing means you get Spring-only extensions and access to Spring properties and profiles.

<!-- logback-spring.xml: Boot loads this, so Spring extensions work -->
<configuration>
    <!-- include Boot's sensible console defaults -->
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <springProperty name="appName" source="spring.application.name"/>
    <springProfile name="prod">
        <root level="WARN"/>
    </springProfile>
</configuration>

With plain logback.xml, the <springProfile> and <springProperty> tags don't work because Spring hasn't initialized when Logback parses the file.

Rule of thumb: Name your config logback-spring.xml so profile- and property-aware logging is available — reserve plain logback.xml for cases where you deliberately want Logback to ignore Spring.

They are Spring Boot's Logback extensions. <springProfile> makes a config block conditional on active profiles; <springProperty> pulls a value from Spring's Environment (properties, application.yml, env vars) into a Logback variable.

<configuration>
    <!-- read a Spring property into a Logback variable -->
    <springProperty scope="context" name="logPath" source="app.log.path"/>

    <springProfile name="dev">                 <!-- only when 'dev' is active -->
        <root level="DEBUG"><appender-ref ref="CONSOLE"/></root>
    </springProfile>
    <springProfile name="prod">                <!-- only when 'prod' is active -->
        <root level="INFO"><appender-ref ref="FILE"/></root>
    </springProfile>
</configuration>

This lets a single logging config behave differently per environment — verbose console in dev, lean file or JSON output in prod — instead of maintaining separate files.

Rule of thumb: Use <springProfile> to vary appenders/levels per environment and <springProperty> to inject config values — both require the -spring filename.

Since Spring Boot 3.4, structured logging is built in — no extra library. Set the format property to a known schema (ecs for Elastic Common Schema, logstash, or gelf) and Boot emits one JSON object per line, ready for log aggregators.

logging:
  structured:
    format:
      console: ecs        # JSON on stdout (Elastic Common Schema)
      file: logstash      # different schema for the file appender
  file:
    name: /var/log/myapp/app.json

Machine-parseable JSON lets Elasticsearch/Loki/Splunk index every field — level, logger, MDC keys, stack traces — so you can query and aggregate instead of grepping free text. You can also register a custom format. Before 3.4 you'd add logstash-logback-encoder and configure an encoder manually.

Rule of thumb: On Boot 3.4+, switch the console to a structured format (ecs/logstash) whenever logs flow into a central aggregator — humans read the UI, machines read the JSON.

Use the actuator /loggers endpoint. A GET shows the configured and effective levels; a POST with a configuredLevel body changes a logger live — invaluable for debugging a production incident without redeploying.

# Inspect the current level for a package
curl localhost:8080/actuator/loggers/com.example.OrderService

# Raise it to DEBUG at runtime (POST), then set it back to null to reset
curl -X POST localhost:8080/actuator/loggers/com.example.OrderService \
     -H 'Content-Type: application/json' \
     -d '{"configuredLevel":"DEBUG"}'

You must expose the endpoint (management.endpoints.web.exposure.include=loggers) and secure it — it changes runtime behavior. Setting configuredLevel to null reverts a logger to its inherited level.

Rule of thumb: Reach for /loggers to flip a single package to DEBUG during an incident, then reset it — far safer than a redeploy, but keep the endpoint locked down.

SLF4J's {} placeholders defer message construction until the framework knows the level is enabled, avoiding wasted string building. String concatenation builds the message every time, even when the log is suppressed.

// GOOD: arguments only formatted if DEBUG is actually enabled
log.debug("Processing order {} for customer {}", orderId, customerId);

// BAD: the concatenation (and any toString) runs even when DEBUG is off
log.debug("Processing order " + orderId + " for customer " + bigObject);

// For an expensive argument, guard it explicitly:
if (log.isDebugEnabled()) log.debug("dump = {}", expensiveDump());

Beyond performance, placeholders keep messages consistent and tidy, and you should never log secrets or PII — passwords, tokens, full card or SSN numbers. Logs are widely readable and long-lived.

Rule of thumb: Always use {} placeholders, guard truly expensive arguments with isDebugEnabled(), and treat logs as public — keep secrets and PII out.

MDC (Mapped Diagnostic Context) is a per-thread key/value map SLF4J keeps; values you put in it can be printed in every log line via the pattern %X{key}. The classic use is stamping a correlation/trace ID on all logs for one request so you can follow a single transaction.

// In a filter or interceptor, at the start of each request:
MDC.put("traceId", request.getHeader("X-Trace-Id"));   // or generate a UUID
try {
    chain.doFilter(request, response);                 // all logs now carry traceId
} finally {
    MDC.clear();   // CRITICAL: clear so pooled threads don't leak context
}
# Reference the MDC key in your pattern
logging.pattern.level=%5p [%X{traceId:-}]

With Micrometer Tracing, Boot can populate traceId/spanId in the MDC for you. The non-negotiable rule is to clear the MDC after the request, since servlet threads are pooled and reused.

Rule of thumb: Put a correlation ID in the MDC per request and print it with %X{...} — but always clear it in a finally to avoid bleeding context across pooled threads.

The --debug command-line flag (or debug=true in properties) is not the same as root=DEBUG. It enables DEBUG for a curated set of core loggers (embedded server, Spring Boot, Hibernate) and prints the auto-configuration report. --trace/trace=true does the same at TRACE. Separately, Lombok's @Slf4j removes the boilerplate LoggerFactory line.

import lombok.extern.slf4j.Slf4j;

@Slf4j                                  // generates: private static final Logger log = ...
@Service
public class OrderService {
    public void place(String id) {
        log.info("Placing order {}", id);   // 'log' field provided by Lombok
    }
}

--debug is great for understanding why a bean was or wasn't auto-configured, without drowning every package in DEBUG output. @Slf4j just generates the conventional log field at compile time.

Rule of thumb: Use --debug to read the auto-config report when diagnosing startup, not as a blanket DEBUG switch — and let @Slf4j write your logger boilerplate.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel