Integrace systémů je podle mé zkušenosti jedna z nejkomplikovanějších věcí (kromě invalidace cache a pojmenování). Ne že by to byla raketová věda, ale existuje spoustu drobností, které se můžou rozsypat. Pravděpodobnost, že se to stane, hraničí téměř s jistotou. Jakmile věci vypustíte z rukou, těžko se ladí. Stopujete pak pouze své logy. Proto tímto apeluji, hlavně sám k sobě: „Loguj pořádně!“ Pojďme se podívat, jak ve Springu zapnout podrobnější výpis HTTP volání.

Úvod

Švy, ve kterých jsou systémy napojené k sobě, jsou místa, kde to může drhnout víc než jinde. Části se obvykle vyvíjejí samostatně a od první spasování snad ani nikdo nečeká, že by klaplo na první dobrou. Ovšem je potřeba mít zpětnou vazbu, co a jak opravit. Případně důkaz, že chyba není na vaší straně. Nemusí se nutně jednat o systémy třetích stran. Může to být jedna z mnoha vašich mikroslužeb.

Jako hlavní zásadu bych uvedl logovat okamžik před voláním jiného systému a pak výsledek. Ať už neúspěch nebo chybu. Samozřejmě dodat co největší kontext s přihlédnutím k osobním údajům (PII). Pro první okamžiky integrace a testovací prostředí bychom měli mít možnost si zapnout podrobnější výpis bez ohledu na citlivost dat.

Základní logování

Dejme tomu, že voláme službu, poskytující informace o uživateli. Může vracet spoustu citlivých údajů jako datum narození, číslo sociálního pojištění a podobně. Jako nutné, nikoliv postačující, minimum, vidím následující.

2022-11-29 21:29:43.262  INFO --- [main] cz.zvestov.blog.UserConnector : Calling user detail for ID: 42
2022-11-29 21:29:43.345  INFO --- [main] cz.zvestov.blog.UserConnector : Got name='Joe Smith' for user ID: 42

A ano, píšu o takové trivialitě, protože se mi to podařilo opomenout.

Spring

Pojďme si ukázat, jak lze nastavit podrobnější výpis HTTP volání ve Spring frameworku. Ten nám nabízí především dvě možnosti. Jednak třídu RestTemplate, která už od verze 5 není vyvíjena, pouze udržovaná, jednak WebClient s podporou asynchronního zpracování.

RestTemplate

Nastavme úroveň logování org.springframework.web.client.RestTemplate na DEBUG.

