The end result of the code developed in this document can be found in the GitHub monorepo
springboot-demo-projects , under the tag
logs.
How Spring Boot Logs
In Spring Boot, SLF4J and Logback work together to provide a logging setup.
SLF4J = S imple L ogging F acade for J ava. Think of it as a logging API (an abstraction layer). Your application code talks to SLF4J, not directly to a specific logging implementation. This is brilliant because it decouples your code from how logs are written.
Logback = The actual logging implementation (the engine that does the heavy lifting). It's the default logging backend that SLF4J uses in a Spring Boot application.
Even though Spring Boot includes SLF4J and Logback automatically and no setup is required to get basic logging working, improving the logging experience is highly recommended. Structured logging with trace IDs, centralized sensitive data masking, and consistent request/response logging make debugging production issues significantly easier.
File Tree
├── build.gradle ├── ... └── src ├── main │ ├── java │ │ └── dev │ │ └── pollito │ │ └── spring_java │ │ ├── config │ │ │ ├── log │ │ │ │ ├── LogAspect.java │ │ │ │ ├── LogFilter.java │ │ │ │ ├── MaskingPatternLayout.java │ │ │ │ └── TraceIdFilter.java │ │ │ └── ... │ │ └── ... │ └── resources │ ├── application-dev.yaml │ └── ... └── test └── ...
Expand(10 more lines) File Tree
├── build.gradle.kts ├── ... └── src ├── main │ ├── kotlin │ │ └── dev │ │ └── pollito │ │ └── spring_kotlin │ │ ├── config │ │ │ ├── log │ │ │ │ ├── LogAspect.kt │ │ │ │ ├── LogFilter.kt │ │ │ │ ├── MaskingPatternLayout.kt │ │ │ │ └── TraceIdFilter.kt │ │ │ └── ... │ │ └── ... │ └── resources │ ├── application-dev.yaml │ └── ... └── test └── ...
Expand(10 more lines) File Tree
├── build.gradle ├── ... └── src ├── main │ ├── groovy │ │ └── dev │ │ └── pollito │ │ └── spring_groovy │ │ ├── config │ │ │ └── log │ │ │ ├── LogAspect.groovy │ │ │ ├── LogFilter.groovy │ │ │ ├── MaskingPatternLayout.groovy │ │ │ └── TraceIdFilter.groovy │ │ └── ... │ └── resources │ ├── application-dev.yaml │ └── ... └── test └── ...
Expand(9 more lines)
Dependencies
build.gradle
dependencies { implementation 'org.aspectj:aspectjtools:1.9.25.1' implementation 'org.springframework.boot:spring-boot-starter-opentelemetry' } build.gradle.kts
dependencies { implementation ( "io.github.oshai:kotlin-logging-jvm:7.0.13" ) implementation ( "org.aspectj:aspectjtools:1.9.25.1" ) implementation ( "org.springframework.boot:spring-boot-starter-opentelemetry" ) } build.gradle
dependencies { implementation 'org.aspectj:aspectjtools:1.9.25.1' implementation 'org.springframework.boot:spring-boot-starter-opentelemetry' }
Logging in Kotling Projects
kotlin-logging is a wrapper
around SLF4J that provides a more idiomatic Kotlin way to log messages. It
does not replace SLF4J or Logback; instead, it simplifies logging calls in
Kotlin by using extension functions and lazy evaluation for messages. The
logging hierarchy remains: Kotlin code → kotlin-logging → SLF4J → Logback
(Spring Boot default)
Avoid Local OTLP Log Publishing
When you add spring-boot-starter-opentelemetry to your dependencies, Spring Boot automatically configures OpenTelemetry exporters to publish metrics and traces via OTLP (OpenTelemetry Protocol).
The problem is that on local development, you typically don't have an OTLP collector running. Spring Boot will repeatedly try to connect to the default OTLP endpoint and fail, spamming your console with connection error messages that drown out your actual application logs.
To avoid this noise during local development, disable OTLP metrics export in your dev profile:
resources/application-dev.yaml
spring : application : name : spring_java management : otlp : metrics : export : enabled : false resources/application-dev.yaml
spring : application : name : spring_kotlin management : otlp : metrics : export : enabled : false resources/application-dev.yaml
spring : application : name : spring_groovy management : otlp : metrics : export : enabled : false
From now on, run the bootRun Gradle task with the SPRING_PROFILES_ACTIVE=dev environment variable to activate the dev profile:
SPRING_PROFILES_ACTIVE = dev ./gradlew bootRun
Log Aspect
Using an Aspect for logging is a classic example of AOP in action. Instead of scattering log statements all over your business logic (which clutters the code and mixes concerns), you define the logging behavior once in a separate module. This aspect then automatically intercepts method calls you care about and applies the logging logic without the target code even knowing it's happening.
For a deeper dive, check out the AOP section on Cross-Cutting Concerns .
java/dev/pollito/spring_java/config/log/LogAspect.java
package dev . pollito . spring_java . config . log ; import java . util . Arrays ; import lombok . extern . slf4j . Slf4j ; import org . aspectj . lang . JoinPoint ; import org . aspectj . lang . annotation . AfterReturning ; import org . aspectj . lang . annotation . Aspect ; import org . aspectj . lang . annotation . Before ; import org . aspectj . lang . annotation . Pointcut ; import org . jspecify . annotations . NonNull ; import org . springframework . stereotype . Component ; @Aspect @Component @Slf4j public class LogAspect { @Pointcut ( "within(@org.springframework.web.bind.annotation.RestController *)" ) public void controllerPublicMethodsPointcut ( ) { } @Before ( "controllerPublicMethodsPointcut()" ) public void logBefore ( @NonNull JoinPoint joinPoint ) { log . info ( "[{}] Args: {}" , joinPoint . getSignature ( ) . toShortString ( ) , Arrays . toString ( joinPoint . getArgs ( ) ) ) ; } @AfterReturning ( pointcut = "controllerPublicMethodsPointcut()" , returning = "result" ) public void logAfterReturning ( @NonNull JoinPoint joinPoint , Object result ) { log . info ( "[{}] Response: {}" , joinPoint . getSignature ( ) . toShortString ( ) , result ) ; } }
Expand(19 more lines) kotlin/dev/pollito/spring_kotlin/config/log/LogAspect.kt
package dev . pollito . spring_kotlin . config . log import io . github . oshai . kotlinlogging . KotlinLogging import org . aspectj . lang . JoinPoint import org . aspectj . lang . annotation . AfterReturning import org . aspectj . lang . annotation . Aspect import org . aspectj . lang . annotation . Before import org . aspectj . lang . annotation . Pointcut import org . springframework . stereotype . Component private val log = KotlinLogging . logger { } @Aspect @Component class LogAspect { @Pointcut ( "within(@org.springframework.web.bind.annotation.RestController *)" ) fun controllerPublicMethodsPointcut ( ) { } @Before ( "controllerPublicMethodsPointcut()" ) fun logBefore ( joinPoint : JoinPoint ) { log . info { "[ ${ joinPoint . signature . toShortString ( ) } ] Args: ${ joinPoint . args . contentToString ( ) } " } } @AfterReturning ( pointcut = "controllerPublicMethodsPointcut()" , returning = "result" ) fun logAfterReturning ( joinPoint : JoinPoint , result : Any ? ) { log . info { "[ ${ joinPoint . signature . toShortString ( ) } ] Response: $ result " } } }
Expand(20 more lines) groovy/dev/pollito/spring_groovy/config/log/LogAspect.groovy
package dev . pollito . spring_groovy . config . log import groovy . transform . CompileStatic import groovy . util . logging . Slf4j import org . aspectj . lang . JoinPoint import org . aspectj . lang . annotation . AfterReturning import org . aspectj . lang . annotation . Aspect import org . aspectj . lang . annotation . Before import org . aspectj . lang . annotation . Pointcut import org . springframework . stereotype . Component @Aspect @Component @Slf4j @CompileStatic class LogAspect { @Pointcut ( "within(@org.springframework.web.bind.annotation.RestController *)" ) void controllerPublicMethodsPointcut ( ) { } @Before ( "controllerPublicMethodsPointcut()" ) void logBefore ( JoinPoint joinPoint ) { log . info "[ ${ joinPoint . signature . toShortString ( ) } ] Args: ${ joinPoint . args *. toString ( ) . join ( ', ' ) } " } @AfterReturning ( pointcut = "controllerPublicMethodsPointcut()" , returning = "result" ) void logAfterReturning ( JoinPoint joinPoint , Object result ) { log . info "[ ${ joinPoint . signature . toShortString ( ) } ] Response: ${ result } " } }
Expand(19 more lines)
Log Filter
Sometimes, a request might not even reach your Spring MVC controllers. By logging at the Servlet level, it grants you visibility into every incoming request and outgoing response, regardless of whether it hits your application's specific endpoints.
This can be incredibly useful for debugging issues like authentication failures, routing problems, or requests that are blocked higher up in the filter chain.
java/dev/pollito/spring_java/config/log/LogFilter.java
package dev . pollito . spring_java . config . log ; import static java . util . Collections . list ; import jakarta . servlet . FilterChain ; import jakarta . servlet . ServletException ; import jakarta . servlet . http . HttpServletRequest ; import jakarta . servlet . http . HttpServletResponse ; import java . io . IOException ; import java . util . ArrayList ; import java . util . List ; import lombok . extern . slf4j . Slf4j ; import org . jspecify . annotations . NonNull ; import org . springframework . core . annotation . Order ; import org . springframework . stereotype . Component ; import org . springframework . web . filter . OncePerRequestFilter ; @Component @Order ( ) @Slf4j public class LogFilter extends OncePerRequestFilter { @Override protected void doFilterInternal ( @NonNull HttpServletRequest request , @NonNull HttpServletResponse response , @NonNull FilterChain filterChain ) throws ServletException , IOException { logRequestDetails ( request ) ; filterChain . doFilter ( request , response ) ; logResponseDetails ( response ) ; } private void logRequestDetails ( @NonNull HttpServletRequest request ) { log . info ( ">>>> Method: {}; URI: {}; QueryString: {}; Headers: {}" , request . getMethod ( ) , request . getRequestURI ( ) , request . getQueryString ( ) , headersToString ( request ) ) ; } private @NonNull String headersToString ( @NonNull HttpServletRequest request ) { List < String > headers = new ArrayList < > ( ) ; List < String > headerNames = list ( request . getHeaderNames ( ) ) ; for ( String headerName : headerNames ) { if ( headerName != null && ! headerName . isBlank ( ) ) { String headerValue = request . getHeader ( headerName ) ; if ( headerValue != null && ! headerValue . isBlank ( ) ) { headers . add ( headerName + ": " + headerValue ) ; } } } if ( headers . isEmpty ( ) ) { return "{}" ; } return "{" + String . join ( ", " , headers ) + "}" ; } private void logResponseDetails ( @NonNull HttpServletResponse response ) { log . info ( "<<<< Response Status: {}" , response . getStatus ( ) ) ; } }
Expand(54 more lines) kotlin/dev/pollito/spring_kotlin/config/log/LogFilter.kt
package dev . pollito . spring_kotlin . config . log import io . github . oshai . kotlinlogging . KotlinLogging import jakarta . servlet . FilterChain import jakarta . servlet . ServletException import jakarta . servlet . http . HttpServletRequest import jakarta . servlet . http . HttpServletResponse import java . io . IOException import org . springframework . core . Ordered . LOWEST_PRECEDENCE import org . springframework . core . annotation . Order import org . springframework . stereotype . Component import org . springframework . web . filter . OncePerRequestFilter private val log = KotlinLogging . logger { } @Component @Order ( LOWEST_PRECEDENCE ) class LogFilter : OncePerRequestFilter ( ) { @Throws ( ServletException :: class , IOException :: class ) override fun doFilterInternal ( request : HttpServletRequest , response : HttpServletResponse , filterChain : FilterChain , ) { logRequestDetails ( request ) filterChain . doFilter ( request , response ) logResponseDetails ( response ) } private fun logRequestDetails ( request : HttpServletRequest ) { log . info { ">>>> Method: ${ request . method } ; URI: ${ request . requestURI } ; QueryString: ${ request . queryString } ; Headers: ${ headersToString ( request ) } " } } private fun headersToString ( request : HttpServletRequest ) : String { val headers = request . headerNames . toList ( ) . filter { ! it . isNullOrBlank ( ) } . mapNotNull { headerName -> val headerValue = request . getHeader ( headerName ) if ( ! headerValue . isNullOrBlank ( ) ) { " $ headerName : $ headerValue " } else { null } } return if ( headers . isEmpty ( ) ) { "{}" } else { headers . joinToString ( separator = ", " , prefix = "{" , postfix = "}" ) } } private fun logResponseDetails ( response : HttpServletResponse ) { log . info { "<<<< Response Status: ${ response . status } " } } }
Expand(48 more lines) groovy/dev/pollito/spring_groovy/config/log/LogFilter.groovy
package dev . pollito . spring_groovy . config . log import groovy . transform . CompileStatic import groovy . util . logging . Slf4j import jakarta . servlet . FilterChain import jakarta . servlet . ServletException import jakarta . servlet . http . HttpServletRequest import jakarta . servlet . http . HttpServletResponse import org . springframework . core . annotation . Order import org . springframework . stereotype . Component import org . springframework . web . filter . OncePerRequestFilter @Component @Order ( 2147483647 ) @Slf4j @CompileStatic class LogFilter extends OncePerRequestFilter { @Override protected void doFilterInternal ( HttpServletRequest request , HttpServletResponse response , FilterChain filterChain ) throws ServletException , IOException { logRequestDetails ( request ) filterChain . doFilter ( request , response ) logResponseDetails ( response ) } private static void logRequestDetails ( HttpServletRequest request ) { log . info ( ">>>> Method: {}; URI: {}; QueryString: {}; Headers: {}" , request . method , request . requestURI , request . queryString , headersToString ( request ) ) } private static String headersToString ( HttpServletRequest request ) { def headers = request . headerNames . toList ( ) . findAll { it && it . trim ( ) } . collect { headerName -> def headerValue = request . getHeader ( headerName ) if ( headerValue && headerValue . trim ( ) ) { " ${ headerName } : ${ headerValue } " } else { null } } . findAll { it != null } if ( headers . isEmpty ( ) ) { "{}" } else { headers . join ( ", " ) . with { "{ ${ it } }" } } } private static void logResponseDetails ( HttpServletResponse response ) { log . info ( "<<<< Response Status: {}" , response . status ) } }
Expand(48 more lines)
Tracing
Ever tried to debug an issue in a busy system by trawling through a massive log file? It's like trying to find a specific needle in a haystack of needles.
This is where tracing comes in. By assigning a unique ID (a 'Trace ID') to each incoming request, you can tag every single log entry generated during that request's lifecycle. Suddenly, you can filter the entire log file to see the journey of just one request across multiple methods, services, or threads.
java/dev/pollito/spring_java/config/log/TraceIdFilter.java
package dev . pollito . spring_java . config . log ; import static io . opentelemetry . api . trace . Span . current ; import static org . slf4j . MDC . put ; import static org . slf4j . MDC . remove ; import static org . springframework . core . Ordered . LOWEST_PRECEDENCE ; import io . opentelemetry . api . trace . SpanContext ; import jakarta . servlet . FilterChain ; import jakarta . servlet . ServletException ; import jakarta . servlet . http . HttpServletRequest ; import jakarta . servlet . http . HttpServletResponse ; import java . io . IOException ; import org . jspecify . annotations . NonNull ; import org . springframework . core . annotation . Order ; import org . springframework . stereotype . Component ; import org . springframework . web . filter . OncePerRequestFilter ; @Component @Order ( LOWEST_PRECEDENCE - 1 ) public class TraceIdFilter extends OncePerRequestFilter { @Override protected void doFilterInternal ( @NonNull HttpServletRequest request , @NonNull HttpServletResponse response , @NonNull FilterChain filterChain ) throws ServletException , IOException { SpanContext spanContext = current ( ) . getSpanContext ( ) ; if ( spanContext . isValid ( ) ) { put ( "trace_id" , spanContext . getTraceId ( ) ) ; put ( "span_id" , spanContext . getSpanId ( ) ) ; put ( "trace_flags" , spanContext . getTraceFlags ( ) . isSampled ( ) ? "01" : "00" ) ; } try { filterChain . doFilter ( request , response ) ; } finally { remove ( "trace_id" ) ; remove ( "span_id" ) ; remove ( "trace_flags" ) ; } } }
Expand(32 more lines) kotlin/dev/pollito/spring_kotlin/config/log/TraceIdFilter.kt
package dev . pollito . spring_kotlin . config . log import io . opentelemetry . api . trace . Span . current import jakarta . servlet . FilterChain import jakarta . servlet . http . HttpServletRequest import jakarta . servlet . http . HttpServletResponse import org . slf4j . MDC . put import org . slf4j . MDC . remove import org . springframework . core . Ordered . LOWEST_PRECEDENCE import org . springframework . core . annotation . Order import org . springframework . stereotype . Component import org . springframework . web . filter . OncePerRequestFilter @Component @Order ( LOWEST_PRECEDENCE - 1 ) class TraceIdFilter : OncePerRequestFilter ( ) { override fun doFilterInternal ( request : HttpServletRequest , response : HttpServletResponse , filterChain : FilterChain , ) { val spanContext = current ( ) . spanContext if ( spanContext . isValid ) { put ( "trace_id" , spanContext . traceId ) put ( "span_id" , spanContext . spanId ) put ( "trace_flags" , if ( spanContext . traceFlags . isSampled ) "01" else "00" ) } try { filterChain . doFilter ( request , response ) } finally { remove ( "trace_id" ) remove ( "span_id" ) remove ( "trace_flags" ) } } }
Expand(25 more lines) groovy/dev/pollito/spring_groovy/config/log/TraceIdFilter.groovy
package dev . pollito . spring_groovy . config . log import static org . slf4j . MDC . put import static org . slf4j . MDC . remove import groovy . transform . CompileStatic import io . opentelemetry . api . trace . Span import io . opentelemetry . api . trace . SpanContext import jakarta . servlet . FilterChain import jakarta . servlet . ServletException import jakarta . servlet . http . HttpServletRequest import jakarta . servlet . http . HttpServletResponse import org . springframework . core . annotation . Order import org . springframework . stereotype . Component import org . springframework . web . filter . OncePerRequestFilter @Component @Order ( 2147483646 ) @CompileStatic class TraceIdFilter extends OncePerRequestFilter { @Override protected void doFilterInternal ( HttpServletRequest request , HttpServletResponse response , FilterChain filterChain ) throws ServletException , IOException { SpanContext spanContext = Span . current ( ) . spanContext if ( spanContext . valid ) { put ( "trace_id" , spanContext . traceId ) put ( "span_id" , spanContext . spanId ) put ( "trace_flags" , spanContext . traceFlags . sampled ? "01" : "00" ) } try { filterChain . doFilter ( request , response ) } finally { remove ( "trace_id" ) remove ( "span_id" ) remove ( "trace_flags" ) } } }
Expand(29 more lines)
Mask Sensitive Data in Logs With Logback
It's important to mask sensitive details when logging (i.e., passwords, SSN, etc.). Let's mask the logs centrally by configuring masking rules for all log entries produced by Logback.
Create MaskingPatternLayout:
java/dev/pollito/spring_java/config/log/MaskingPatternLayout.java
package dev . pollito . spring_java . config . log ; import static java . util . regex . Matcher . quoteReplacement ; import static java . util . regex . Pattern . CASE_INSENSITIVE ; import static java . util . regex . Pattern . MULTILINE ; import ch . qos . logback . classic . PatternLayout ; import ch . qos . logback . classic . spi . ILoggingEvent ; import java . util . ArrayList ; import java . util . List ; import java . util . Objects ; import java . util . regex . MatchResult ; import java . util . regex . Pattern ; import java . util . stream . IntStream ; import org . jspecify . annotations . NonNull ; public class MaskingPatternLayout extends PatternLayout { private Pattern multilinePattern ; private final List < String > maskPatterns = new ArrayList < > ( ) ; public void addMaskPattern ( String maskPattern ) { maskPatterns . add ( maskPattern ) ; multilinePattern = Pattern . compile ( String . join ( "|" , maskPatterns ) , MULTILINE | CASE_INSENSITIVE ) ; } @Override public String doLayout ( ILoggingEvent event ) { return maskMessage ( super . doLayout ( event ) ) ; } private String maskMessage ( String message ) { if ( multilinePattern == null ) { return message ; } return multilinePattern . matcher ( message ) . replaceAll ( this :: computeReplacement ) ; } private String computeReplacement ( @NonNull MatchResult matchResult ) { List < String > nonNullGroups = IntStream . rangeClosed ( 1 , matchResult . groupCount ( ) ) . mapToObj ( matchResult :: group ) . filter ( Objects :: nonNull ) . limit ( 2 ) . toList ( ) ; String replacement = switch ( nonNullGroups . size ( ) ) { case 0 -> matchResult . group ( 0 ) ; case 1 -> nonNullGroups . getFirst ( ) ; default -> nonNullGroups . getFirst ( ) + "****" ; } ; return quoteReplacement ( replacement ) ; } }
Expand(44 more lines) kotlin/dev/pollito/spring_kotlin/config/log/MaskingPatternLayout.kt
package dev . pollito . spring_kotlin . config . log import ch . qos . logback . classic . PatternLayout import ch . qos . logback . classic . spi . ILoggingEvent import kotlin . text . RegexOption . IGNORE_CASE import kotlin . text . RegexOption . MULTILINE class MaskingPatternLayout : PatternLayout ( ) { private var multilineRegex : Regex ? = null private val maskPatterns = mutableListOf < String > ( ) fun addMaskPattern ( maskPattern : String ) { maskPatterns . add ( maskPattern ) multilineRegex = maskPatterns . joinToString ( "|" ) . toRegex ( setOf ( MULTILINE , IGNORE_CASE ) ) } override fun doLayout ( event : ILoggingEvent ) : String = maskMessage ( super . doLayout ( event ) ) private fun maskMessage ( message : String ) : String { val regex = multilineRegex ?: return message return regex . replace ( message ) { match -> val nonNullGroups = ( 1 .. match . groupValues . lastIndex ) . mapNotNull { match . groups [ it ] ? . value } nonNullGroups . firstOrNull ( ) ? . let { prefix -> if ( nonNullGroups . size >= 2 ) " $ prefix ****" else prefix } ?: match . value } } }
Expand(17 more lines) groovy/dev/pollito/spring_groovy/config/log/MaskingPatternLayout.groovy
package dev . pollito . spring_groovy . config . log import static java . util . regex . Matcher . quoteReplacement import static java . util . regex . Pattern . CASE_INSENSITIVE import static java . util . regex . Pattern . MULTILINE import static java . util . regex . Pattern . compile import ch . qos . logback . classic . PatternLayout import ch . qos . logback . classic . spi . ILoggingEvent import groovy . transform . CompileStatic import java . util . regex . Matcher import java . util . regex . Pattern @CompileStatic class MaskingPatternLayout extends PatternLayout { private Pattern multilinePattern private final List < String > maskPatterns = [ ] void addMaskPattern ( String maskPattern ) { maskPatterns . add ( maskPattern ) multilinePattern = compile ( maskPatterns . join ( "|" ) , MULTILINE | CASE_INSENSITIVE ) } @Override String doLayout ( ILoggingEvent event ) { maskMessage ( super . doLayout ( event ) ) } private String maskMessage ( String message ) { if ( multilinePattern == null ) { return message } Matcher matcher = multilinePattern . matcher ( message ) StringBuilder sb = new StringBuilder ( ) while ( matcher . find ( ) ) { List < String > groups = ( 1 .. matcher . groupCount ( ) ) . collect { matcher . group ( it ) } . findAll { it != null } String replacement = groups . empty ? matcher . group ( 0 ) : groups [ 0 ] + ( groups . size ( ) > 1 ? "****" : "" ) matcher . appendReplacement ( sb , quoteReplacement ( replacement ) ) } matcher . appendTail ( sb ) sb . toString ( ) } }
Expand(41 more lines)
Add regex patterns in maskPattern tags inside logback.xml:
resources/logback-spring.xml
< configuration > < appender name = " CONSOLE " class = " ch.qos.logback.core.ConsoleAppender " > < encoder class = " ch.qos.logback.core.encoder.LayoutWrappingEncoder " > < layout class = " dev.pollito.spring_java.config.log.MaskingPatternLayout " > < maskPattern > (?i)((?:authorization|proxy-authorization|cookie|x-api-key|x-auth-token|x-csrf-token):\s+)([^\r\n,]+) </ maskPattern > < maskPattern > (?i)((?:password|token|secret)[\s:="]+)(\S+) </ maskPattern > < pattern > %d{yyyy-MM-dd} %d{HH:mm:ss.SSS} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %-5level %thread --- %logger{36} %msg%n </ pattern > </ layout > </ encoder > </ appender > < root level = " INFO " > < appender-ref ref = " CONSOLE " /> </ root > </ configuration >
Expand(3 more lines) resources/logback-spring.xml
< configuration > < appender name = " CONSOLE " class = " ch.qos.logback.core.ConsoleAppender " > < encoder class = " ch.qos.logback.core.encoder.LayoutWrappingEncoder " > < layout class = " dev.pollito.spring_kotlin.config.log.MaskingPatternLayout " > < maskPattern > (?i)((?:authorization|proxy-authorization|cookie|x-api-key|x-auth-token|x-csrf-token):\s+)([^\r\n,]+) </ maskPattern > < maskPattern > (?i)((?:password|token|secret)[\s:="]+)(\S+) </ maskPattern > < pattern > %d{yyyy-MM-dd} %d{HH:mm:ss.SSS} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %-5level %thread --- %logger{36} %msg%n </ pattern > </ layout > </ encoder > </ appender > < root level = " INFO " > < appender-ref ref = " CONSOLE " /> </ root > </ configuration >
Expand(3 more lines) resources/logback-spring.xml
< configuration > < appender name = " CONSOLE " class = " ch.qos.logback.core.ConsoleAppender " > < encoder class = " ch.qos.logback.core.encoder.LayoutWrappingEncoder " > < layout class = " dev.pollito.spring_groovy.config.log.MaskingPatternLayout " > < maskPattern > (?i)((?:authorization|proxy-authorization|cookie|x-api-key|x-auth-token|x-csrf-token):\s+)([^\r\n,]+) </ maskPattern > < maskPattern > (?i)((?:password|token|secret)[\s:="]+)(\S+) </ maskPattern > < pattern > %d{yyyy-MM-dd} %d{HH:mm:ss.SSS} trace_id=%X{trace_id} span_id=%X{span_id} trace_flags=%X{trace_flags} %-5level %thread --- %logger{36} %msg%n </ pattern > </ layout > </ encoder > </ appender > < root level = " INFO " > < appender-ref ref = " CONSOLE " /> </ root > </ configuration >
Expand(5 more lines)
Check How Logs Look Like
Run the following curl command (with placeholder values) to see everything working: sensitive data masking, the aspect intercepting controller method invocations, how the logs contain the trace, and the filter printing the HTTP request and response.
Terminal
curl -s --request GET --url http://localhost:8080/api/films/42 --header 'Accept: application/json' --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' --header 'Cookie: JSESSIONID=A1B2C3D4E5F6G7H8I9J0; auth_token=secret123token456' --header 'Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=' --header 'User-Agent: Mozilla/5.0 (Test Client)' --header 'X-API-Key: super-secret-api-key' --header 'X-Auth-Token: super-secret-auth-token-12345' --header 'X-CSRF-Token: csrf_abc123def456ghi789' | jq
Logs should look something like this:
Application logs
2026-02-18 15:28:11.600 trace_id=b8e1447340832e9b466fde0a1f172b55 span_id=a4fa1234784f7c02 trace_flags=01 INFO http-nio-8080-exec-1 --- d.p.spring_java.config.log.LogFilter >>>> Method: GET; URI: /api/films/42; QueryString: null; Headers: {Host: localhost:8080, Accept: application/json, Authorization: ****, Cookie: ****, Proxy-Authorization: ****, User-Agent: Mozilla/5.0 (Test Client), X-API-Key: ****, X-Auth-Token: ****, X-CSRF-Token: **** 2026-02-18 15:28:11.619 trace_id=b8e1447340832e9b466fde0a1f172b55 span_id=a4fa1234784f7c02 trace_flags=01 INFO http-nio-8080-exec-1 --- d.p.spring_java.config.log.LogAspect [FilmRestController.findById(..)] Args: [42] 2026-02-18 15:28:11.620 trace_id=b8e1447340832e9b466fde0a1f172b55 span_id=a4fa1234784f7c02 trace_flags=01 INFO http-nio-8080-exec-1 --- d.p.spring_java.config.log.LogAspect [FilmRestController.findById(..)] Response: FilmResponse(id=42, title=ACADEMY DINOSAUR, description=A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies, releaseYear=2006, rating=PG, lengthMinutes=86, language=English) 2026-02-18 15:28:11.664 trace_id=b8e1447340832e9b466fde0a1f172b55 span_id=a4fa1234784f7c02 trace_flags=01 INFO http-nio-8080-exec-1 --- d.p.spring_java.config.log.LogFilter <<<< Response Status: 200
+ - Reset Full Screen Scroll to zoom • Drag corner to resize