Logging in Spring Boot: SLF4J, Logback and Log Levels
Introduction
This tutorial explains logging in Spring Boot: how an application writes log messages, how Spring Boot formats them, and how to control what gets logged and where. We will start with the pieces involved - SLF4J, the API you call, and Logback, the library that does the work - and write log messages at each level from a small application. We will then read Spring Boot's default log format, change log levels per package, and group packages so that one setting controls several. Next we will change the format of the lines, switch to structured JSON logging with context data, and write the log to a file. Lastly, we will look atlogback-spring.xml for full control, swapping Logback for Log4j 2, and best practices.(This tutorial sets logging properties on the command line; any of them can equally go in application.properties or application.ymlClick to read tutorial: application.properties vs application.yml in Spring Boot, or in a profile-specific fileClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration.)
SLF4J and Logback
Java has several logging libraries, and an application usually contains libraries that use different ones. Spring Boot brings order with two layers -- SLF4J (Simple Logging Facade for Java) is the API: your code calls
org.slf4j.Loggermethods such asinfo()andwarn(), and does not depend on any particular logging library. - Logback is the implementation behind SLF4J that Spring Boot uses by default: it decides which messages to keep, formats them, and writes them to the console or a file.
- Bridges route the other logging APIs into SLF4J -
log4j-to-slf4jfor Log4j 2 calls andjul-to-slf4jforjava.util.logging- so every library's messages end up in the same place, in the same format.
spring-boot-starter-logging, which every starter includes (it showed up in the dependency tree in Spring Boot Starters ExplainedClick to read tutorial: Spring Boot Starters Explained with Examples). Spring Boot then configures Logback itself at startup, so an application needs no logging configuration file at all.
Writing log messages
The application below has two services in two packages, an order service and a payment service. Each gets a logger withLoggerFactory.getLogger() and logs at each of SLF4J's five levels, from the most detailed to the most severe: TRACE, DEBUG, INFO, WARN and ERROR -
com.javabrahman.boot.logging.orders - OrderService logging at every level
//OrderService.java
package com.javabrahman.boot.logging.orders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void placeOrder(int orderId, String customer, double amount) {
MDC.put("orderId", String.valueOf(orderId)); // context data, used by structured logging below
try {
log.trace("Validating order {} line by line", orderId);
log.debug("Order {} for {} has amount {}", orderId, customer, amount);
log.info("Order {} placed by {}", orderId, customer);
if (amount > 1000) {
log.warn("Order {} is unusually large: {}", orderId, amount);
}
} finally {
MDC.remove("orderId");
}
}
}
com.javabrahman.boot.logging.payments - PaymentService logging an exception
//PaymentService.java
package com.javabrahman.boot.logging.payments;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
public void charge(int orderId, String cardNumber) {
log.debug("Charging order {}", orderId);
try {
if (cardNumber.isBlank()) {
throw new IllegalArgumentException("card number is empty");
}
} catch (IllegalArgumentException e) {
log.error("Payment for order {} failed: {}", orderId, e.getMessage());
}
}
}
LoggingApp.java - calls both services
//LoggingApp.java
package com.javabrahman.boot.logging;
import com.javabrahman.boot.logging.orders.OrderService;
import com.javabrahman.boot.logging.payments.PaymentService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LoggingApp {
public static void main(String[] args) {
try (var context = SpringApplication.run(LoggingApp.class, args)) {
context.getBean(OrderService.class).placeOrder(1001, "Tom Jones", 1250.0);
context.getBean(PaymentService.class).charge(1001, " ");
}
}
}
2026-11-06T13:40:39.357+05:30 INFO 4804 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 placed by Tom Jones 2026-11-06T13:40:39.365+05:30 WARN 4804 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 is unusually large: 1250.0 2026-11-06T13:40:39.368+05:30 ERROR 4804 --- [shop] [ main] c.j.b.logging.payments.PaymentService : Payment for order 1001 failed: card number is empty
Reading the default format
Only three of the seven messages appeared: Spring Boot's default level isINFO, so TRACE and DEBUG messages are discarded. Each line has these parts -
- Date and time with milliseconds and the time-zone offset, in ISO-8601 form.
- Level -
INFO,WARN,ERROR. - Process id of the JVM, then a
---separator. - Application name in brackets, from
spring.application.name; it is left out when the name is not set. - Thread name in brackets, padded to a fixed width.
- Logger name, normally the class name, shortened to fit (
c.j.b.logging.orders.OrderService). - The message, after a colon. The
{}placeholders were filled in with the arguments.
Use placeholders, not string concatenation. With
log.debug("Order {} ...", orderId), the message string is built only if DEBUG is enabled. With log.debug("Order " + orderId + ...), it is built every time, even when the line is thrown away.
Changing log levels
Thelogging.level.<logger> property sets the level for a logger and everything below it in the package hierarchy; logging.level.root sets the default for everything. Turning on DEBUG for the order package only -
OUTPUT with logging.level.com.javabrahman.boot.logging.orders=DEBUG2026-11-06T13:40:45.998+05:30 DEBUG 28864 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 for Tom Jones has amount 1250.0 2026-11-06T13:40:46.007+05:30 INFO 28864 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 placed by Tom Jones 2026-11-06T13:40:46.007+05:30 WARN 28864 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 is unusually large: 1250.0 2026-11-06T13:40:46.009+05:30 ERROR 28864 --- [shop] [ main] c.j.b.logging.payments.PaymentService : Payment for order 1001 failed: card number is empty
WARN, to keep only warnings and errors -
OUTPUT with logging.level.root=WARN2026-11-06T13:40:53.108+05:30 WARN 18120 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 is unusually large: 1250.0 2026-11-06T13:40:53.112+05:30 ERROR 18120 --- [shop] [ main] c.j.b.logging.payments.PaymentService : Payment for order 1001 failed: card number is empty
Explanation of the output
- With
DEBUGfor the orders package, the order service's debug line appeared - but not itsTRACEline, which needsTRACE, and not the payment service's debug line, which is in another package. - With the root at
WARN, theINFOline disappeared and only the warning and the error remained. - Levels can be set for any library the same way. Common ones are
logging.level.org.springframework.web=DEBUGto see how Spring MVC handles each request, andlogging.level.org.hibernate.SQL=DEBUGto see the SQL that JPA runs. - Setting
logging.level.root=DEBUGis rarely useful: every library logs at debug, and the interesting lines drown.
Log groups
Debugging a feature often means changing the level of several packages at once. A log group gives them one name. Grouping the order and payment packages ascheckout, and setting the group to DEBUG -
OUTPUT with a checkout log group at DEBUG2026-11-06T13:40:59.708+05:30 DEBUG 16812 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 for Tom Jones has amount 1250.0 2026-11-06T13:40:59.715+05:30 INFO 16812 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 placed by Tom Jones 2026-11-06T13:40:59.716+05:30 WARN 16812 --- [shop] [ main] c.j.boot.logging.orders.OrderService : Order 1001 is unusually large: 1250.0 2026-11-06T13:40:59.718+05:30 DEBUG 16812 --- [shop] [ main] c.j.b.logging.payments.PaymentService : Charging order 1001 2026-11-06T13:40:59.718+05:30 ERROR 16812 --- [shop] [ main] c.j.b.logging.payments.PaymentService : Payment for order 1001 failed: card number is empty
web (Spring MVC, codecs and related packages) and sql (Spring JDBC, jOOQ and Hibernate SQL logging), so logging.level.web=DEBUG is the quick way to watch requests.
Changing the format
Thelogging.pattern.console property replaces the console format with your own Logback pattern. A shorter format for local development, with the time, the level, the logger's simple class name and the message, in a configuration file loaded with --spring.config.name=pattern -
pattern.properties - a custom console pattern
logging.pattern.console=%d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n
13:41:06.779 INFO OrderService - Order 1001 placed by Tom Jones 13:41:06.779 WARN OrderService - Order 1001 is unusually large: 1250.0 13:41:06.787 ERROR PaymentService - Payment for order 1001 failed: card number is empty
%d{...} is the date in the given format, %-5level the level padded to five characters, %logger{0} the logger name shortened to its last part, %msg the message and %n a line break. logging.pattern.file does the same for the log file, and logging.pattern.dateformat changes only the date part of the default format.
Structured logging in JSON
Logs read by people are best as text; logs read by machines - collected into Elasticsearch, Grafana Loki or a cloud logging service - are best as JSON, one object per line, with every part in its own field. Spring Boot has structured logging built in, with three standard formats:ecs (Elastic Common Schema), logstash and gelf (Graylog) -
OUTPUT with logging.structured.format.console=ecs{"@timestamp":"2026-11-06T08:11:13.636086800Z","log":{"level":"INFO","logger":"com.javabrahman.boot.logging.orders.OrderService"},"process":{"pid":19120,"thread":{"name":"main"}},"service":{"name":"shop","node":{}},"message":"Order 1001 placed by Tom Jones","orderId":"1001","ecs":{"version":"8.11"}}
{"@timestamp":"2026-11-06T08:11:13.676505900Z","log":{"level":"WARN","logger":"com.javabrahman.boot.logging.orders.OrderService"},"process":{"pid":19120,"thread":{"name":"main"}},"service":{"name":"shop","node":{}},"message":"Order 1001 is unusually large: 1250.0","orderId":"1001","ecs":{"version":"8.11"}}
{"@timestamp":"2026-11-06T08:11:13.683209300Z","log":{"level":"ERROR","logger":"com.javabrahman.boot.logging.payments.PaymentService"},"process":{"pid":19120,"thread":{"name":"main"}},"service":{"name":"shop","node":{}},"message":"Payment for order 1001 failed: card number is empty","ecs":{"version":"8.11"}}
Explanation of the output
- Each log event became one JSON object with named fields: the timestamp,
log.level, the process id and thread, the logger,service.namefromspring.application.name, and the message. A log collector can index and search on every field. - The order service's lines carry an
orderIdfield. It came from the MDC (Mapped Diagnostic Context):MDC.put("orderId", ...)attaches a value to every log line written by that thread until it is removed. Putting a request id or order id in the MDC lets you find every line that belongs to one request. - The payment service's error line has no
orderId, becauseplaceOrder()removed it from the MDC in itsfinallyblock - always clean up the MDC, since threads are reused. logging.structured.format.fileswitches the log file to JSON independently, so the console can stay human-readable.
Writing the log to a file
By default Spring Boot logs only to the console, which suits containers, where the platform collects the console output. To also write a file, setlogging.file.name (a file) or logging.file.path (a directory, in which Boot writes spring.log) -
OUTPUT - the contents of logs/shop.log2026-11-06T13:41:21.065+05:30 INFO 1124 --- [shop] [main] c.j.boot.logging.orders.OrderService : Order 1001 placed by Tom Jones 2026-11-06T13:41:21.071+05:30 WARN 1124 --- [shop] [main] c.j.boot.logging.orders.OrderService : Order 1001 is unusually large: 1250.0 2026-11-06T13:41:21.077+05:30 ERROR 1124 --- [shop] [main] c.j.b.logging.payments.PaymentService : Payment for order 1001 failed: card number is empty
Explanation of the output
- The console output of this run was discarded, and the box shows the file Spring Boot wrote: the same lines, in the same default format.
- The file rolls over when it reaches 10 MB by default, and old files are kept for 7 days;
logging.logback.rollingpolicy.max-file-sizeandlogging.logback.rollingpolicy.max-historychange these limits. - Two levels were combined in this run:
root=OFFsilenced everything, thencom.javabrahman=INFOswitched our own packages back on - a more specific logger always wins over a more general one.
Full control with logback-spring.xml
Properties cover most needs. For anything more - several files, different levels per appender, sending some loggers to a separate file - put a Logback configuration file insrc/main/resources. Name it logback-spring.xml rather than logback.xml: with the -spring name, Spring Boot loads it after the environment is ready, so it can use Spring extensions such as <springProfile> and <springProperty> -
logback-spring.xml - Spring Boot's defaults plus a profile-specific level
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<springProfile name="dev">
<logger name="com.javabrahman" level="DEBUG"/>
</springProfile>
</configuration>
include lines reuse Spring Boot's own defaults and console appender, so the format stays the same and only the additions are yours.
Using Log4j 2 instead of Logback
Spring Boot also supports Log4j 2. Exclude the logging starter fromspring-boot-starter (or from the web starter, which includes it) and add the Log4j 2 starter -
pom.xml - replacing Logback with Log4j 2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<exclusions>
<exclusion>
<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>
</dependency>
logging.* properties work the same way, and a log4j2-spring.xml file takes the place of logback-spring.xml.
Logging best practices
- Use
INFOfor events that matter to someone running the application (order placed, job finished),WARNfor unexpected but handled situations,ERRORfor failures that need attention, andDEBUGfor details needed while investigating. - Always use
{}placeholders, and pass an exception as the last argument (log.error("Payment failed", e)) when you want its stack trace logged. - Never log passwords, tokens, full card numbers or personal data that the log's readers should not see.
- Put identifiers such as a request id in the MDC, and use structured logging wherever logs are collected by a tool.
- Configure levels in properties, per environment through profiles, rather than in code.
Summary
In this tutorial we first saw how SLF4J, Logback and the logging bridges fit together, and logged messages at all five levels from two services. We read Spring Boot's default format and saw that onlyINFO and above are kept by default. We then changed levels per package and for the root, grouped two packages into a log group, and replaced the console pattern. Next we switched to structured JSON logging in the ECS format, with an order id carried through the MDC, and wrote the log to a rolling file. Lastly, we looked at logback-spring.xml, replacing Logback with Log4j 2, and logging best practices. The next tutorial makes the edit-and-run cycle faster: Spring Boot DevToolsClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity.Spring Boot Tutorials
Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Type-Safe Configuration with @ConfigurationPropertiesClick to read tutorial: Type-Safe Configuration with @ConfigurationProperties
Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Type-Safe Configuration with @ConfigurationPropertiesClick to read tutorial: Type-Safe Configuration with @ConfigurationProperties