CodeGym /Cursos /JAVA 25 SELF /Registro en aplicaciones multihilo y web

Registro en aplicaciones multihilo y web

JAVA 25 SELF
Nivel 63 , Lección 2
Disponible

1. Seguridad de hilos en el logging

En programas monohilo todo es sencillo: un solo hilo escribe los logs y nadie le estorba. Pero en aplicaciones reales —servicios web, microservicios— se ejecutan a la vez decenas o cientos de hilos. Imagina que varias personas escribieran al mismo tiempo con bolígrafo en la misma línea de un cuaderno: el resultado sería, por decirlo suavemente, ilegible.

Seguridad de hilos (thread safety) — es la garantía de que, incluso si 100500 hilos escriben logs a la vez, los mensajes no se mezclarán, no se solaparán y no se perderán.

¿Cómo lo implementan las bibliotecas?

Las bibliotecas modernas de logging (Log4j 2, Logback, java.util.logging) están diseñadas desde el principio para ser seguras en entornos multihilo. Esto significa:

  • Cada hilo puede invocar con seguridad los métodos del logger.
  • Dentro de la biblioteca se emplean sincronización y colas para que los mensajes no interfieran entre sí.
  • Incluso si varios hilos escriben simultáneamente en el mismo archivo, los logs no se mezclarán.

IMPORTANTE: El propio logger (por ejemplo, el objeto Logger de SLF4J o Log4j) se puede usar como un campo static final en cualquier clase — no provocará problemas de concurrencia.

Ejemplo

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MultiThreadedLoggerExample {
    private static final Logger logger = LoggerFactory.getLogger(MultiThreadedLoggerExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                logger.info("El hilo {} escribe el mensaje {}", Thread.currentThread().getName(), i);
            }
        };

        Thread t1 = new Thread(task, "Primero");
        Thread t2 = new Thread(task, "Segundo");
        t1.start();
        t2.start();
    }
}

En los logs verás mensajes ordenados de ambos hilos — sin caos ni solapamientos.

2. Contexto de logging: MDC (Mapped Diagnostic Context)

Imagina que tu aplicación procesa cientos de peticiones de forma simultánea, cada una en su propio hilo. En los logs aparecen mensajes, pero no está claro a qué petición pertenece cada uno. Queremos ver no solo «qué pasó», sino con quién y en qué petición ocurrió.

MDC (Mapped Diagnostic Context) es un mecanismo especial que permite «adjuntar» información adicional a los logs asociada con el hilo actual. Todos los mensajes que escribe el hilo obtienen automáticamente esos datos adicionales.

Ejemplo: registramos el identificador de la petición

En una aplicación web se puede asignar a cada petición un ID único (por ejemplo, un UUID). Con MDC, ese ID se añadirá automáticamente a todos los logs que escriba el hilo que atiende la petición.

Cómo se ve en código (SLF4J + Logback):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;

public class MdcExample {
    private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            // Generamos un identificador único de la petición
            String requestId = UUID.randomUUID().toString();
            MDC.put("requestId", requestId); // lo añadimos a MDC

            logger.info("Procesamos la petición");
            doSomeWork();
            logger.info("Hemos finalizado el procesamiento");

            MDC.clear(); // ¡imprescindible limpiar al terminar!
        };

        Thread t1 = new Thread(task, "Hilo-1");
        Thread t2 = new Thread(task, "Hilo-2");
        t1.start();
        t2.start();
    }

    static void doSomeWork() {
        logger.debug("Ejecutando trabajo...");
    }
}

Configuración del formato del log (por ejemplo, logback.xml):

<encoder>
    <pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n</pattern>
</encoder>

Resultado:

12:01:23 [Hilo-1] INFO  MdcExample [requestId=ad8d...f3] - Procesamos la petición
12:01:23 [Hilo-1] DEBUG MdcExample [requestId=ad8d...f3] - Ejecutando trabajo...
12:01:23 [Hilo-1] INFO  MdcExample [requestId=ad8d...f3] - Hemos finalizado el procesamiento

