Saltar al contenido principal

Logs

Código Completo
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 = Simple Logging Facade for Java. 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
└── ...

Dependencias

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

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);
}
}

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());
}
}

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");
}
}
}

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.

  1. 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);
    }
    }
  2. 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>

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
Scroll to zoom • Drag corner to resize