Personaliza WebClient en Java para Logging de Solicitudes

Introducción

En este tutorial, vamos a mostrar cómo personalizar el WebClient de Spring, un cliente HTTP reactivo, para registrar tanto las solicitudes como las respuestas. La capacidad de registrar detalles de las solicitudes y respuestas es crucial en el desarrollo de aplicaciones, ya que permite depurar errores y entender mejor el flujo de datos. El WebClient es una herramienta poderosa en el ecosistema de Spring y su personalización puede enriquecer nuestras aplicaciones, así como facilitar la identificación de problemas de red.

1. Overview

En este tutorial, vamos a mostrar cómo personalizar Spring’s WebClient – un cliente HTTP reactivo – para registrar solicitudes y respuestas. Seremos claros y concisos, y le proporcionaremos ejemplos de código para ilustrar cada concepto.

2. WebClient

WebClient es una interfaz reactiva y no bloqueante para realizar solicitudes HTTP, basada en Spring WebFlux. Tiene una API funcional, fluida y utiliza tipos reactivos para la composición declarativa.

Detrás de escena, WebClient llama a un cliente HTTP. Reactor Netty es el cliente HTTP reactivo por defecto, también se admite el cliente HTTP de Jetty. Además, es posible conectar implementaciones adicionales del cliente HTTP configurando un ClientConnector para WebClient.

3. Logging Requests and Responses

El cliente HTTP por defecto utilizado por WebClient es la implementación de Netty, por lo que, después de cambiar el nivel de registro de reactor.netty.http.client a DEBUG, podemos ver algo de registro de solicitudes, pero si necesitamos un registro más personalizado, podemos configurar nuestros registradores a través de WebClient#filters:

WebClient
  .builder()
  .filters(exchangeFilterFunctions -> {
      exchangeFilterFunctions.add(logRequest());
      exchangeFilterFunctions.add(logResponse());
  })
  .build();

En este fragmento de código, hemos añadido dos filtros separados para registrar la solicitud y la respuesta.

### Implementando logRequest

Implementemos logRequest utilizando ExchangeFilterFunction#ofRequestProcessor:

ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
        if (log.isDebugEnabled()) {
            StringBuilder sb = new StringBuilder("Request: \n");
            clientRequest.headers().forEach((name, values) -> values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")));
            log.debug(sb.toString());
        }
        return Mono.just(clientRequest);
    });
}

### Implementando logResponse

El proceso para logResponse es similar, pero utilizaremos ExchangeFilterFunction#ofResponseProcessor en su lugar.

ExchangeFilterFunction logResponse() {
    return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
        if (log.isDebugEnabled()) {
            StringBuilder sb = new StringBuilder("Response: \n");
            sb.append("Status: ").append(clientResponse.statusCode().toString()).append("\n");
            clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> sb.append(name).append(": ").append(value).append("\n")));
            log.debug(sb.toString());
        }
        return Mono.just(clientResponse);
    });
}

Ahora podemos cambiar el nivel de registro de reactor.netty.http.client a INFO o ERROR para tener una salida más limpia.

4. Logging Request and Response with Body

Los clientes HTTP tienen características para registrar los cuerpos de las solicitudes y respuestas. Por lo tanto, para lograr el objetivo, vamos a utilizar un cliente HTTP habilitado para registro con nuestro WebClient.

Podemos hacer esto configurando manualmente WebClient.Builder#clientConnector. Veamos cómo hacerlo con el cliente HTTP de Jetty y Netty.

4.1. Logging with Jetty HttpClient

Primero, agreguemos la dependencia Maven para el *jetty-reactive-httpclient* a nuestro pom.xml:

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-reactive-httpclient</artifactId>
    <version>1.1.6</version>
</dependency>

Luego, crearemos un cliente HTTP de Jetty personalizado:

SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
HttpClient httpClient = new HttpClient(sslContextFactory) {
    @Override
    public Request newRequest(URI uri) {
        Request request = super.newRequest(uri);
        return enhance(request);
    }
};

Aquí, hemos anulado HttpClient#newRequest, luego envuelto el Request en un mejorador de registro.

A continuación, necesitamos registrar eventos con la solicitud para que podamos registrar cada parte de la solicitud cuando esté disponible:

Request enhance(Request request) {
    StringBuilder group = new StringBuilder();
    request.onRequestBegin(theRequest -> {
        group.append("Request URI: ").append(theRequest.getURI()).append("\n");
        group.append("Method: ").append(theRequest.getMethod()).append("\n");
    });
    request.onRequestHeaders(theRequest -> {
        for (HttpField header : theRequest.getHeaders()) {
            group.append(header.getName()).append(": ").append(header.getValue()).append("\n");
        }
    });
    request.onRequestContent((theRequest, content) -> {
        group.append("Content: ").append(content.toString()).append("\n");
    });
    request.onRequestSuccess(theRequest -> {
        log.debug(group.toString());
        group.setLength(0);
    });
    return request;
}

Finalmente, debemos construir la instancia de WebClient:

WebClient
  .builder()
  .clientConnector(new JettyClientHttpConnector(httpClient))
  .build();

4.2. Logging with Netty HttpClient

Ahora, creemos un cliente HTTP de Netty:

HttpClient httpClient = HttpClient
  .create()
  .wiretap(true);

Habiendo habilitado el wiretap, cada solicitud y respuesta se registrará con detalle completo.

A continuación, tenemos que establecer el nivel de registro del paquete del cliente de Netty reactor.netty.http.client a DEBUG:

logging.level.reactor.netty.http.client=DEBUG

Ahora, construyamos el WebClient:

WebClient
  .builder()
  .clientConnector(new ReactorClientHttpConnector(httpClient))
  .build();

Nuestro WebClient registrará cada solicitud y respuesta en detalle completo. Sin embargo, el formato por defecto del registrador interno de Netty contiene tanto la representación en Hex como la representación en texto de los cuerpos, así como muchos datos sobre los eventos de solicitud y respuesta.

Para obtener solo el registrador de texto para Netty, podemos configurar el HttpClient:

HttpClient httpClient = HttpClient
  .create()
  .wiretap("reactor.netty.http.client.HttpClient", 
    LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL);

5. Conclusión

En este tutorial, hemos utilizado varias técnicas para registrar datos de solicitudes y respuestas mientras utilizamos Spring WebClient. Hemos aprendido cómo personalizar el cliente HTTP Reactivo de Spring para capturar y registrar información detallada, lo que es esencial para una aplicación robusta y transparentemente rastreable.

### Consejos prácticos:

  • Usa niveles de log ajustados: Ajusta los niveles de log según el entorno (desarrollo, producción) para evitar la recopilación de información innecesaria.
  • Filtros personalizados: Mantén los filtros para las solicitudes y respuestas personalizados y reutilizables para diversas partes de tu aplicación.
  • Implementa trazabilidad: Considera integrar características adicionales, como rastreadores de solicitudes, para seguir el rastro a través de microservicios.
  • Revisión periódica: Revisa y ajusta la lógica de registro según se detecten cuellos de botella o problemas en la aplicación.

Al seguir estos consejos y aprender a personalizar el WebClient, podrás optimizar el rendimiento de tus aplicaciones y facilitar la depuración y el mantenimiento a largo plazo. ¡Feliz codificación!