Читаемый журнал отладки для HTTP-запросов с помощью Spring Webclient

Я использую Spring реактивный WebClient для отправки запросов на http-сервер. Чтобы просмотреть отправляемый базовый запрос и ответ, я включил ведение журнала отладки для пакета reactor.ipc.netty.

Заголовки исходящих запросов можно просмотреть в обычном режиме.

Хотя я отправляю и получаю простой текст через http, журнал содержит запросы и ответы в формате ниже (это шестнадцатеричный?)

Я не знаю, как просматривать записанные данные в удобной для понимания форме. А еще лучше записать запрос и ответ в понятной форме

Вот фрагмент записанных данных

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 53 65 61 72 63 68 5f 47 43 2e 61 |GET /Search_GC.a|
|00000010| 73 70 78 20 48 54 54 50 2f 31 2e 31 0d 0a 75 73 |spx HTTP/1.1..us|
|00000020| 65 72 2d 61 67 65 6e 74 3a 20 52 65 61 63 74 6f |er-agent: Reacto|
|00000030| 72 4e 65 74 74 79 2f 30 2e 37 2e 32 2e 52 45 4c |rNetty/0.7.2.REL|
|00000040| 45 41 53 45 0d 0a 68 6f 73 74 3a 20 63 65 6f 6b |EASE..host: ceok|
|00000050| 61 72 6e 61 74 61 6b 61 2e 6b 61 72 2e 6e 69 63 |arnataka.kar.nic|
|00000060| 2e 69 6e 0d 0a 61 63 63 65 70 74 3a 20 2a 2f 2a |.in..accept: */*|
|00000070| 0d 0a 61 63 63 65 70 74 2d 65 6e 63 6f 64 69 6e |..accept-encodin|
|00000080| 67 3a 20 67 7a 69 70 0d 0a 63 6f 6e 74 65 6e 74 |g: gzip..content|
|00000090| 2d 6c 65 6e 67 74 68 3a 20 30 0d 0a 0d 0a       |-length: 0....  |
+--------+-------------------------------------------------+----------------+

Обнаружен вопрос без ответа, который, должно быть, происходит из-за той же библиотеки: Чтение HttpContent с PooledUnsafeDirectByteBuf

Проблема возникла здесь

Похоже, что существует ортодоксальная точка зрения, что для реактивных клиентов отладка не требуется. Это совершенно бессмысленные аргументы, поскольку мы используем такие инструменты, как rest client, postman, curl, httpie и другие для отправки запроса и просмотра ответа.


person Ashok Koyi    schedule 01.12.2017    source источник
comment
Вы можете поместить прокси, например MembraneSoa, между   -  person Marged    schedule 01.12.2017
comment
Не уверен, насколько полезно было бы отлаживать запрос приложения через специальные прокси.   -  person Ashok Koyi    schedule 01.12.2017
comment
Возможный дубликат Как регистрировать тела запросов и ответов весной WebFlux   -  person Abhijit Sarkar    schedule 02.01.2018
comment
Вы нашли хорошее решение?   -  person membersound    schedule 11.07.2019
comment
@membersound Да, я нашел хорошее решение, см. мой ответ.   -  person Honza Zidek    schedule 09.04.2021
comment
@AbhijitSarkar, я не согласен с вами, вопрос, о котором вы говорите, касается общего механизма перехвата запроса / ответа и / или реализации любого пользовательского журнала, в то время как этот вопрос касается того, как настроить формат встроенного механизм низкоуровневого ведения журнала.   -  person Honza Zidek    schedule 09.04.2021


Ответы (3)


Они изменили класс reactor.netty.http.client.HttpClient, после того, как я обновился до io.projectreactor.netty:reactor-netty-http:1.0.5, следующий код компилируется и делает то, что вы ожидаете. (Я не уверен, какая версия минимальная, я обновил что-то более старое, но думаю, что это 1.0.0. Это транзитивная зависимость, я обновил spring-boot-starter-webflux с 2.3.4.RELEASE до 2.4.4.)

Важнейшей частью является вызов _ 7_:

wiretap("reactor.netty.http.client.HttpClient", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL, StandardCharsets.UTF_8)

Он также регистрирует заголовок и тело запроса и ответа.

Весь контекст таков:

package com.example;

import io.netty.handler.logging.LogLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.logging.AdvancedByteBufFormat;

import java.nio.charset.StandardCharsets;

@Slf4j
class RestClientTest {
    private WebClient createWebClient() {
        final HttpClient httpClient = HttpClient.create()
                .wiretap(HttpClient.class.getCanonicalName(), LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL, StandardCharsets.UTF_8);
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class User {
        int id;
        int userId;
        String title;
        String body;
    }

    @Test
    void createUsersReactive() {
        final WebClient webClient = createWebClient();
        final String url = "http://jsonplaceholder.typicode.com/posts";
        final Mono<User> userMono = webClient.method(HttpMethod.POST)
                .uri(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .header("X-My-Header", "MyValue1", "MyValue2")
                .body(BodyInserters.fromValue(User.builder().userId(1).title("foo").body("bar").build()))
                .retrieve()
                .bodyToMono(User.class);
        final User user = userMono.block();
        log.info("Created user: " + user);
    }
}

И вывод в журнал доступен для чтения человеком, когда вы запрашиваете:

... reactor.netty.http.client.HttpClient     : [id:e7d7ed93] REGISTERED
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93] CONNECT: jsonplaceholder.typicode.com/<unresolved>:80
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93] ACTIVE
... r.netty.http.client.HttpClientConnect    : [id:e7d7ed93-1] Handler is being applied: {uri=http://jsonplaceholder.typicode.com/posts, method=POST}
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93-1] WRITE: 217B POST /posts HTTP/1.1
user-agent: ReactorNetty/1.0.5
host: jsonplaceholder.typicode.com
accept: */*
Content-Type: application/json;charset=UTF-8
X-My-Header: MyValue1
X-My-Header: MyValue2
content-length: 46


