La librería Zalando Logbook para emitir trazas de peticiones web

Escrito por picodotdev el .
java
Enlace permanente Comentarios

Las trazas en llamadas REST son fundamentales para la observabilidad en microservicios, ya que la red es un medio poco fiable con múltiples puntos de fallo. Sin un service mesh, cada servicio debe instrumentar sus propias llamadas HTTP, lo que con soluciones manuales resulta limitado y difícil de mantener. Zalando Logbook resuelve esto ofreciendo soporte para las librerías HTTP más populares de Java y Spring Boot, con funcionalidades avanzadas como correlación petición/respuesta, filtrado y ofuscación de datos sensibles. El artículo complementa el ejemplo con una configuración de Log4j2 que distingue entre entornos locales con salida en consola y entornos productivos con formato JSON estructurado para GCP.

Java

Las trazas o logs son uno de los métodos para tener observabilidad en un servicio. En los microservicios que usa REST que utilizan un método poco fiable como la red o aún no fallando es posible obtener una respuesta indicando algún tipo de error como validación de datos o error interno del servidor.

Añadir observabilidad en las llamadas REST es una forma de observar si los servicios están funcionando de la forma esperada o si hay errores que manifiestan algún tipo de comportamiento anómalo.

La librería Zalando Logbook permite emitir trazas en las llamadas recibidas o realizadas y las respuestas devueltas o que se obtienen. Tiene soporte para las librerías que permiten hacer peticiones http más populares y una buena cantidad de opciones de configuración para adaptarla a las necesidades del servicio.

Llamadas remotas

La red está compuesta por múltiples elementos (balanceadores, proxies, firewalls, DNS) cada uno de ellos un potencial punto de fallo. Pueden fallar de formas muy diversas: timeouts, pérdida de paquetes, reinicios abruptos o configuraciones incorrectas que interrumpen el servicio de forma temporal pero suficiente para causar errores en cascada.

En arquitecturas de microservicios, una solución es adoptar un service mesh como Istio, Linkerd o Consul, que traslada las responsabilidades de monitorización, trazabilidad y resiliencia a la infraestructura, evitando duplicar esa lógica en cada servicio. Sin embargo, cuando no se dispone de un service mesh, instrumentar los clientes HTTP con logs es la alternativa más directa para ganar visibilidad sobre esos puntos de fallo.

La solución simple

La forma de añadir logs en las llamadas HTTP depende de la librería utilizada: HttpClient de Java, WebClient de Spring o OkHttp. En cada caso, es necesario instrumentar el cliente implementando un interceptor o listener que emita los mensajes de log. Esta responsabilidad recae completamente en el desarrollador, lo que implica más código a mantener y un resultado con capacidades limitadas.

A continuación se muestra un ejemplo básico con OkHttp, válido para casos simples. Para necesidades más avanzadas, lo recomendable es utilizar una librería especializada como Zalando Logbook.

La clase del OkHttp HttpLoggingInterceptor permite cierto nivel de configucación en cuanto a que nivel de detalle mostrar pero no permite modificar el formato y tantas opciones de configuración como Logbook.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.github.picodotdev.blogbitix.zalandologbook;

...

@Component
public class Beans {

    @Bean
    HttpLogFormatter buildHttpLogFormatter() {
        return new CustomHttpLogFormatter();
    }

    @Bean("okHttpClient")
    OkHttpClient buildOkHttpClient() {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
        return new OkHttpClient.Builder()
                   .addInterceptor(logging)
                   .build();
    }

    ...
}
Beans-1.java

El resultado es el siguiente.

1
2
3
4
...
2026-04-26 11:57:48,135  INFO  0d042cea-6d13-400f-9f44-43aac43e106b                                         okhttp3.OkHttpClient --> GET https://duckduckgo.com/
2026-04-26 11:57:48,446  INFO  0d042cea-6d13-400f-9f44-43aac43e106b                                         okhttp3.OkHttpClient <-- 200 https://duckduckgo.com/ (309ms, unknown-length body)
...
System-1.out

La librería Zalando Logbook

Zalando Logbook es una librería Java diseñada específicamente para registrar trazas de las peticiones y respuestas HTTP, tanto entrantes como salientes. Sus principales características son:

  • Soporte multi-librería: compatible con OkHttp, HttpClient, WebClient, RestTemplate, Feign y otras.
  • Correlación petición/respuesta: asocia cada petición con su respuesta correspondiente en los logs.
  • Filtrado condicional: permite registrar solo las llamadas que cumplan determinados criterios.
  • Ofuscación de datos sensibles: enmascara cabeceras o campos del cuerpo como tokens, contraseñas o datos personales.
  • Formatos de salida configurables: JSON estructurado, texto plano u otros formatos personalizados.
  • Integración nativa con Spring Boot: configuración mediante application.yml sin necesidad de código adicional.

Otra ventaja relevante es el desacoplamiento, si en algún momento se decide cambiar de librería HTTP o de formato de log, basta con ajustar la configuración de Logbook sin tocar la lógica de negocio o código de la aplicación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package io.github.picodotdev.blogbitix.zalandologbook;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.logging.log4j.util.Strings;
import org.zalando.logbook.Correlation;
import org.zalando.logbook.HttpLogFormatter;
import org.zalando.logbook.HttpRequest;
import org.zalando.logbook.HttpResponse;
import org.zalando.logbook.Origin;
import org.zalando.logbook.Precorrelation;

