Logging

Good logs are your best tool when something fails in production. Spring Boot uses SLF4J with Logback by default, so you can log without any setup.

Log levels

LevelUse for
TRACEVery detailed flow (rarely enabled)
DEBUGDeveloper details, for troubleshooting
INFOImportant business events (default level)
WARNUnexpected but handled situations
ERRORFailures that need attention

Logging in code

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void place(Long orderId, String user) {
        log.info("Placing order {} for user {}", orderId, user);     // use {} placeholders, not +
        try {
            // work ...
        } catch (Exception ex) {
            log.error("Order {} failed", orderId, ex);               // exception LAST: prints the stack trace
        }
        if (log.isDebugEnabled()) {
            log.debug("Details: {}", expensiveToString());           // avoid costly work when disabled
        }
    }
}

Configuration

logging.level.root=INFO
logging.level.com.example.demo=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.springframework.web=WARN

logging.file.name=logs/app.log
logging.logback.rollingpolicy.max-file-size=10MB
logging.logback.rollingpolicy.max-history=14
logging.pattern.console=%d{HH:mm:ss.SSS} %-5level [%X{requestId}] %logger{36} - %msg%n

Change the level at runtime (Actuator)

curl -X POST localhost:8080/actuator/loggers/com.example.demo \
  -H "Content-Type: application/json" -d '{"configuredLevel":"DEBUG"}'

Advanced: logback-spring.xml with profiles

<configuration>
    <springProfile name="dev">
        <root level="DEBUG"><appender-ref ref="CONSOLE"/></root>
    </springProfile>
    <springProfile name="prod">
        <root level="INFO"><appender-ref ref="CONSOLE"/></root>
    </springProfile>
</configuration>

Use the file name logback-spring.xml (not logback.xml) so Spring can process the springProfile tags.

Structured (JSON) logging

Log tools such as Elasticsearch, Loki and Splunk work best with JSON. Spring Boot 3.4 and newer can write structured logs without extra libraries:

logging.structured.format.console=ecs
# other formats: logstash, gelf

MDC: add context to every log line

MDC.put("userId", "ram");
try {
    log.info("Processing");        // pattern %X{userId} prints ram
} finally {
    MDC.clear();
}

How logging works in Spring Boot (internals)

your code:   log.info("...")                      ─► SLF4J API (facade)
                                                       ▼
                                          SLF4J binding on the classpath:  Logback (default)  or Log4j2
                                                       ▼
LoggingApplicationListener (an early ApplicationListener) initializes the LoggingSystem during startup:
   1. before the context: console logging with defaults (so startup problems are visible)
   2. after the Environment is ready: read logging.* properties, logback-spring.xml, apply levels and patterns
   3. LoggingSystem abstraction (LogbackLoggingSystem / Log4J2LoggingSystem) lets Actuator change levels at runtime
Logger hierarchy: com.example.shop.order.OrderService inherits its level from com.example.shop.order, then com.example.shop, com.example, then ROOT

Asynchronous appender: do not slow requests with disk I/O

<!-- src/main/resources/logback-spring.xml -->
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <property name="LOG_FILE" value="${LOG_FILE:-logs/app.log}"/>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>14</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
        </rollingPolicy>
        <encoder><pattern>${FILE_LOG_PATTERN}</pattern></encoder>
    </appender>

    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>8192</queueSize>
        <discardingThreshold>0</discardingThreshold>      <!-- 0 = never drop INFO and WARN when the queue is nearly full -->
        <neverBlock>false</neverBlock>
        <appender-ref ref="FILE"/>
    </appender>

    <springProfile name="prod"><root level="INFO"><appender-ref ref="ASYNC_FILE"/></root></springProfile>
    <springProfile name="!prod"><include resource="org/springframework/boot/logging/logback/console-appender.xml"/><root level="DEBUG"><appender-ref ref="CONSOLE"/></root></springProfile>
</configuration>

Keep MDC data in async code

@Configuration
public class AsyncLoggingConfig {
    // Boot applies a TaskDecorator bean to the auto-configured applicationTaskExecutor (@Async and MVC async)
    @Bean
    TaskDecorator mdcTaskDecorator() {
        return runnable -> {
            Map<String, String> context = MDC.getCopyOfContextMap();          // captured in the CALLER thread
            return () -> {
                try {
                    if (context != null) MDC.setContextMap(context);          // restored in the worker thread
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        };
    }
}

Structured JSON logs with extra fields

import static net.logstash.logback.argument.StructuredArguments.kv;   // with logstash-logback-encoder

log.info("order placed {} {}", kv("orderId", order.id()), kv("amount", order.total()));
// JSON line: {"@timestamp":"...","level":"INFO","message":"order placed orderId=42 amount=99.5","orderId":42,"amount":99.5,"traceId":"..."}
// Boot 3.4+ native structured logging: logging.structured.format.console=ecs   (no extra library)

What to log, what not to log

LogLevelExample
Business events with idsINFOOrder 42 placed for customer 7
Recoverable problemsWARNPayment gateway timeout, retrying (attempt 2)
Failures with the exceptionERRORlog.error("Order {} failed", id, ex)
Detailed flow for developersDEBUGLoaded 12 items from cache
NEVER-Passwords, tokens, full card numbers, personal data, secrets in URLs

Common mistake: Building log messages with string concatenation, for example log.debug('User ' + user). The string is created even when DEBUG is off. Use placeholders.

Interview questions

Which logging framework does Spring Boot use?

SLF4J as the API and Logback as the default implementation.

Why put the exception as the last argument?

SLF4J then prints its stack trace. If you put it inside the message with {}, only toString is printed.

How do I see SQL statements?

Set logging.level.org.hibernate.SQL=DEBUG. Avoid spring.jpa.show-sql in production because it prints to standard output without your log format.

Why does my log level change not apply to a class?

A more specific logger configuration wins. For example logging.level.com.example.shop=DEBUG is overridden by logging.level.com.example.shop.order=WARN for classes in that package.

Logback or Log4j2?

Logback is the default and enough for most systems. Log4j2 offers lower latency async logging (LMAX Disruptor). Switch by excluding spring-boot-starter-logging and adding spring-boot-starter-log4j2.