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>