Spring BootLogback
Spring Boot

Logback

Logback is Spring Boot's default logging implementation. It is configured by logback-spring.xml, which supports Spring profile-specific appender activation, rolling file policies, async appenders, and log filtering. This entry covers full logback-spring.xml configuration, rolling policies, async appenders, filters, and profile-based appender selection.

logback-spring.xml Structure

Place logback-spring.xml in src/main/resources. Spring Boot loads it automatically and supports the <springProfile> tag for profile-conditional configuration. Use logback-spring.xml instead of logback.xml to benefit from Spring's property placeholder resolution and profile support.
XML
<!-- src/main/resources/logback-spring.xml -->
<configuration scan="true" scanPeriod="30 seconds">

  <!-- ── Import Spring Boot default base configuration ───────────── -->
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

  <!-- ── Properties ─────────────────────────────────────────────── -->
  <springProperty scope="context" name="appName"
      source="spring.application.name"
      defaultValue="application"/>
  <springProperty scope="context" name="logPath"
      source="logging.file.path"
      defaultValue="logs"/>

  <!-- ── Console appender ────────────────────────────────────────── -->
  <appender name="CONSOLE"
      class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>
        %d{HH:mm:ss.SSS} %5p [${appName},%X{traceId:-},%X{spanId:-}]
        [%X{correlationId:-}] %-40.40logger{39} : %m%n%wEx
      </pattern>
      <charset>UTF-8</charset>
    </encoder>
    <!-- Only show WARN and above in prod console -->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>WARN</level>
    </filter>
  </appender>

  <!-- ── Rolling file appender ───────────────────────────────────── -->
  <appender name="FILE"
      class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${logPath}/${appName}.log</file>
    <encoder>
      <pattern>
        %d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%X{correlationId:-}]
        [%t] %-40.40logger{39} : %m%n%wEx
      </pattern>
    </encoder>
    <rollingPolicy
        class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>
        ${logPath}/${appName}-%d{yyyy-MM-dd}.%i.log.gz
      </fileNamePattern>
      <maxFileSize>10MB</maxFileSize>
      <maxHistory>30</maxHistory>
      <totalSizeCap>1GB</totalSizeCap>
    </rollingPolicy>
  </appender>

  <!-- ── Root logger ─────────────────────────────────────────────── -->
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
  </root>

  <!-- ── Package-level overrides ─────────────────────────────────── -->
  <logger name="com.myapp"              level="DEBUG" additivity="false">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
  </logger>
  <logger name="org.springframework"    level="WARN"/>
  <logger name="org.hibernate.SQL"      level="DEBUG"/>

</configuration>

Profile-Based Appender Configuration

The <springProfile> tag activates appender blocks only when the named profile is active. This avoids maintaining separate logback files per environment — one file handles development, staging, and production through conditional blocks.
XML
<!-- src/main/resources/logback-spring.xml -->
<configuration>

  <!-- ── Development: coloured console, DEBUG ────────────────────── -->
  <springProfile name="dev,default">
    <appender name="CONSOLE_DEV"
        class="ch.qos.logback.core.ConsoleAppender">
      <encoder
          class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>
          %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5p)
          [%magenta(%X{correlationId:-none})]
          %yellow(%-40.40logger{39}) : %m%n%wEx
        </pattern>
      </encoder>
    </appender>

    <root level="DEBUG">
      <appender-ref ref="CONSOLE_DEV"/>
    </root>
    <logger name="com.myapp"           level="TRACE"/>
    <logger name="org.hibernate.SQL"   level="DEBUG"/>
  </springProfile>

  <!-- ── Staging: structured JSON + rolling file ─────────────────── -->
  <springProfile name="staging">
    <appender name="JSON_CONSOLE"
        class="ch.qos.logback.core.ConsoleAppender">
      <encoder
          class="net.logstash.logback.encoder.LogstashEncoder">
        <customFields>{"env":"staging"}</customFields>
      </encoder>
    </appender>

    <root level="DEBUG">
      <appender-ref ref="JSON_CONSOLE"/>
    </root>
  </springProfile>

  <!-- ── Production: async JSON, WARN root ───────────────────────── -->
  <springProfile name="prod">
    <appender name="JSON"
        class="ch.qos.logback.core.ConsoleAppender">
      <encoder
          class="net.logstash.logback.encoder.LogstashEncoder">
        <customFields>{"env":"production"}</customFields>
        <includeContext>true</includeContext>
        <includeMdc>true</includeMdc>
      </encoder>
    </appender>

    <!-- Async wrapper — non-blocking production logging -->
    <appender name="ASYNC_JSON"
        class="ch.qos.logback.classic.AsyncAppender">
      <appender-ref ref="JSON"/>
      <queueSize>512</queueSize>
      <discardingThreshold>0</discardingThreshold>
      <neverBlock>false</neverBlock>
      <includeCallerData>false</includeCallerData>
    </appender>

    <root level="WARN">
      <appender-ref ref="ASYNC_JSON"/>
    </root>
    <logger name="com.myapp" level="INFO" additivity="false">
      <appender-ref ref="ASYNC_JSON"/>
    </logger>
  </springProfile>