¡Importante!

  • MDC funciona únicamente dentro de un mismo hilo. Si transfieres trabajo a otro hilo (por ejemplo, mediante un pool de hilos), debes pasar manualmente los valores de MDC (o usar bibliotecas especiales que lo hagan automáticamente).
  • ¡No olvides limpiar MDC! Si no lo limpias, los datos pueden «derramarse» a la siguiente petición en el mismo hilo (por ejemplo, en un pool de hilos del servidor web). Usa MDC.clear() en un bloque finally.

3. Logging en aplicaciones web

Una aplicación web no es solo un programa que se ejecuta y ya. Es una auténtica línea de montaje: llegan peticiones, se procesan y se envían respuestas. Y todo ello a la vez, por cientos. Aquí el logging no es un lujo, ¡es una necesidad!

¿Qué registrar en aplicaciones web?

  • Peticiones y respuestas HTTP: método, URL, parámetros, estado devuelto, tiempo de procesamiento.
  • Errores y excepciones: todos los fallos inesperados, stack trace.
  • Eventos de negocio: registro, inicio de sesión, creación de pedido, pago, etc.
  • Detalles técnicos: interacción con la base de datos, servicios externos, tiempo de ejecución de operaciones.

Regla principal: registra de forma que, dentro de un mes, cuando algo falle a las 3 de la mañana, puedas entender qué salió mal.

Ejemplo: registro de una petición HTTP (Spring Boot)

La forma más sencilla es usar un filtro o un aspecto que registre cada petición entrante.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Component
public class RequestLoggingFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        logger.info("Petición: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());

        long start = System.currentTimeMillis();
        try {
            chain.doFilter(request, response); // sigue la cadena (hasta el controlador)
        } finally {
            long duration = System.currentTimeMillis() - start;
            logger.info("Respuesta enviada, tiempo de procesamiento: {} ms", duration);
            MDC.clear();
        }
    }
}

Registro de errores y excepciones

En los frameworks web (por ejemplo, Spring) es habitual usar manejadores de errores especiales (@ExceptionHandler) para registrar de forma elegante todos los fallos inesperados.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex) {
        logger.error("Se produjo un error: ", ex); // registramos con el stack trace completo
        return "error"; // devolvemos la página de error
    }
}

Integración con frameworks web

Casi todos los frameworks web modernos (Spring, Jakarta EE, Micronaut, etc.) se integran con loggers «de serie». Normalmente basta con añadir la dependencia SLF4J/Logback al proyecto — y todos los mensajes estándar (arranque de la aplicación, procesamiento de peticiones, errores) se registrarán automáticamente.

4. Práctica: ejemplo de logging en una tarea multihilo

Añadamos a nuestra aplicación didáctica (por ejemplo, un servicio de procesamiento de pedidos) procesamiento multihilo y veamos cómo el logging ayuda a mantener la cordura.

Ejemplo: procesamiento de pedidos en varios hilos con MDC

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class OrderProcessingApp {
    private static final Logger logger = LoggerFactory.getLogger(OrderProcessingApp.class);

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int orderId = i;
            executor.submit(() -> {
                String requestId = UUID.randomUUID().toString();
                MDC.put("requestId", requestId);

                try {
                    logger.info("Comenzamos a procesar el pedido {}", orderId);
                    processOrder(orderId);
                    logger.info("El pedido {} se ha procesado correctamente", orderId);
                } catch (Exception ex) {
                    logger.error("Error al procesar el pedido " + orderId, ex);
                } finally {
                    MDC.clear();
                }
            });
        }
        executor.shutdown();
    }

    static void processOrder(int orderId) throws InterruptedException {
        if (orderId % 2 == 0) {
            throw new RuntimeException("Simulación de error para pedido par");
        }
        Thread.sleep(500); // simulación de trabajo
    }
}

