Volver al inicio
Anexo: Logging con Log4j2

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.

1

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.

2

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%n
8 </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>
3

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 final
7 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
ConsoleEscribe en la terminal (stdout/stderr)Siempre en desarrollo, opcional en producción
FileEscribe en un archivo fijoCuando no necesitas rotación
RollingFileEscribe en archivo con rotación automáticaProducción — el más importante
AsyncWrapper que hace cualquier appender asíncronoAlto 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 hora2026-04-20 13:45:30.123
%tNombre del threadhttp-nio-8080-exec-1
%-5levelNivel (con padding de 5 caracteres)INFO   / ERROR
%logger{36}Nombre del logger (max 36 chars)c.t.tuapp.PedidoService
%msgEl mensajePedido #42 creado correctamente
%nSalto de línea del sistema\n o \r\n
%exStack trace de la excepciónjava.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 correctamente

5 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:

application-dev.propertiesproperties
1logging.config=classpath:log4j2-dev.xml
application-prod.propertiesproperties
1logging.config=classpath:log4j2-prod.xml

Opció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@RestController
7@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 @GetMapping
19 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 @PostMapping
42 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:

  • info al entrar al endpoint (qué se pidió)
  • debug con detalles internos (resultado de queries)
  • warn cuando algo no se encuentra (404) — no es un error, pero es notable
  • error con la excepción completa (e como ú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@RestController
11@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 @GetMapping
18 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íneas
28 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 JSON
40 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 ignoramos
64 }
65 }
66}

Consumir desde Astro (fetch):

1// En una página .astro o un componente
2const 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ñadir spring-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/logs para exponer logs en JSON al frontend (protégelo con auth).
  • Nunca: System.out.println, concatenar strings, loguear datos sensibles.