2022-11-29 21:34:51.427  INFO --- [main] cz.zvestov.blog.UserConnector : Calling user detail for ID: 42
2022-11-29 21:34:51.447 DEBUG --- [main] o.s.web.client.RestTemplate   : HTTP GET http://localhost:8080/user/42
2022-11-29 21:34:51.484 DEBUG --- [main] o.s.web.client.RestTemplate   : Accept=[application/json, application/*+json]
2022-11-29 21:34:51.497 DEBUG --- [main] o.s.web.client.RestTemplate   : Response 200 OK
2022-11-29 21:34:51.500 DEBUG --- [main] o.s.web.client.RestTemplate   : Reading to [cz.zvestov.blog.User]
2022-11-29 21:34:51.510  INFO --- [main] cz.zvestov.blog.UserConnector : Got name='Joe Smith' for user ID: 42

To jsme si moc nepomohli, že?

Podle článku Spring RestTemplate Request/Response Logging nastavíme použití Apache HttpClient.

final RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

Dále nastavíme úroveň logování org.apache.http.wire na DEBUG.

2022-11-29 21:40:26.685  INFO --- [-main] cz.zvestov.blog.UserConnector : Calling user detail for ID: 42
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "GET /user/42 HTTP/1.1[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "Accept: application/json, application/*+json[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "Host: localhost:8080[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.13 (Java/19.0.1)[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
2022-11-29 21:40:26.759 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 >> "[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "HTTP/1.1 200 [\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "Content-Type: application/json[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "Transfer-Encoding: chunked[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "Date: Tue, 29 Nov 2022 20:40:26 GMT[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "Keep-Alive: timeout=60[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "Connection: keep-alive[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "39[\r][\n]"
2022-11-29 21:40:26.761 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "{"id":"42","name":"Joe Smith","dateOfBirth":"1979-08-10"}[\r][\n]"
2022-11-29 21:40:26.779 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "0[\r][\n]"
2022-11-29 21:40:26.779 DEBUG --- [-main] org.apache.http.wire          : http-outgoing-0 << "[\r][\n]"
2022-11-29 21:40:26.779  INFO --- [-main] cz.zvestov.blog.UserConnector : Got name='Joe Smith' for user ID: 42

Ano, je to hrozně ukecané, ale nikdo přece neříká, že to máte mít puštěné pořád.

WebClient

Podle článku Logging Spring WebClient Calls pojďme nastavit použití Netty HttpClient.

final HttpClient httpClient = HttpClient.create()
        .wiretap(UserConnector.class.getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL);
final WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

Dále nastavíme úroveň logování cz.zvestov.blog.UserConnector na TRACE.

2022-11-29 21:49:48.252  INFO --- [           main] cz.zvestov.blog.UserConnector : Calling user detail for ID: 42
2022-11-29 21:49:48.783 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae] REGISTERED
2022-11-29 21:49:48.810 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae] CONNECT: localhost/127.0.0.1:8080
2022-11-29 21:49:48.815 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] ACTIVE
2022-11-29 21:49:48.840 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae-1, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] WRITE: 93B GET /user/42 HTTP/1.1
user-agent: ReactorNetty/1.0.17
host: localhost:8080
accept: */*


2022-11-29 21:49:48.841 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae-1, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] FLUSH
2022-11-29 21:49:48.844 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae-1, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] WRITE: 0B
2022-11-29 21:49:48.845 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae-1, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] FLUSH
2022-11-29 21:49:48.846 TRACE --- [ctor-http-nio-2] cz.zvestov.blog.UserConnector : [cb7440ae-1, L:/127.0.0.1:65325 - R:localhost/127.0.0.1:8080] READ: 182B HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 Nov 2022 20:49:48 GMT

39
{"id":"42","name":"Joe Smith","dateOfBirth":"1979-08-10"}
0


2022-11-29 21:49:48.908  INFO --- [           main] cz.zvestov.blog.UserConnector : Got name='Joe Smith' for user ID: 42

Ano, stejně jako případ RestTemplate, i tohle použití je hrozně ukecané, ale opět nikdo netvrdí, že je to řešení na stálo.

Pozorný čtenář si všimne, že v odkazovaném článku je kód

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

Zatím co já navrhuji

.wiretap(UserConnector.class.getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL);

To mi umožňuje vypínat a zapínat podrobný výpis s jemnější granularitou, ne pro všechna použití HttpClient. Zanikne však zdroj dat. Možná by šlo vyumělkovat na použití neexistující třídy cz.zvestov.blog.UserConnector.HttpClient pomocí:

.wiretap(UserConnector.class.getCanonicalName() + ".HttpClient", LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL);

Chtěl bych každopádně zvýraznit možnost (oproti RestTemplate) volby úrovně logování LogLevel.TRACE. Vídal jsem projekty, kde se nechávala úroveň DEBUG v prvních okamžicích i na produkčním prostředí a to tady pochopitelně nechceme.

Závěr

Ukázali jsme si způsob, jak si můžete připravit možnost zapnout podrobnější výpis HTTP komunikace ve Spring frameworku. Mít možnost zvýšit úroveň logování bez změny binárek je k nezaplacení. Bez podobných informací jste doslova slepí a nezbývá než jen hádat, co se na prostředí děje. Hádám, že nakonec stejně člověk skončí u toho, že prosí o nasazení nové verze, kde už logování vylepšil. A jak to řešíte vy (tedy pokud vám tam nedovolili napíchnout Wireshark)?

Související