</configuration>

Rolling Policies

Logback supports three rolling policies. TimeBasedRollingPolicy rotates by time — daily, hourly, or any pattern. SizeAndTimeBasedRollingPolicy adds a file size limit so a single day's log does not grow unbounded. FixedWindowRollingPolicy keeps a numbered set of backup files.
XML
<!-- ── Daily rotation with compression ─────────────────────────────── -->
<appender name="DAILY"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>logs/application.log</file>
  <encoder>
    <pattern>%d{ISO8601} %-5p [%t] %logger{36} : %m%n</pattern>
  </encoder>
  <rollingPolicy
      class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <!-- .gz extension triggers automatic gzip compression -->
    <fileNamePattern>
      logs/application-%d{yyyy-MM-dd}.log.gz
    </fileNamePattern>
    <maxHistory>30</maxHistory>         <!-- keep 30 days -->
    <totalSizeCap>5GB</totalSizeCap>    <!-- cap total disk usage -->
  </rollingPolicy>
</appender>

<!-- ── Size and time based rotation ─────────────────────────────────── -->
<appender name="SIZED"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>logs/application.log</file>
  <encoder>
    <pattern>%d{ISO8601} %-5p [%t] %logger{36} : %m%n</pattern>
  </encoder>
  <rollingPolicy
      class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <!-- %i — index within one day when size limit is hit -->
    <fileNamePattern>
      logs/application-%d{yyyy-MM-dd}.%i.log.gz
    </fileNamePattern>
    <maxFileSize>50MB</maxFileSize>
    <maxHistory>14</maxHistory>
    <totalSizeCap>2GB</totalSizeCap>
  </rollingPolicy>
</appender>

<!-- ── Separate appender for errors only ────────────────────────────── -->
<appender name="ERROR_FILE"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>logs/error.log</file>
  <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>ERROR</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
  </filter>
  <encoder>
    <pattern>%d{ISO8601} %-5p [%X{correlationId:-}]
      %logger{36} : %m%n%ex</pattern>
  </encoder>
  <rollingPolicy
      class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <fileNamePattern>logs/error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
    <maxFileSize>10MB</maxFileSize>
    <maxHistory>90</maxHistory>   <!-- errors kept 90 days -->
  </rollingPolicy>
</appender>

<root level="INFO">
  <appender-ref ref="DAILY"/>
  <appender-ref ref="ERROR_FILE"/>  <!-- errors go to both -->
</root>

Async Appender

