Emitir trazas de las peticiones y respuestas HTTP con clientes Java

Escrito por picodotdev el .
java planeta-codigo programacion
Enlace permanente Comentarios

En las peticiones HTTP que se hacen unos microservicios a otros o a recursos externos son varias las cosas por las que una petición HTTP es capaz de fallar. El fallo es un código de estado distinto al correcto 200, ya sea petición invalida 400, un recurso no encontrado 404, credenciales requeridas 401, credenciales inválidas 403 o un error interno de servidor 500 entre otros códigos de estado, otras posibilidades son fallos de conexión de red. Añadir trazas de las peticiones que hacen los microservicios permite obtener información con la que averiguar cual es el motivo de fallo, si además emiten las trazas con las cabeceras y parámetros en formato de la herramienta curl es fácil probar y reproducir el mismo error o la misma acción.

Hay varios aspectos de la aplicación en los que es muy útil añadir trazas, los más comunes son añadir trazas en determinados puntos del código que permiten averiguar cuál ha sido el camino seguido en el procesamiento, también ciertos valores de variables. Las trazas es un aspecto importante en la monitorización de una aplicación.

Otra información interesante a emitir en las trazas son las sentencias SQL que genera una aplicación ya sea en el momento de desarrollo para detectar problemas de rendimiento ocasionados por un 1+N al usar algún ORM como JPA o Hibernate. También por una consulta que tiene mal rendimiento ya que no hace uso de índices lo que hace que el motor de la base de datos tenga acceder a demasiados registros o hace demasiadas joins. Conocer la SQL exacta o consultas con mal rendimiento que se están ejecutando permite optimizarlas.

En este artículo comento otro caso habitual en el que es interesante añadir trazas, las peticiones HTTP a otros sistemas.

Añadir trazas de las peticiones HTTP

Una de las características que definen a los microservicios es que unos se comunican con otros. Pueden estar basados en REST o en GraphQL en ambos casos utilizando el protocolo HTTP, también pueden estar basados en RPC con gRPC.

En el caso de los microservicios que utilizan HTTP es interesante que emitan en las trazas la petición HTTP que están realizando ya que las comunicaciones entre microservicios son un punto de fallo común, servicios que no están respondiendo con los datos esperados, el código de estado, cabeceras de respuesta y que a qué recurso se está llamando, con qué datos y cabeceras en la petición. Además, si las trazas se emiten en el formato de la herramienta curl hace muy fácil probar la misma consulta que hace la aplicación.

Dependiendo de la librería que se utilice para hacer las peticiones HTTP depende de como instrumentalizadas para añadirles el soporte para que emitan trazas, los clientes HTTP más populares son el cliente HTTP de Java añadido entre las novedades de Java 9, WebClient de Spring, Retrofit y OkHttp.

A continuación están los ejemplos de cómo añadir trazas en cada una de estas librerías, todos consisten básicamente añadir un interceptor que es llamado cuando se realiza una petición y se recibe la respuestas, en este interceptor es posible emitir las trazas de las peticiones HTTP.

Estas son las dependencias de librerías para los cliente.

 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
plugins {
    id 'application'
}

repositories {
    jcenter()
}

dependencies {
    // HttpClient
    implementation 'codes.rafael.interceptablehttpclient:interceptable-http-client:1.0'
    // WebClient
    implementation 'org.springframework:spring-webflux:5.3.0'
    implementation 'org.projectreactor:reactor-spring:1.0.1.RELEASE'
    implementation 'io.projectreactor.netty:reactor-netty:1.0.0'
    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
    // OkHttp
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
}

application {
    mainClass = 'io.github.picodotdev.blogbitix.httpclientlog.Main'
}
build.gradle

Trazas con el cliente Java

El cliente de Java en la versión de Java 9 no incluye en su API un soporte sencillo para añadir trazas al contrario de las otras librerías, sin embargo, aún no ofrececiento este soporte como el cliente está incluido en el JDK sigue siendo una buena opción para reducir el número de dependencias.

Una posibilidad para no tener que crear una implementación propia para añadirle trazabilidad al cliente HTTP de Java es utilizar la librería interceptable-http-client que precisamente prporciona la implementación, esta librería es del mismo autor que otras conocidas como Byte Buddy.

 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
package io.github.picodotdev.blogbitix.httpclientlog;

...

public class Main {

    ...