public final class CustomHttpLogFormatter implements HttpLogFormatter {

    @Override
    public String format(final Precorrelation precorrelation, final HttpRequest request) throws IOException {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("precorrelation", precorrelation.getId());
        fields.put("remote", request.getRemote());
        fields.put("host", request.getHost());
        fields.put("method", request.getMethod());
        fields.put("uri", request.getRequestUri());
        fields.put("contentType", request.getContentType());
        fields.put("protocol", request.getProtocolVersion());
        fields.put("bodySize", String.valueOf(request.getBody().length));
        return direction(request) + format(fields);
    }

    @Override
    public String format(final Correlation correlation, final HttpResponse response) throws IOException {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("correlation", correlation.getId());
        fields.put("contentType", response.getContentType());
        fields.put("status", String.valueOf(response.getStatus()));
        fields.put("protocol", response.getProtocolVersion());
        fields.put("duration", String.valueOf(correlation.getDuration().toMillis()));
        fields.put("bodySize", String.valueOf(response.getBody().length));
        return direction(response) + format(fields);
    }

    private String direction(final HttpRequest request) {
        return (request.getOrigin() == Origin.REMOTE) ? "Incoming request" : "Outgoing request";
    }

    private String direction(final HttpResponse response) {
        return (response.getOrigin() == Origin.REMOTE) ? "Incoming response" : "Outgoing response";
    }

    private String format(Map<String, String> map) {
        return map.entrySet().stream().filter(it -> Strings.isNotBlank(it.getValue()))
                  .map(it -> String.format("%s: %s", it.getKey(), it.getValue()))
                  .collect(Collectors.joining(", ", "(", ")"));
    }
}
CustomHttpLogFormatter.java

Esta es la configuración necesaria para proporcionar el interceptor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.github.picodotdev.blogbitix.zalandologbook;

...

@Component
public class Beans {

    @Bean
    HttpLogFormatter buildHttpLogFormatter() {
        return new CustomHttpLogFormatter();
    }

    @Bean("okHttpClient")
    OkHttpClient buildOkHttpClient() {
        HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
        logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
        return new OkHttpClient.Builder()
                   .addInterceptor(logging)
                   .build();
    }

    @Bean("logbookOkHttpClient")
    OkHttpClient buildLogbookOkHttpClient(Logbook logbook) {
        return new OkHttpClient.Builder()
                   .addNetworkInterceptor(new LogbookInterceptor(logbook))
                   .addNetworkInterceptor(new GzipInterceptor())
                   .build();
    }
}
Beans-2.java

Y el código donde se emite la traza.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.github.picodotdev.blogbitix.zalandologbook;

...

@RestController
public class Controller {

    private static final Logger logger = LogManager.getLogger();

    private final OkHttpClient okHttpClient;
    private final OkHttpClient logbookOkHttpClient;

    public Controller(@Qualifier("okHttpClient") OkHttpClient okHttpClient, @Qualifier("logbookOkHttpClient") OkHttpClient logbookOkHttpClient) {
        this.okHttpClient = okHttpClient;
        this.logbookOkHttpClient = logbookOkHttpClient;
    }

    @GetMapping("/")
    public String hello() throws Exception {
        try {
            ThreadContext.put("correlation", UUID.randomUUID().toString());

            Request request = new Request.Builder().url("https://duckduckgo.com/").build();
            Call callOkHttpClient = okHttpClient.newCall(request);
            Call callLogbookOkHttpClient = logbookOkHttpClient.newCall(request);

            logger.info("Without Logbook");
            callOkHttpClient.execute();

            logger.info("With Logbook");
            callLogbookOkHttpClient.execute();

            return "Hello World!";
        } finally {
            ThreadContext.clearAll();
        }
    }
}
Controller.java

El resultado muestra no solo las peticiones que realiza la aplicación, también muestra las peticiones que recibe.

1
2
3
4
5
2026-04-26 11:57:48,108  TRACE                                   org.zalando.logbook.Logbook Incoming request(precorrelation: 90414981e848eddf, remote: 0:0:0:0:0:0:0:1, host: localhost, method: GET, uri: http://localhost:8080/?aaa=b, protocol: HTTP/1.1, bodySize: 0)
...
2026-04-26 11:57:48,537  TRACE 0d042cea-6d13-400f-9f44-43aac43e106b                                  org.zalando.logbook.Logbook Outgoing request(precorrelation: ba2a883ea8c6b2ca, remote: localhost, host: duckduckgo.com, method: GET, uri: https://duckduckgo.com/, protocol: HTTP/1.1, bodySize: 0)
2026-04-26 11:57:48,654  TRACE 0d042cea-6d13-400f-9f44-43aac43e106b                                  org.zalando.logbook.Logbook Incoming response(correlation: ba2a883ea8c6b2ca, contentType: text/html; charset=UTF-8, status: 200, protocol: H2, duration: 117, bodySize: 0)
2026-04-26 11:57:48,664  TRACE                                   org.zalando.logbook.Logbook Outgoing response(correlation: 90414981e848eddf, contentType: text/plain;charset=ISO-8859-1, status: 200, protocol: HTTP/1.1, duration: 559, bodySize: 0)
System-2.out
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: