Setting Up Logging in Spring Boot: From logback-spring.xml Basics to Production-Ready Configuration

When developing a Spring Boot application, you’ll inevitably need to configure logging. During development you want to see detailed debug information, but in production you only want to save the necessary logs to a file. This article walks through Spring Boot 3.x logging configuration step by step, from the basics to practical, production-ready setups.

Spring Boot’s default logging behavior

How to output logs from code (LoggerFactory / @Slf4j)

Before diving into configuration, let’s cover the basics of outputting logs from application code. The standard approach is to obtain a logger from SLF4J’s LoggerFactory.

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

@Service
public class OrderService {

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

    public void createOrder(String orderId) {
        log.info("注文を受け付けました: orderId={}", orderId);
        log.debug("注文処理の詳細情報: {}", orderId);
    }
}

The {} in the message is a placeholder where the argument value gets embedded. Unlike string concatenation ("orderId=" + orderId), this avoids the cost of building the string when that log level is disabled, so always use the placeholder format.

Also, avoid using System.out.println in production code. It can’t be controlled by log level, doesn’t include a timestamp or thread name, and isn’t subject to file output or rotation. If you go through a logger instead, all the configuration covered in this article applies automatically.

If your project uses Lombok, you can skip the logger declaration entirely with the @Slf4j annotation.

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class OrderService {

    public void createOrder(String orderId) {
        log.info("注文を受け付けました: orderId={}", orderId);
    }
}

Just adding @Slf4j automatically generates a logger field named log. In practice, this is the predominant style used in Spring Boot projects.

The log-level methods, in ascending order of severity, are trace / debug / info / warn / error. Here’s a rough guide for when to use each:

  • ERROR: Failures that require immediate action (caught exceptions, failed connections to external services)
  • WARN: States that won’t break things immediately but need attention (retries occurring, use of deprecated APIs)
  • INFO: Significant business events (order received, batch job started/finished)
  • DEBUG: Detailed information used for investigation during development (parameter contents, which branch was taken)
  • TRACE: Even more granular execution tracing (rarely used)

When logging an exception, pass the exception object as the last argument to have the stack trace printed.

try {
    externalApi.call();
} catch (ApiException e) {
    log.error("外部API呼び出しに失敗しました: orderId={}", orderId, e);
}

Spring Boot outputs logs from startup without any special configuration. This is because Spring Boot adopts SLF4J as its logging facade and Logback as the implementation by default.

If you’re using a starter like spring-boot-starter-web, spring-boot-starter-logging is automatically included as a dependency. By default, logs at INFO level and above are printed to the console.

You can also switch the implementation to Log4j2. Exclude spring-boot-starter-logging from the dependencies and add spring-boot-starter-log4j2 instead — as long as your application code uses the SLF4J API, no code changes are needed. That said, Logback rarely causes trouble, so unless you have a specific reason (such as an existing project already using Log4j2), sticking with the default is fine.

Changing log levels in application.properties

Let’s start with the simplest approach. Using application.properties, you can change log levels without creating an XML file.

# Log level for the entire application
logging.level.root=INFO

# Log level for a specific package
logging.level.com.example.myapp=DEBUG
logging.level.org.springframework.web=WARN

logging.level.root sets the default level for the whole application, and logging.level.<package-name> lets you fine-tune specific packages. A handy approach during development is to set only your own package to DEBUG while suppressing logs from the Spring framework itself.

Basic structure of logback-spring.xml

For more advanced configuration, create a Logback configuration file. Place it at src/main/resources/logback-spring.xml.

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

A Logback configuration file is mainly made up of three elements:

  • appender defines the log output destination and format
  • logger configures settings for a specific package
  • root is the overall default setting

By naming the file logback-spring.xml instead of logback.xml, you gain access to Spring Boot–specific features. For example, environment-specific configuration via the springProfile tag, or referencing properties defined in application.properties via the springProperty tag, don’t work with logback.xml. That’s because logback.xml is loaded directly by Logback before Spring Boot’s initialization. In Spring Boot projects, it’s safest to standardize on logback-spring.xml.

Customizing the console appender