Synchronous appenders block the application thread while writing. The AsyncAppender wraps any appender in a non-blocking queue — the application thread enqueues the event and returns immediately while a dedicated thread drains the queue. Set discardingThreshold=0 to prevent message loss when the queue fills.
XML
<!-- ── Async wrapper for any synchronous appender ─────────────────── -->
<appender name="ASYNC_FILE"
    class="ch.qos.logback.classic.AsyncAppender">

  <!-- The real appender doing the I/O work -->
  <appender-ref ref="FILE"/>

  <!-- Queue capacity — events queued before blocking/discarding -->
  <queueSize>1024</queueSize>

  <!-- 0 = never discard (default 20 = discard when queue 80% full) -->
  <discardingThreshold>0</discardingThreshold>

  <!-- false = block when full rather than dropping events -->
  <neverBlock>false</neverBlock>

  <!-- false = faster (skips collecting caller class/method/line) -->
  <includeCallerData>false</includeCallerData>

  <!-- Max ms to wait for queue to drain on shutdown -->
  <maxFlushTime>5000</maxFlushTime>
</appender>

<root level="INFO">
  <appender-ref ref="ASYNC_FILE"/>
</root>

<!-- ── AsyncAppender configuration guide ────────────────────────────
  queueSize:           larger = more memory buffering, less blocking
  discardingThreshold: 0 = no loss, 20 = drop TRACE/DEBUG/INFO at 80%
  neverBlock:          true  = never block, may drop events
                       false = block when full, no data loss
  includeCallerData:   true  = includes class/method/line in log
                       false = faster, recommended for production
  maxFlushTime:        graceful shutdown: time to drain queue
────────────────────────────────────────────────────────────────── -->

<!-- ── Sentry appender — send errors to Sentry ───────────────────── -->
<dependency>
  io.sentry:sentry-logback:7.8.0
</dependency>

<appender name="SENTRY"
    class="io.sentry.logback.SentryAppender">
  <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>ERROR</level>
  </filter>
</appender>

Logback Filters

Filters control which log events an appender accepts. LevelFilter accepts or denies events at an exact level. ThresholdFilter accepts all events at or above a level. TurboFilter and MDCFilter make decisions based on MDC values. Custom filters implement Filter<ILoggingEvent> for application-specific rules.
XML
<!-- ── LevelFilter — exact level match ──────────────────────────────── -->
<appender name="WARN_ONLY"
    class="ch.qos.logback.core.ConsoleAppender">
  <filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>WARN</level>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
  </filter>
  <encoder><pattern>%m%n</pattern></encoder>
</appender>

<!-- ── ThresholdFilter — at or above a level ────────────────────────── -->
<appender name="INFO_UP"
    class="ch.qos.logback.core.ConsoleAppender">
  <filter
      class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>INFO</level>   <!-- accepts INFO, WARN, ERROR -->
  </filter>
  <encoder><pattern>%m%n</pattern></encoder>
</appender>

<!-- ── MDCFilter — filter based on MDC value ────────────────────────── -->
<!-- Only log events where MDC tenantId=premium -->
<appender name="PREMIUM_LOG"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
  <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
    <evaluator class="ch.qos.logback.classic.boolex.MDCValuePresentEvaluator">
      <key>tenantId</key>
      <value>premium</value>
    </evaluator>
    <onMatch>ACCEPT</onMatch>
    <onMismatch>DENY</onMismatch>
  </filter>
  <encoder><pattern>%d %m%n</pattern></encoder>
  <rollingPolicy
      class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>logs/premium-%d{yyyy-MM-dd}.log</fileNamePattern>
  </rollingPolicy>
</appender>

// ── Custom filter in Java ─────────────────────────────────────────────
public class HealthCheckFilter extends Filter<ILoggingEvent> {

    @Override
    public FilterReply decide(ILoggingEvent event) {
        // Suppress health check endpoint noise
        String msg = event.getMessage();
        if (msg != null && (
                msg.contains("GET /actuator/health") ||
                msg.contains("GET /api/v1/public/ping"))) {
            return FilterReply.DENY;
        }
        return FilterReply.NEUTRAL;
    }
}

<!-- Register custom filter in XML ──────────────────────────────────── -->
<appender name="CONSOLE"
    class="ch.qos.logback.core.ConsoleAppender">
  <filter class="com.myapp.config.HealthCheckFilter"/>
  <encoder><pattern>%m%n</pattern></encoder>
</appender>