Qué ocurre:

  • Cada pedido se procesa en un hilo aparte.
  • Para cada hilo se crea un requestId único (a través de MDC).
  • Todos los logs de un pedido se pueden localizar por ese identificador.
  • Los errores se registran con el stack completo.

El formato del log está configurado para mostrar el requestId.

5. Matices y particularidades importantes

  • Hilos, pools y MDC. Si trabajas con pools de hilos (y seguramente lo haces), recuerda: ¡los hilos en el pool se reutilizan! Si olvidas limpiar MDC, los datos de una petición pueden acabar en los logs de otra. Llama siempre a MDC.clear() al final del trabajo.
  • MDC y tareas asíncronas. En frameworks web asíncronos (por ejemplo, Spring WebFlux) MDC no siempre funciona «de serie», porque el procesamiento de la petición puede saltar entre hilos. Para estos casos existen extensiones o adaptadores específicos.
  • Logging en microservicios. En una arquitectura de microservicios es habitual registrar no solo el identificador local de la petición, sino también uno global (traceId) que se propaga entre servicios. Esto permite seguir el recorrido de la petición por todo el sistema (trazabilidad distribuida). Para ello a menudo se usan sistemas como Zipkin, Jaeger, OpenTelemetry.

6. Demostración: diferencia entre System.out.println y el logging

System.out.println simplemente imprime una cadena en la consola. En un entorno multihilo:

  • Los mensajes pueden mezclarse.
  • No hay información sobre hora, hilo, nivel, contexto.
  • No se puede configurar la salida a archivo, el formato o la filtración por nivel.

El logger escribe mensajes estructurados, tiene en cuenta hilos, niveles y formato, y permite enviar la salida a distintos destinos (archivo, consola, red).

Ejemplo de comparación

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintVsLogger {
    private static final Logger logger = LoggerFactory.getLogger(PrintVsLogger.class);

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("System.out: " + Thread.currentThread().getName() + " paso " + i);
                logger.info("Logger: paso {}", i);
            }
        };
        new Thread(task, "T1").start();
        new Thread(task, "T2").start();
    }
}

Conclusión:

  • System.out — los mensajes pueden llegar mezclados, sin hora ni nivel.
  • El logger — cada mensaje contiene hora, hilo y nivel; se puede filtrar y encontrar rápido lo necesario.

7. Errores típicos al registrar en entornos multihilo y aplicaciones web

Error n.º 1: usar System.out.println en lugar de un logger. En un entorno multihilo esto provoca «caos» en la consola, imposibilidad de filtrar mensajes y pérdida de información de contexto.

Error n.º 2: ignorar MDC o usarlo incorrectamente. Si no usas MDC para propagar el identificador de petición/usuario, los logs pierden sentido — es imposible entender a qué petición pertenece un error. Si olvidas limpiar MDC, los datos pueden «fugarse» a otra petición.

Error n.º 3: crear el logger como variable local. Es mejor usar private static final Logger — así el logger se crea una sola vez por clase, no malgastas memoria y reduces el riesgo de errores.

Error n.º 4: registrar datos sensibles. No deben llegar a los logs contraseñas, tarjetas bancarias, datos personales — ¡es un problema de seguridad!

Error n.º 5: registrar solo "INFO" o solo "ERROR". Usa los niveles adecuados: DEBUG para depuración, INFO para eventos de negocio, ERROR para errores. No escribas todo con un único nivel — de lo contrario los logs pierden su sentido.

Error n.º 6: no registrar el stack trace de las excepciones. Si solo escribes logger.error("Error: " + ex.getMessage()), pierdes información sobre la causa. Registra siempre la excepción completa: logger.error("Error", ex).

Error n.º 7: loggers caseros no thread-safe. Si alguien decide «hacer su propio logger» sin sincronización, en un entorno multihilo casi seguro habrá pérdida o corrupción de logs.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION