Logging con Log4j2 en Spring Boot
Aprende a configurar un sistema de logging profesional en tu API REST con Spring Boot usando SLF4J + Log4j2. Desde la configuración básica hasta la rotación de archivos y la exposición de logs vía endpoint REST para consumirlos desde Astro.
1 ¿Qué es Log4j2 y por qué usarlo en Spring Boot?
Log4j2 es un framework de logging para Java, la evolución de Log4j clásico. Es el motor que se encarga de dónde, cómo y cuándo se escriben los registros (consola, archivos, bases de datos, servicios remotos...).
🏗️ Analogía:
Piensa en el logging como la caja negra de un avión. Todo lo que pasa durante el vuelo queda registrado: despegue, altitud, velocidad, incidencias. Cuando algo falla, abres la caja negra y sabes exactamente qué pasó. Tu API REST necesita lo mismo.
¿Cómo encaja en Spring Boot?
Spring Boot usa una arquitectura en dos capas para el logging:
Capa 1: SLF4J (la fachada)
Es la API que usas en tu código Java:
logger.info("...").
Es una interfaz, no implementa nada. Tu código solo habla con SLF4J.
Capa 2: Log4j2 (el motor)
Es la implementación que realmente escribe los logs. Decide el formato, los destinos (consola/archivo), los filtros, la rotación. Spring Boot trae Logback por defecto, pero puedes cambiarlo a Log4j2.
💡 ¿Por qué Log4j2 en lugar de Logback?
Ambos son excelentes. Log4j2 destaca en rendimiento asíncrono (Disruptor-based), configuración en XML/YAML/JSON y soporte nativo de Lookups (variables de entorno, propiedades del sistema). Logback es el default de Spring y funciona bien para la mayoría. Enseñamos Log4j2 porque es el estándar en muchas empresas y aprenderlo te da conocimiento transferible.
2 Niveles de log
Log4j2 tiene 6 niveles de severidad, de menor a mayor importancia. El nivel que configures determina qué mensajes se escriben: todo lo que esté en ese nivel o por encima.
| Nivel | Prioridad | Cuándo usarlo | Ejemplo |
|---|---|---|---|
| TRACE | 1 (mínima) | Traza ultra detallada, solo para debugging profundo | Valor de cada variable en cada iteración |
| DEBUG | 2 | Información útil para desarrolladores | Parámetros de entrada de un método |
| INFO | 3 | Eventos normales de la aplicación | Usuario autenticado, pedido creado |
| WARN | 4 | Algo inesperado pero la app sigue funcionando | API externa lenta, caché fallido, recurso casi lleno |
| ERROR | 5 | Error grave, una operación falló | Excepción no controlada, BD no disponible |
| FATAL | 6 (máxima) | Error crítico, la aplicación no puede continuar | Fallo de arranque, recurso esencial no disponible |
1// Ejemplo de uso de cada nivel:2logger.trace("Entrando en el método calcular() con x={}, y={}", x, y);3logger.debug("Query SQL ejecutada: {}", sql);4logger.info("Pedido #{} creado para el usuario {}", pedidoId, userId);5logger.warn("La API de pagos tardó {}ms (umbral: 3000ms)", duracion);6logger.error("Error al guardar el pedido: {}", e.getMessage(), e);7logger.fatal("No se pudo conectar a la base de datos al arrancar");💡 Regla de oro:
En desarrollo configuras nivel DEBUG.
En producción configuras nivel INFO o WARN.
Así en producción no llenas los archivos de logs con trazas de debug.
3 Paso a paso: configurar Log4j2 en Spring Boot
Spring Boot viene con Logback por defecto. Para usar Log4j2 hay que hacer un swap en 3 pasos.
Excluir Logback y añadir Log4j2 en pom.xml
En tu pom.xml, excluye la dependencia de logging por defecto
y añade el starter de Log4j2:
1<dependencies>2 <!-- Spring Boot Web (ya lo tienes) -->3 <dependency>4 <groupId>org.springframework.boot</groupId>5 <artifactId>spring-boot-starter-web</artifactId>6 <!-- PASO 1: Excluir Logback -->7 <exclusions>8 <exclusion>9 <groupId>org.springframework.boot</groupId>10 <artifactId>spring-boot-starter-logging</artifactId>11 </exclusion>12 </exclusions>13 </dependency>14 15 <!-- PASO 2: Añadir Log4j2 -->16 <dependency>17 <groupId>org.springframework.boot</groupId>18 <artifactId>spring-boot-starter-log4j2</artifactId>19 </dependency>20</dependencies> ⚠️ Importante: si tienes más starters que incluyan
spring-boot-starter-logging (como
spring-boot-starter-data-jpa), también necesitas
excluirlo de cada uno. O haz la exclusión en un <dependencyManagement> global.
Crear el archivo de configuración log4j2-spring.xml
Crea el archivo src/main/resources/log4j2-spring.xml.
Este es el cerebro de tu logging: aquí defines los destinos (Appenders), los formatos (Layouts) y los filtros (Loggers).
1<?xml version="1.0" encoding="UTF-8"?>2<Configuration status="WARN" monitorInterval="30">3 4 <!-- Variables reutilizables -->5 <Properties>6 <Property name="LOG_PATTERN">7 %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n8 </Property>9 <Property name="LOG_DIR">logs</Property>10 </Properties>11 12 <!-- APPENDERS: ¿Dónde van los logs? -->13 <Appenders>14 <!-- 1. Consola (desarrollo) -->15 <Console name="Console" target="SYSTEM_OUT">16 <PatternLayout pattern="${LOG_PATTERN}" />17 </Console>18 19 <!-- 2. Archivo con rotación (producción) -->20 <RollingFile name="File"21 fileName="${LOG_DIR}/app.log"22 filePattern="${LOG_DIR}/app-%d{yyyy-MM-dd}-%i.log.gz">23 <PatternLayout pattern="${LOG_PATTERN}" />24 <Policies>25 <TimeBasedTriggeringPolicy interval="1" />26 <SizeBasedTriggeringPolicy size="10MB" />27 </Policies>28 <DefaultRolloverStrategy max="30" />29 </RollingFile>30 </Appenders>31 32 <!-- LOGGERS: ¿Qué nivel y hacia dónde? -->33 <Loggers>34 <!-- Logger para TU paquete (nivel DEBUG en dev) -->35 <Logger name="com.tuempresa.tuapp" level="DEBUG" additivity="false">36 <AppenderRef ref="Console" />37 <AppenderRef ref="File" />38 </Logger>39 40 <!-- Logger raíz: todo lo demás -->41 <Root level="INFO">42 <AppenderRef ref="Console" />43 </Root>44 </Loggers>45 46</Configuration>Usar el Logger en tu código Java
En cualquier clase Java, importas SLF4J y creas un logger. Tu código nunca habla directamente con Log4j2: siempre usa la fachada SLF4J, así si mañana cambias de motor, no tocas ni una línea.
1import org.slf4j.Logger;2import org.slf4j.LoggerFactory;3 4public class PedidoService {5 6 // Un Logger por clase — SIEMPRE estático y final7 private static final Logger logger = LoggerFactory.getLogger(PedidoService.class);8 9 public Pedido crear(PedidoDTO dto) {10 logger.info("Creando pedido para el usuario: {}", dto.getUserId());11 12 try {13 Pedido pedido = pedidoRepository.save(mapear(dto));14 logger.info("Pedido #{} creado correctamente", pedido.getId());15 return pedido;16 } catch (Exception e) {17 logger.error("Error al crear pedido para usuario {}: {}",18 dto.getUserId(), e.getMessage(), e);19 throw e;20 }21 }22} 💡 Los {}: SLF4J usa placeholders en lugar de concatenación.
logger.info("Hola ", nombre) es más eficiente que
logger.info("Hola " + nombre) porque si el nivel está desactivado,
no se ejecuta la concatenación.
4 Anatomía de log4j2-spring.xml
El archivo de configuración tiene 3 bloques principales. Piénsalo como una fábrica:
📦 Properties
Variables reutilizables: rutas, patrones de formato. Evitas repetir strings.
📤 Appenders
Los destinos: dónde van los logs. Consola, archivo, BD, servicio remoto.
🎯 Loggers
Los filtros: qué paquetes loguear, a qué nivel, hacia qué appender.
Appenders más usados:
| Appender | Para qué | Cuándo usarlo |
|---|---|---|
| Console | Escribe en la terminal (stdout/stderr) | Siempre en desarrollo, opcional en producción |
| File | Escribe en un archivo fijo | Cuando no necesitas rotación |
| RollingFile | Escribe en archivo con rotación automática | Producción — el más importante |
| Async | Wrapper que hace cualquier appender asíncrono | Alto volumen, rendimiento crítico |
Tokens del PatternLayout:
El PatternLayout formatea cada línea de log. Estos son los tokens más usados:
| Token | Qué muestra | Ejemplo de salida |
|---|---|---|
| %d{patrón} | Fecha y hora | 2026-04-20 13:45:30.123 |
| %t | Nombre del thread | http-nio-8080-exec-1 |
| %-5level | Nivel (con padding de 5 caracteres) | INFO / ERROR |
| %logger{36} | Nombre del logger (max 36 chars) | c.t.tuapp.PedidoService |
| %msg | El mensaje | Pedido #42 creado correctamente |
| %n | Salto de línea del sistema | \n o \r\n |
| %ex | Stack trace de la excepción | java.lang.NullPointerException... |
Ejemplo de salida con el patrón %d [%t] %-5level %logger{36} - %msg%n:
12026-04-20 13:45:30.123 [http-nio-8080-exec-1] INFO c.t.tuapp.PedidoService - Pedido #42 creado correctamente5 Perfiles: desarrollo vs producción
Spring Boot soporta perfiles (application-dev.properties,
application-prod.properties).
Puedes usar distintos archivos de configuración de Log4j2 según el entorno.
Opción 1: Un archivo, condicionales por perfil
En application.properties de cada perfil, apuntas a un config distinto:
1logging.config=classpath:log4j2-dev.xml1logging.config=classpath:log4j2-prod.xmlOpción 2: SpringProfile en el mismo XML
Log4j2 con Spring Boot soporta <SpringProfile> directamente en el XML:
1<Configuration>2 <Appenders>3 <Console name="Console" target="SYSTEM_OUT">4 <PatternLayout pattern="%d %-5level %logger{36} - %msg%n" />5 </Console>6 <RollingFile name="File"7 fileName="logs/app.log"8 filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">9 <PatternLayout pattern="%d [%t] %-5level %logger{36} - %msg%n" />10 <Policies>11 <TimeBasedTriggeringPolicy interval="1" />12 </Policies>13 </RollingFile>14 </Appenders>15 16 <Loggers>17 <!-- Perfil DEV: mucha info, solo consola -->18 <SpringProfile name="dev">19 <Logger name="com.tuempresa" level="DEBUG" additivity="false">20 <AppenderRef ref="Console" />21 </Logger>22 </SpringProfile>23 24 <!-- Perfil PROD: menos info, consola + archivo -->25 <SpringProfile name="prod">26 <Logger name="com.tuempresa" level="INFO" additivity="false">27 <AppenderRef ref="Console" />28 <AppenderRef ref="File" />29 </Logger>30 </SpringProfile>31 32 <Root level="WARN">33 <AppenderRef ref="Console" />34 </Root>35 </Loggers>36</Configuration>6 Ejemplo práctico: logging en un REST Controller
Un controller de Spring Boot con logging completo: request de entrada, resultado exitoso, y error con stack trace.
1import org.slf4j.Logger;2import org.slf4j.LoggerFactory;3import org.springframework.http.ResponseEntity;4import org.springframework.web.bind.annotation.*;5 6@RestController7@RequestMapping("/api/productos")8public class ProductoController {9 10 private static final Logger logger = LoggerFactory.getLogger(ProductoController.class);11 12 private final ProductoService productoService;13 14 public ProductoController(ProductoService productoService) {15 this.productoService = productoService;16 }17 18 @GetMapping19 public ResponseEntity<?> listar() {20 logger.info("GET /api/productos — listando todos los productos");21 var productos = productoService.findAll();22 logger.debug("Se encontraron {} productos", productos.size());23 return ResponseEntity.ok(productos);24 }25 26 @GetMapping("/{id}")27 public ResponseEntity<?> obtener(@PathVariable Long id) {28 logger.info("GET /api/productos/{}", id);29 30 return productoService.findById(id)31 .map(p -> {32 logger.debug("Producto encontrado: {}", p.getNombre());33 return ResponseEntity.ok(p);34 })35 .orElseGet(() -> {36 logger.warn("Producto con id={} no encontrado", id);37 return ResponseEntity.notFound().build();38 });39 }40 41 @PostMapping42 public ResponseEntity<?> crear(@RequestBody ProductoDTO dto) {43 logger.info("POST /api/productos — creando: {}", dto.getNombre());44 45 try {46 var producto = productoService.crear(dto);47 logger.info("Producto #{} creado correctamente", producto.getId());48 return ResponseEntity.status(201).body(producto);49 } catch (Exception e) {50 logger.error("Error al crear producto '{}': {}",51 dto.getNombre(), e.getMessage(), e);52 return ResponseEntity.internalServerError()53 .body(Map.of("error", "No se pudo crear el producto"));54 }55 }56 57 @DeleteMapping("/{id}")58 public ResponseEntity<?> eliminar(@PathVariable Long id) {59 logger.info("DELETE /api/productos/{}", id);60 61 try {62 productoService.eliminar(id);63 logger.info("Producto #{} eliminado", id);64 return ResponseEntity.noContent().build();65 } catch (Exception e) {66 logger.error("Error al eliminar producto #{}: {}", id, e.getMessage(), e);67 return ResponseEntity.internalServerError().build();68 }69 }70}💡 Fíjate en el patrón:
infoal entrar al endpoint (qué se pidió)debugcon detalles internos (resultado de queries)warncuando algo no se encuentra (404) — no es un error, pero es notableerrorcon la excepción completa (ecomo último argumento imprime el stack trace)
7 Logging a archivos con rotación
En producción, los logs van a archivos. Sin rotación, un archivo de log crece indefinidamente
hasta llenar el disco. El RollingFile resuelve esto:
1<RollingFile name="AppLog"2 fileName="logs/app.log"3 filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">4 5 <PatternLayout pattern="%d [%t] %-5level %logger{36} - %msg%n" />6 7 <!-- ¿Cuándo rotar? -->8 <Policies>9 <!-- Cada día a medianoche -->10 <TimeBasedTriggeringPolicy interval="1" />11 <!-- O cuando el archivo llegue a 10MB -->12 <SizeBasedTriggeringPolicy size="10MB" />13 </Policies>14 15 <!-- Máximo 30 archivos antiguos (los más viejos se borran) -->16 <DefaultRolloverStrategy max="30" />17 18</RollingFile>Separar logs por tipo:
Es común tener un archivo para todo y otro solo para errores:
1<!-- Archivo solo de errores -->2<RollingFile name="ErrorLog"3 fileName="logs/error.log"4 filePattern="logs/error-%d{yyyy-MM-dd}.log.gz">5 <PatternLayout pattern="%d [%t] %-5level %logger{36} - %msg%n%ex" />6 <LevelRangeFilter minLevel="ERROR" maxLevel="FATAL" onMatch="ACCEPT" onMismatch="DENY" />7 <Policies>8 <TimeBasedTriggeringPolicy interval="1" />9 </Policies>10</RollingFile>
El LevelRangeFilter solo deja pasar logs de nivel ERROR o FATAL.
Así el archivo error.log contiene exclusivamente errores — fácil de monitorizar.
8 Endpoint REST para exponer logs al cliente
Si quieres que tu frontend (Astro) pueda leer los logs del backend, necesitas un endpoint que devuelva logs en JSON. Este es un ejemplo básico que lee las últimas N líneas del archivo de log:
1import org.slf4j.Logger;2import org.slf4j.LoggerFactory;3import org.springframework.http.ResponseEntity;4import org.springframework.web.bind.annotation.*;5import java.io.IOException;6import java.nio.file.*;7import java.util.*;8import java.util.stream.*;9 10@RestController11@RequestMapping("/api/logs")12public class LogController {13 14 private static final Logger logger = LoggerFactory.getLogger(LogController.class);15 private static final Path LOG_FILE = Paths.get("logs/app.log");16 17 @GetMapping18 public ResponseEntity<?> obtenerLogs(19 @RequestParam(defaultValue = "50") int lineas,20 @RequestParam(defaultValue = "") String nivel) {21 22 logger.debug("GET /api/logs?lineas={}&nivel={}", lineas, nivel);23 24 try {25 List<String> todas = Files.readAllLines(LOG_FILE);26 27 // Tomar las últimas N líneas28 List<String> recientes = todas.stream()29 .skip(Math.max(0, todas.size() - lineas))30 .collect(Collectors.toList());31 32 // Filtrar por nivel si se especifica (INFO, ERROR, etc.)33 if (!nivel.isEmpty()) {34 recientes = recientes.stream()35 .filter(l -> l.contains(nivel.toUpperCase()))36 .collect(Collectors.toList());37 }38 39 // Convertir a objetos JSON40 List<Map<String, String>> resultado = recientes.stream()41 .map(this::parsearLinea)42 .filter(Objects::nonNull)43 .collect(Collectors.toList());44 45 return ResponseEntity.ok(resultado);46 47 } catch (IOException e) {48 logger.error("Error al leer archivo de logs: {}", e.getMessage());49 return ResponseEntity.internalServerError()50 .body(Map.of("error", "No se pudieron leer los logs"));51 }52 }53 54 private Map<String, String> parsearLinea(String linea) {55 // Parsea: "2026-04-20 13:45:30.123 [thread] INFO logger - mensaje"56 try {57 Map<String, String> entry = new LinkedHashMap<>();58 entry.put("timestamp", linea.substring(0, 23));59 entry.put("level", linea.split("\\s+")[3]);60 entry.put("message", linea.substring(linea.indexOf(" - ") + 3));61 return entry;62 } catch (Exception e) {63 return null; // línea mal formada, la ignoramos64 }65 }66}Consumir desde Astro (fetch):
1// En una página .astro o un componente2const response = await fetch('http://localhost:8080/api/logs?lineas=20&nivel=ERROR');3const logs = await response.json();4 5// logs = [6// { timestamp: "2026-04-20 13:45:30.123", level: "ERROR", message: "..." },7// ...8// ]⚠️ Seguridad:
Este endpoint es solo para desarrollo o paneles internos. En producción, protégelo con autenticación
(@PreAuthorize("hasRole('ADMIN')")) o muévelo detrás de un API Gateway.
Nunca expongas logs al público.
9 Buenas prácticas
Un Logger por clase, estático y final
private static final Logger logger = LoggerFactory.getLogger(MiClase.class). No lo heredes, no lo compartas.
Usa placeholders, no concatenación
logger.info("User ", id) en lugar de "User " + id. Si el nivel está desactivado, no se evalúa.
Pasa la excepción como último argumento
logger.error("Fallo: ", msg, e). SLF4J detecta que el último arg es un Throwable e imprime el stack trace automáticamente.
Configura rotación en producción
Usa RollingFile con rotación diaria o por tamaño. Sin rotación, el disco se llena.
Nunca loguees datos sensibles
Contraseñas, tokens, números de tarjeta, datos personales (GDPR). Si lo logueas, queda en un archivo que alguien puede leer.
No uses System.out.println
System.out no tiene niveles, no tiene formato, no se guarda en archivo, no se filtra. Usa siempre el Logger.
10 Resumen
- •SLF4J es la fachada (API), Log4j2 es el motor (implementación). Tu código solo habla con SLF4J.
- •6 niveles: TRACE → DEBUG → INFO → WARN → ERROR → FATAL. En producción usa INFO o superior.
- •Swap en Spring Boot: excluir
spring-boot-starter-logging, añadirspring-boot-starter-log4j2. - •
log4j2-spring.xml: Properties + Appenders + Loggers. - •RollingFile con rotación diaria/tamaño para producción. Separa archivos por nivel (app.log, error.log).
- •Endpoint
/api/logspara exponer logs en JSON al frontend (protégelo con auth). - •Nunca:
System.out.println, concatenar strings, loguear datos sensibles.