    public void httpClient() throws Exception {
        Function<HttpRequest, String> onRequest = (HttpRequest r) -> {
            String headers = r.headers().map().entrySet().stream().map((e) -> {
                return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
            }).collect(Collectors.joining(","));
            return String.format("curl -v %s -X %s %s %s", VERSION.get(r.version().orElse(HttpClient.Version.HTTP_2)), r.method().toUpperCase(), headers, r.uri());
        };
        BiConsumer<HttpResponse<?>, String> onResponse = (HttpResponse<?> r, String curl) -> {
            String headers = r.headers().map().entrySet().stream().map((e) -> {
                return String.format("[%s: %s]", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
            }).collect(Collectors.joining(","));
            System.out.printf("%s%n", curl);
            System.out.printf("%s %s%n", r.statusCode(), headers);
        };
        BiConsumer<Throwable, String> onError = (Throwable t, String curl) -> {
            System.out.printf("%s%n", curl);
            t.printStackTrace();
        };

        HttpClient client = InterceptableHttpClient.builder().version(HttpClient.Version.HTTP_2).interceptor(onRequest, onResponse, onError).build();

        HttpResponse<String> response = client.send(HttpRequest.newBuilder(new URI("https://www.google.es/")).headers("User-Agent", "java/1.0").GET().build(), HttpResponse.BodyHandlers.ofString());
    }

    ...
}
Main-HttpClient.java
1
2
3
HttpClient
curl -v --http2 -X GET -H "User-Agent: java/1.0" https://www.google.es/
200 :status: 200,accept-ranges: none,alt-svc: h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43",cache-control: private, max-age=0,content-type: text/html; charset=ISO-8859-1,date: Fri, 06 Nov 2020 18:02:55 GMT,expires: -1,p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info.",server: gws,set-cookie: NID=204=EGARz6ltyG7XQjKh88UMLg3-_hzPTBHpFe1ieqw71HQ-1dSaJqJVsQXAqrVQwxSB7bocuJ-higxMOBehSJvKnGOBOoxGWThDuVyvRlW-FYJfMSw7QW0UWRYZqApuiunasmE8AWCOS0W5pAKxLykbJK_6HFSOMYcjEHE6vzLkTvI; expires=Sat, 08-May-2021 18:02:55 GMT; path=/; domain=.google.es; HttpOnly,vary: Accept-Encoding,x-frame-options: SAMEORIGIN,x-xss-protection: 0
System.out-HttpClient

Trazas con Spring WebClient

El cliente WebClient de Spring posee una API que permite realizar peticiones HTTP con pocas líneas de código.

 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
package io.github.picodotdev.blogbitix.httpclientlog;

...

public class Main {

    ...

    public void webClient() {
        ExchangeFilterFunction logRequest = ExchangeFilterFunction.ofRequestProcessor(r -> {
            String headers = r.headers().entrySet().stream().map((e) -> {
                return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
            }).collect(Collectors.joining(","));
            System.out.printf("curl -v %s -X %s %s %s%n", HttpClient.Version.HTTP_2, r.method().name().toUpperCase(), headers, r.url());
            return Mono.just(r);
        });

        ExchangeFilterFunction logRespose = ExchangeFilterFunction.ofResponseProcessor(r -> {
            String headers = r.headers().asHttpHeaders().entrySet().stream().map((e) -> {
                return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
            }).collect(Collectors.joining(","));
            System.out.printf("%s %s%n", r.statusCode(), headers);
            return Mono.just(r);
        });

        WebClient client = WebClient.builder().filters(f -> {
            f.add(logRequest);
            f.add(logRespose);
        }).baseUrl("https://www.google.com/").build();
        client.get().uri("/").header("User-Agent", "java/1.0").retrieve().toEntity(String.class).block();
    }

    ...
}
Main-WebClient.java
1
2
3
WebClient
curl -v HTTP_2 -X GET -H "User-Agent: java/1.0" https://www.google.com/
200 Date: Fri, 06 Nov 2020 18:02:56 GMT,Expires: -1,Cache-Control: private, max-age=0,Content-Type: text/html; charset=ISO-8859-1,P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info.",Server: gws,X-XSS-Protection: 0,X-Frame-Options: SAMEORIGIN,Set-Cookie: NID=204=OdP7rZlC5x02VjtcCx2tJGcyi68LaugdvQVzGmXWeeUazgiI1qkvMQvwRxCCvxsKvf1asPh4smC8jeIb0vGCQ9D2SGmUWziHFOLgr-Dhq4DnyIQmG0OUlSwhTrl3SoyFvcZrP_xBBCZksDhAgI-iFZdWqb4-U6lnMiY5q2oj8BY; expires=Sat, 08-May-2021 18:02:56 GMT; path=/; domain=.google.com; HttpOnly,Alt-Svc: h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43",Transfer-Encoding: chunked
System.out-WebClient

Trazas con Retrofit

Lo interesante del cliente Retrofit es que convierte una interfaz REST de un servicio en una interfaz de Java, lo interesante de esto es que aparentemente se está usando un objeto pero que de forma subyacente se hacen peticiones HTTP. La librería cliente de HTTP que utiliza es OkHttp.

El interceptor se añade sobre el cliente OkHttp que luego es utilizado para construir el cliente del servicio con Retrofit.

 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
58
package io.github.picodotdev.blogbitix.httpclientlog;

...

public class Main {