... reactor.netty.http.client.HttpClient     : [id:e7d7ed93-1] WRITE: 46B {"id":0,"userId":1,"title":"foo","body":"bar"}
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93-1] FLUSH
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93-1] READ: 1347B HTTP/1.1 201 Created
Date: Tue, 13 Apr 2021 12:49:33 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 65
X-Powered-By: Express
X-Ratelimit-Limit: 1000
X-Ratelimit-Remaining: 999
X-Ratelimit-Reset: 1618318233
Vary: Origin, X-HTTP-Method-Override, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Access-Control-Expose-Headers: Location
Location: http://jsonplaceholder.typicode.com/posts/101
X-Content-Type-Options: nosniff
Etag: W/"41-0LtsWqhuQ7Zsjlj0tYnOrT/Vw5o"
Via: 1.1 vegur
CF-Cache-Status: DYNAMIC
cf-request-id: 096ce0bd560000736722853000000001
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=laAKjgGcoi8SLu%2F6VX5pQIAksdmj9xi31elC5Ld97eljznKIpYjdkQsittoMJp3lJoQIwOACmj89bKSa%2Ff15gRHRmyasV2Xcl%2FmVjJBJm7ytbWocp39UBd90JwVM"}],"max_age":604800,"group":"cf-nel"}
NEL: {"max_age":604800,"report_to":"cf-nel"}
Server: cloudflare
CF-RAY: 63f4d0a88ed07367-CPH
alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400
Proxy-Connection: Keep-Alive
Connection: Keep-Alive
Set-Cookie: __cfduid=d11c86fbd953f7cf768cf7db0c346f22b1618318173; expires=Thu, 13-May-21 12:49:33 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax

{
  "id": 101,
  "userId": 1,
  "title": "foo",
  "body": "bar"
}
... r.n.http.client.HttpClientOperations     : [id:e7d7ed93-1] Received response (auto-read:false) : [Date=Tue, 13 Apr 2021 12:49:33 GMT, Content-Type=application/json; charset=utf-8, X-Powered-By=Express, X-Ratelimit-Limit=1000, X-Ratelimit-Remaining=999, X-Ratelimit-Reset=1618318233, Vary=Origin, X-HTTP-Method-Override, Accept-Encoding, Access-Control-Allow-Credentials=true, Cache-Control=no-cache, Pragma=no-cache, Expires=-1, Access-Control-Expose-Headers=Location, Location=http://jsonplaceholder.typicode.com/posts/101, X-Content-Type-Options=nosniff, Etag=W/"41-0LtsWqhuQ7Zsjlj0tYnOrT/Vw5o", Via=1.1 vegur, CF-Cache-Status=DYNAMIC, cf-request-id=096ce0bd560000736722853000000001, Report-To={"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=laAKjgGcoi8SLu%2F6VX5pQIAksdmj9xi31elC5Ld97eljznKIpYjdkQsittoMJp3lJoQIwOACmj89bKSa%2Ff15gRHRmyasV2Xcl%2FmVjJBJm7ytbWocp39UBd90JwVM"}],"max_age":604800,"group":"cf-nel"}, NEL={"max_age":604800,"report_to":"cf-nel"}, Server=cloudflare, CF-RAY=63f4d0a88ed07367-CPH, alt-svc=h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, Proxy-Connection=Keep-Alive, Connection=Keep-Alive, Set-Cookie=__cfduid=d11c86fbd953f7cf768cf7db0c346f22b1618318173; expires=Thu, 13-May-21 12:49:33 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax, content-length=65]
... r.n.http.client.HttpClientOperations     : [id:e7d7ed93-1] Received last HTTP packet
... reactor.netty.http.client.HttpClient     : [id:e7d7ed93] READ COMPLETE
... com.example.RestClientTest              : Created user: RestClientIT.User(id=101, userId=1, title=foo, body=bar)
person Honza Zidek    schedule 09.04.2021

Вы можете сделать это с помощью doOnNext (), если вы используете DataBuffer в качестве читателя:

public Mono<ServerResponse> selectByPost(ServerRequest request) {
  Flux<DataBuffer> requestBodyFlux = request.bodyToFlux(DataBuffer.class)
    .doOnNext(dataBuffer -> {
      if (debug ) {
        log.debug(new String(dataBuffer.asByteBuffer().array()));
      }
      Scannable.from(dataBuffer).tags().forEach(System.out::println);
    });
}

Это, вероятно, не лучший способ сделать это, конечно, было бы неплохо, если бы netty предоставляла разные способы регистрации полезной нагрузки. У Hex есть свои преимущества, в зависимости от того, что вам нужно отлаживать.

person Frischling    schedule 21.09.2018
comment
Теперь netty действительно предоставляет более элегантный способ, см. Мой ответ. - person Honza Zidek; 13.04.2021

Похоже, что отвечающий сервер возвращает сжатый контент, поэтому имеет смысл, что вы не можете его прочитать.

Если вы действительно хотите перехватить на необработанном уровне HTTP, убедитесь, что заголовок вашего запроса не указывает, что он может принимать GZip-контент (accept-encoding: gzip).

Другой альтернативой может быть регистрация запроса на другом уровне, когда он уже был распакован из потока необработанных данных, но еще не обработан кодом вашего приложения - хотя не уверен, как это будет работать в Reactive webclient;)

person Frederik Heremans    schedule 01.12.2017
comment
Нет, даже тело моего запроса, представляющее собой обычный текст, находится в этом формате. - person Ashok Koyi; 01.12.2017
comment
Предоставленное вами тело может быть в виде обычного текста, но клиент Spring применит к нему некоторую магию, прежде чем он будет помещен в сеть, чтобы минимизировать накладные расходы / пропускную способность. Не уверен, можно ли отключить GZipping - person Frederik Heremans; 01.12.2017
comment
Здесь мы используем пружину на стороне клиента. Может ли клиент отправлять данные в формате gzip, не зная возможностей сервера? или это вообще разрешено для клиентских запросов на отправку в формате gzip? Я не думаю, что проблема в gzip. Я предполагаю, что они печатают его в каком-то шестнадцатеричном формате, чтобы охватить как двоичные, так и текстовые сценарии использования тела запроса. - person Ashok Koyi; 02.12.2017
comment
Это не сжато - это просто запрос, отображаемый как в формате ascii, так и в шестнадцатеричном формате ... - person Brian Clozel; 24.02.2018