Logging Dependencies
Cover the default Logback logging starter, structured JSON logs with logstash-logback-encoder, and swapping to Log4j2 with the required exclusion.
Introduction
Every Spring Boot application logs something from the moment it starts, even if you never add a single logging dependency yourself — spring-boot-starter-web and every other starter pull in logging transitively. This lesson covers what is there by default, how to shape it into structured output, and how to replace it entirely if you need to.
- What spring-boot-starter-logging provides by default and why it is already on your classpath.
- How to configure output with logback-spring.xml.
- How to produce structured JSON logs with logstash-logback-encoder.
- How to swap to Log4j2, and why the default starter must be explicitly excluded first.
The Default: Logback
spring-boot-starter-logging is Spring Boot's default logging implementation, built on Logback, and it comes in transitively through every spring-boot-starter-* dependency — you do not add it yourself, and you will not typically see it listed explicitly in a pom.xml.
import org.slf4j.Logger;import org.slf4j.LoggerFactory;
@Servicepublic class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void placeOrder(String customerId, double amount) { log.info("Placing order for customer={} amount={}", customerId, amount);
if (amount <= 0) { log.warn("Rejected order with non-positive amount={}", amount); throw new IllegalArgumentException("Amount must be positive"); } }}Click Run to see what this code prints.
Application code talks to SLF4J (the logging facade), and Logback is simply the implementation underneath actually formatting and writing those log lines — this separation is exactly what makes it possible to swap in Log4j2 later without touching a single log.info() call.
Configuring logback-spring.xml
Placing a logback-spring.xml file in src/main/resources lets you control log levels, output format, and destinations (console, rolling file, or both) without touching application code. The "-spring" suffix matters: it lets the file use Spring profile-specific configuration, unlike a plain logback.xml.
<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender>
<root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </root>
<logger name="com.example.shop" level="DEBUG" /></configuration>Click Run to see what this code prints.
Structured JSON Logs
Plain-text log lines are easy to read on a laptop but hard for a log aggregation system (like the ELK stack or Datadog) to parse reliably. net.logstash.logback:logstash-logback-encoder formats each log line as a single JSON object instead, with fields the aggregator can index directly.
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4</version></dependency><configuration> <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder" /> </appender>
<root level="INFO"> <appender-ref ref="JSON_CONSOLE" /> </root></configuration>Click Run to see what this code prints.
Swapping to Log4j2
Some teams standardize on Log4j2 across all their services instead of Logback. Because logging comes in transitively, switching requires two steps: excluding the default logging starter, and adding the Log4j2 starter in its place.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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>Adding spring-boot-starter-log4j2 without excluding spring-boot-starter-logging first puts two competing SLF4J bindings on the classpath at once. The application will fail to start with a "Class path contains multiple SLF4J bindings" error.
Everything else — the log.info() calls scattered through the codebase, since they go through SLF4J — stays exactly the same. Only the configuration file changes from logback-spring.xml to log4j2-spring.xml.
Common Mistakes
- Adding a Log4j2 starter without excluding spring-boot-starter-logging first, causing a startup failure.
- Naming the config file logback.xml instead of logback-spring.xml when profile-specific sections are needed — plain logback.xml loads too early for Spring profiles to apply.
- Logging sensitive data (passwords, full card numbers, tokens) at INFO level where it ends up in aggregated, searchable logs.
- Leaving the root logger at DEBUG in production, which floods log storage and slows the application under load.
Frequently Asked Questions
Not strictly — you could ship plain-text logs and parse them downstream — but emitting structured JSON at the source is far more reliable than trying to regex-parse free-form text later.
Yes — define two appenders with different encoders in logback-spring.xml (a plain PatternLayout for the console, LogstashEncoder for the file) and attach both to the root logger.
SLF4J is a facade — application code depends only on its API, so the concrete implementation underneath (Logback, Log4j2) can be swapped without touching a single logging call.
Summary
Logback arrives by default through every starter, logback-spring.xml is where you shape its output, logstash-logback-encoder gets you machine-parseable JSON, and swapping to Log4j2 only requires one careful exclusion. Next, we look at the dependencies that make local development itself faster.