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
| Level | Use for |
|---|---|
| TRACE | Very detailed flow (rarely enabled) |
| DEBUG | Developer details, for troubleshooting |
| INFO | Important business events (default level) |
| WARN | Unexpected but handled situations |
| ERROR | Failures 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%nChange 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, gelfMDC: add context to every log line
MDC.put("userId", "ram");
try {
log.info("Processing"); // pattern %X{userId} prints ram
} finally {
MDC.clear();
}- Never log passwords, tokens or personal data.
- Log with meaning: include ids (order id, user id) so you can search.
- Do not log and rethrow the same exception in every layer. That creates duplicate stack traces. Log once, where you handle it.
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 ROOTAsynchronous 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
| Log | Level | Example |
|---|---|---|
| Business events with ids | INFO | Order 42 placed for customer 7 |
| Recoverable problems | WARN | Payment gateway timeout, retrying (attempt 2) |
| Failures with the exception | ERROR | log.error("Order {} failed", id, ex) |
| Detailed flow for developers | DEBUG | Loaded 12 items from cache |
| NEVER | - | Passwords, tokens, full card numbers, personal data, secrets in URLs |
- Log an exception once, where it is handled, not in every layer.
- Logs at request level: method, path, status, duration, user id and trace id.
- Change to DEBUG for one package at runtime via /actuator/loggers instead of redeploying.
- Ship logs to a central place (ELK, Loki, CloudWatch) as JSON, and alert on ERROR rate.
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.