You can freely change the console output format using pattern. Here are a few commonly used patterns.

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{40}) - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
</appender>
  • %d{...} is the timestamp format
  • %thread is the thread name
  • %-5level is the log level (- means left-aligned, padded to a width of 5 characters)
  • %logger{40} is the logger name (up to 40 characters)
  • %msg is the log message
  • %n is a newline
  • %highlight, %cyan apply coloring (easier to read in a development environment)

Colored output is convenient during development, but ANSI escape sequences aren’t needed in production environments or log files, so it’s a good idea to split the configuration by environment.

File output and log rotation

In production, you’ll want to save logs to a file. Since a log file growing without bound causes problems, in practice you should set up rotation from the start using RollingFileAppender.

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.log</file>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
        <maxFileSize>10MB</maxFileSize>
        <maxHistory>30</maxHistory>
        <totalSizeCap>1GB</totalSizeCap>
    </rollingPolicy>
</appender>

<root level="INFO">
    <appender-ref ref="CONSOLE" />
    <appender-ref ref="FILE" />
</root>
  • maxFileSize is the maximum size of a single file (switches to a new file once it exceeds 10MB)
  • maxHistory is the number of days to keep (retains 30 days’ worth)
  • totalSizeCap is the total size cap across all log files (up to 1GB)

Adding .gz to the filename causes old log files to be automatically gzip-compressed.

Important: Logback does not automatically create the logs directory. If you specify a directory that doesn’t exist, it will cause an error at startup, so either create it beforehand with mkdir logs, or specify an absolute path to a directory that’s guaranteed to exist.

Besides log file bloat and disk space, there are also failures that can be detected numerically, like HikariCP connection pool exhaustion. It’s also worth setting up metric monitoring alongside this — see the article on monitoring HikariCP’s connection pool with Micrometer and Actuator — so you can catch anomalies before you even need to look at the logs.

Switching logging configuration per environment

Structured logging (JSON output) in Spring Boot 3.4+

Starting with Spring Boot 3.4, structured logging (JSON format) is natively supported without having to write a Logback configuration file. Just add one line to application.properties.

# Output console logs as JSON in Elastic Common Schema format
logging.structured.format.console=ecs

# To structure file output as well
logging.structured.format.file=ecs

Three formats are supported: ecs (Elastic Common Schema), logstash, and gelf (Graylog, 3.5+). The output looks like this:

{"@timestamp":"2026-08-29T10:15:30.123Z","log.level":"INFO","message":"注文を受け付けました: orderId=A123","process.thread.name":"http-nio-8080-exec-1","log.logger":"com.example.myapp.OrderService"}

Since logging platforms like Elasticsearch, CloudWatch Logs, and Loki can ingest JSON as structured data, searching and aggregating on fields like orderId becomes dramatically easier. When running Spring Boot in containers such as Kubernetes, the preferred setup is usually “output JSON to standard output and let the collection platform handle it” rather than file output plus rotation. For the bigger picture of container operations, see the article on deploying a Spring Boot app to Kubernetes.

Note that this feature is only available in Spring Boot 3.4 and later. If you need JSON logging on an earlier version, use the traditional approach of incorporating the logstash-logback-encoder library into logback-spring.xml. For details, see the Structured Logging chapter of the official Spring Boot documentation.

Using Spring Boot’s Profile feature, you can apply different logging configurations for development and production environments. For more on environment-specific configuration, see the article on Profiles.

<configuration>
    <springProfile name="dev">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{40}) - %msg%n</pattern>
            </encoder>
        </appender>
        <root level="DEBUG">
            <appender-ref ref="CONSOLE" />
        </root>
    </springProfile>

    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/application.log</file>
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>10MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>1GB</totalSizeCap>
            </rollingPolicy>
        </appender>
        <root level="INFO">
            <appender-ref ref="FILE" />
        </root>
    </springProfile>
</configuration>

By specifying spring.profiles.active=prod in application.properties, the production configuration is applied. You can easily achieve an operational setup where detailed DEBUG logs are shown on the console in development, while INFO-level and above logs are saved to a file in production.

Note that in container environments (Docker, Kubernetes, etc.), it’s also common to output logs to standard output rather than to a log file, and feed them to an external log collection system. Choose based on your environment.