    ...

    public interface GoogleService {
        @GET("/")
        @Headers("User-Agent: java/1.0")
        Call<String> get();
    }


    public void retrofit() throws Exception {
        Interceptor interceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                logRequest(chain.request());
                Response response = chain.proceed(chain.request());
                logResponse(response);
                return response;
            }

            private void logRequest(Request r) throws IOException {
                String headers = r.headers().toMultimap().entrySet().stream().map((e) -> {
                    return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
                }).collect(Collectors.joining(","));
                System.out.printf("curl -v %s -X %s %s %s%n", HttpClient.Version.HTTP_2, r.method().toUpperCase(), headers, r.url());
            }

            private void logResponse(Response r) throws IOException {
                String headers = r.headers().toMultimap().entrySet().stream().map((e) -> {
                    return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
                }).collect(Collectors.joining(","));
                System.out.printf("%s %s%n", r.code(), headers);
            }
        };

        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://www.google.com/")
                .client(client)
                .addConverterFactory(ScalarsConverterFactory.create())
                .build();

        GoogleService service = retrofit.create(GoogleService.class);
        service.get().execute();
        client.dispatcher().executorService().shutdown();
        client.connectionPool().evictAll();
    }

    ...
}
Main-Retrofit.java
1
2
3
Retrofit
curl -v HTTP_2 -X GET -H "user-agent: java/1.0" https://www.google.com/
200 alt-svc: h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43",cache-control: private, max-age=0,content-type: text/html; charset=ISO-8859-1,date: Fri, 06 Nov 2020 18:02:57 GMT,expires: -1,p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info.",server: gws,set-cookie: NID=204=X1R50uh209S4FMzXNh5uLVYax4PghnyMqaHHk-q3YndWWnTxE8oShyfmWRIEM-tcWXvoja2S_N_gO_aHlCL6X0r1e3ITJ-dgSBR2G4wTsfFLAi1G_MBVV8MTkHtKkqAHkR5H73p9D5t-nJG3PcnXLZFHYUKNdlzoF6BYStOCpyQ; expires=Sat, 08-May-2021 18:02:57 GMT; path=/; domain=.google.com; HttpOnly,x-frame-options: SAMEORIGIN,x-xss-protection: 0
System.out-Retrofit

Trazas con OkHttp

OkHttp es otra de las librerías para realizar peticiones HTTP populares en Java. Para añadir trazas hay que crear un interceptor y añadirlo al cliente.

 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
package io.github.picodotdev.blogbitix.httpclientlog;

...

public class Main {

    ...

    public void okHttp() throws Exception {
        Interceptor interceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                logRequest(chain.request());
                Response response = chain.proceed(chain.request());
                logResponse(response);
                return response;
            }

            private void logRequest(Request r) throws IOException {
                String headers = r.headers().toMultimap().entrySet().stream().map((e) -> {
                    return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
                }).collect(Collectors.joining(","));
                System.out.printf("curl -v %s -X %s %s %s%n", HttpClient.Version.HTTP_2, r.method().toUpperCase(), headers, r.url());
            }

            private void logResponse(Response r) throws IOException {
                String headers = r.headers().toMultimap().entrySet().stream().map((e) -> {
                    return String.format("-H \"%s: %s\"", e.getKey(), e.getValue().stream().collect(Collectors.joining(",")));
                }).collect(Collectors.joining(","));
                System.out.printf("%s %s%n", r.code(), headers);
            }
        };

        Request request = new Request.Builder()
                .url("https://www.google.com/")
                .addHeader("User-Agent", "java/1.0")
                .build();
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(interceptor)
                .build();
        client.newCall(request).execute().close();
        client.dispatcher().executorService().shutdown();
        client.connectionPool().evictAll();
    }

    ...
}
Main-OkHttp.java
1
2
3
OkHttp
curl -v HTTP_2 -X GET -H "user-agent: java/1.0" https://www.google.com/
200 alt-svc: h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43",cache-control: private, max-age=0,content-type: text/html; charset=ISO-8859-1,date: Fri, 06 Nov 2020 18:02:57 GMT,expires: -1,p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info.",server: gws,set-cookie: NID=204=C7o2wM5asqIWOZgvG7m8RlrZtILJdO8xc0gogTUwLwXhNPK2PeDmhDKbXIsCDBbG5dhA3zXyM4caW0roWpyGJZaUoyMDrDtRgjOsChlT7NkxanFIY8xxI4plxUrqFZspkNlB3r25wjo2UwFXCOumBjSSZI7XRqp-AvOTF8e-DMQ; expires=Sat, 08-May-2021 18:02:57 GMT; path=/; domain=.google.com; HttpOnly,x-frame-options: SAMEORIGIN,x-xss-protection: 0
System.out-OkHttp

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: