El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub
springboot-demo-projects , bajo el tag
logs.
Cómo hace Spring Boot los Logs
En Spring Boot, SLF4J y Logback trabajan juntos para proveer una configuración de logging.
SLF4J = S imple L ogging F acade for J ava. Pensalo como una API de logging (una capa de abstracción). Tu código de aplicación habla con SLF4J, no directamente con una implementación específica de logging. Esto es brillante porque desacopla tu código de cómo los logs se escriben.
Logback = La implementación de logging real (el motor que hace el trabajo pesado). Es el backend de logging por defecto que SLF4J usa en una aplicación Spring Boot.
Aunque Spring Boot incluye SLF4J y Logback automáticamente y no se requiere configuración para tener logging básico funcionando, mejorar la experiencia de logging es altamente recomendado. Logging estructurado con trace IDs, enmascaramiento centralizado de datos sensibles, y logging consistente de request/response hacen que debuggear problemas de producción sea significativamente más fácil.
Archivos a Crear/Modificar
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)
Dependencias
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 en proyectos Kotlin
kotlin-logging es un wrapper
alrededor de SLF4J que provee una forma más idiomática de Kotlin para loguear
mensajes. No reemplaza SLF4J o Logback; en cambio, simplifica las llamadas
de logging en Kotlin usando funciones de extensión y evaluación lazy para
mensajes. La jerarquía de logging permanece: Código Kotlin → kotlin-logging
→ SLF4J → Logback (default de Spring Boot)
Evitar Publicación Local de Logs OTLP
Cuando agregás spring-boot-starter-opentelemetry a tus dependencias, Spring Boot configura automáticamente exportadores de OpenTelemetry para publicar métricas y traces vía OTLP (OpenTelemetry Protocol).
El problema es que en desarrollo local, típicamente no tenés un colector OTLP corriendo. Spring Boot va a intentar repetidamente conectarse al endpoint OTLP default y fallar, inundando tu consola con mensajes de error de conexión que ahogan tus logs reales de aplicación.
Para evitar este ruido durante desarrollo local, deshabilitá la exportación de métricas OTLP en tu perfil de dev:
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
De ahora en adelante, ejecutá la tarea Gradle bootRun con la variable de entorno SPRING_PROFILES_ACTIVE=dev para activar el perfil de dev:
SPRING_PROFILES_ACTIVE = dev ./gradlew bootRun
Log Aspect
Usar un Aspect para logging es un ejemplo clásico de AOP en acción. En lugar de esparcir declaraciones de log por toda tu lógica de negocio (que ensucia el código y mezcla concerns), definís el comportamiento de logging una vez en un módulo separado. Este aspecto luego intercepta automáticamente llamadas a métodos que te importan y aplica la lógica de logging sin que el código objetivo se dé cuenta.
Para un análisis más profundo, chequeá la sección de AOP en 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
A veces, un request puede ni siquiera llegar a tus controladores Spring MVC. Al loguear a nivel de Servlet, te da visibilidad sobre cada request entrante y respuesta saliente, independientemente de si golpea los endpoints específicos de tu aplicación.
Esto puede ser increíblemente útil para debuggear issues como fallos de autenticación, problemas de ruteo, o requests que son bloqueados más arriba en la cadena de filtros.
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
¿Alguna vez intentaste debuggear un issue en un sistema ocupado rebuscándote en un archivo de log masivo? Es como tratar de encontrar una aguja específica en un pajar de agujas.
Acá es donde entra el tracing. Al asignar un ID único (un 'Trace ID') a cada request entrante, podés etiquetar cada entrada de log generada durante el ciclo de vida de ese request. De repente, podés filtrar todo el archivo de log para ver el viaje de solo un request a través de múltiples métodos, servicios, o 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)
Enmascarar Datos Sensibles en Logs con Logback
Es importante enmascarar datos sensibles cuando logueamos (por ejemplo, contraseñas, SSN, etc.). Enmascará los logs de forma central configurando reglas de enmascaramiento para todas las entradas de log producidas por Logback.
Creá 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)
Agregá patrones regex en tags maskPattern dentro de 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)
Ver Cómo Se Ven los Logs
Corré el siguiente comando curl (con valores placeholder) para ver todo funcionando: enmascaramiento de datos sensibles, el aspect interceptando invocaciones de métodos del controlador, cómo los logs contienen el trace, y el filtro imprimiendo el HTTP request y 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
Los logs deberían verse algo así:
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