A practical, complete configuration example

Automatically attaching a request ID to logs with MDC

In a production environment where multiple requests are processed concurrently, it’s important to be able to trace “which request does this log entry belong to.” This is where MDC (Mapped Diagnostic Context) comes in. By storing a value in thread-local storage, it gets automatically attached to every log line emitted from that thread.

Here’s an example of issuing an ID per request using a servlet filter.

@Component
public class RequestIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        MDC.put("requestId", requestId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

Adding %X{key-name} to the log pattern outputs the MDC value.

<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>

Don’t forget to call MDC.remove() in the finally block. Because application servers reuse threads, failing to remove the value will leak it into a subsequent, unrelated request.

For an implementation that combines MDC with exception handling to achieve request tracing via a traceID, see the article on implementing a GlobalExceptionHandler for production use.

Finally, here’s a complete configuration example you can use in practice. It supports both development and production environments and includes per-package log level adjustments.

<configuration>
    <property name="LOG_PATH" value="logs" />
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" />

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/application.log</file>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <springProfile name="dev">
        <root level="DEBUG">
            <appender-ref ref="CONSOLE" />
        </root>
        <logger name="com.example.myapp" level="DEBUG" />
    </springProfile>

    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="FILE" />
        </root>
        <logger name="org.springframework" level="WARN" />
        <logger name="com.example.myapp" level="INFO" />
    </springProfile>
</configuration>

Defining variables with the <property> tag reduces duplication in the configuration and improves maintainability.

When you want to automatically include supplementary information in logs, such as a request ID or user ID, the MDC (Mapped Diagnostic Context) mechanism is handy. We’ll cover it in detail in another article, but for now it’s worth remembering as a feature that stores a value in thread-local storage and automatically attaches it to log output.

Troubleshooting logging configuration

Here’s a summary of common logging configuration issues and how to resolve them.

  • Logs aren’t being output → Check for syntax errors in logback-spring.xml. Look for a Logback configuration error in the console at startup, and confirm the file is placed at src/main/resources/logback-spring.xml
  • Can’t write to the file → Check whether the log directory exists and has write permissions
  • Log level isn’t being applied → Check the precedence of settings. In general, logback-spring.xml takes priority over application.properties, which takes priority over the default settings. A misspelled logger name (package name) is also a common cause

It’s also common to run into trouble tracing which request a log entry belongs to when an exception occurs. The article on implementing a GlobalExceptionHandler for production use also explains how to attach a traceID, so be sure to check that out as well.

Running logging in production

Let’s cover a few key points for operating logging in practice.

The basic rule is to set the level to INFO or above in production, and DEBUG in development. DEBUG and TRACE produce too much log volume, which negatively affects disk space and performance.

Consider rotation settings mandatory. A log file growing without bound can exhaust disk space and cause the application to stop.

Personal information and sensitive data (passwords, tokens, credit card numbers, etc.) must never be left in logs. Be careful not to accidentally output them during debugging.

It’s also common for logs to get cut off at deploy time, making incident investigation difficult. The article on achieving graceful shutdown and zero-downtime deployment in Spring Boot explains settings that prevent dropping requests and logs during a deploy.

Summary

This article walked through Spring Boot logging configuration step by step, from the basics to practical, production-ready setups.

Starting from simple log-level changes in application.properties, through to full-fledged configuration with logback-spring.xml, we’ve covered the knowledge you need in practice. Including environment-specific configuration switching and log rotation, you should now be able to implement a configuration that’s ready to operate in production.

Combined with logging configuration, using Spring Boot Actuator lets you monitor your application as well. With production operations in mind, be sure to give it a try.

Here are some articles related to production operations that pair well with logging configuration.

Outputting JSON logs in Logstash format (for Spring Boot below 3.4)

On versions earlier than Spring Boot 3.4, logging.structured.format isn’t available, so use the logstash-logback-encoder library to achieve similar JSON output. Just add the dependency and switch the encoder in logback-spring.xml.

<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>

This automatically outputs each log field as a JSON key, so it can be ingested as-is into logging platforms that expect Logstash format, such as Elasticsearch or Graylog.