1717
1818import java .io .IOException ;
1919import java .util .*;
20+ import java .util .concurrent .atomic .AtomicLong ;
21+ import okio .Buffer ;
2022
2123/**
2224 * Hermes Server HTTP 客户端,封装 REST API。
2628public class HermesHttpClient implements AutoCloseable {
2729
2830 private static final MediaType JSON = MediaType .get ("application/json; charset=utf-8" );
31+ private static final AtomicLong REQUEST_SEQUENCE = new AtomicLong ();
2932
3033 private final HermesHttpClientConfig config ;
3134 private final ObjectMapper objectMapper ;
@@ -49,6 +52,11 @@ private HermesHttpClient(HermesHttpClientConfig config, ObjectMapper objectMappe
4952 .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false ) : objectMapper ;
5053 this .httpClient = Objects .requireNonNull (httpClient , "httpClient" );
5154 this .ownsHttpClient = ownsHttpClient ;
55+ log .debug ("Hermes HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
56+ + "callTimeoutMs={}, retryOnConnectionFailure={}, detailedLoggingEnabled={}" ,
57+ config .getBaseUrl (), config .getConnectTimeoutMillis (), config .getReadTimeoutMillis (),
58+ config .getCallTimeoutMillis (), config .isRetryOnConnectionFailure (),
59+ config .isDetailedLoggingEnabled ());
5260 }
5361
5462 // ============================================================
@@ -377,16 +385,23 @@ private <T> T execute(Request request, Class<T> type) {
377385 }
378386
379387 private <T > T execute (Request request , Class <T > type , HttpCallCancellation cancellation ) {
388+ long requestId = beginTrace (request );
389+ long startedAt = System .nanoTime ();
380390 Call call = httpClient .newCall (request );
381391 AutoCloseable registration = cancellation != null ? cancellation .onCancel (call ::cancel ) : null ;
382392 try (Response response = call .execute ()) {
383393 String respBody = response .body () != null ? response .body ().string () : "" ;
394+ logResponse (requestId , request , response .code (), respBody , startedAt );
384395 if (!response .isSuccessful ()) {
385396 throw new HermesHttpException (response .code (), respBody );
386397 }
387398 return objectMapper .readValue (respBody , type );
388399 } catch (IOException e ) {
400+ logFailure (requestId , request , startedAt , e );
389401 throw new HermesHttpException ("HTTP request failed: " + e .getMessage (), e );
402+ } catch (RuntimeException e ) {
403+ logFailure (requestId , request , startedAt , e );
404+ throw e ;
390405 } finally {
391406 closeRegistration (registration );
392407 }
@@ -404,17 +419,81 @@ private void closeRegistration(AutoCloseable registration) {
404419 }
405420
406421 private <T > T executeList (Request request , TypeReference <T > typeRef ) {
422+ long requestId = beginTrace (request );
423+ long startedAt = System .nanoTime ();
407424 try (Response response = httpClient .newCall (request ).execute ()) {
408425 String respBody = response .body () != null ? response .body ().string () : "" ;
426+ logResponse (requestId , request , response .code (), respBody , startedAt );
409427 if (!response .isSuccessful ()) {
410428 throw new HermesHttpException (response .code (), respBody );
411429 }
412430 return objectMapper .readValue (respBody , typeRef );
413431 } catch (IOException e ) {
432+ logFailure (requestId , request , startedAt , e );
414433 throw new HermesHttpException ("HTTP request failed: " + e .getMessage (), e );
434+ } catch (RuntimeException e ) {
435+ logFailure (requestId , request , startedAt , e );
436+ throw e ;
415437 }
416438 }
417439
440+ private long beginTrace (Request request ) {
441+ long requestId = REQUEST_SEQUENCE .incrementAndGet ();
442+ log .debug ("HTTP request started: requestId={}, method={}, url={}" ,
443+ requestId , request .method (), request .url ());
444+ if (config .isDetailedLoggingEnabled ()) {
445+ log .debug ("HTTP request details: requestId={}, headers={}, body={}" , requestId ,
446+ redactHeaders (request .headers ()), requestBody (request ));
447+ }
448+ return requestId ;
449+ }
450+
451+ private void logResponse (long requestId , Request request , int status , String body , long startedAt ) {
452+ log .debug ("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}" ,
453+ requestId , request .method (), request .url (), status , body .length (), elapsedMillis (startedAt ));
454+ if (config .isDetailedLoggingEnabled ()) {
455+ log .debug ("HTTP response body: requestId={}, body={}" , requestId , truncate (body ));
456+ }
457+ }
458+
459+ private void logFailure (long requestId , Request request , long startedAt , Exception error ) {
460+ log .warn ("HTTP request failed: requestId={}, method={}, url={}, elapsedMs={}, error={}" ,
461+ requestId , request .method (), request .url (), elapsedMillis (startedAt ), error .getMessage ());
462+ }
463+
464+ private long elapsedMillis (long startedAt ) {
465+ return (System .nanoTime () - startedAt ) / 1_000_000L ;
466+ }
467+
468+ private String requestBody (Request request ) {
469+ if (Objects .isNull (request .body ())) {
470+ return "" ;
471+ }
472+ try {
473+ Buffer buffer = new Buffer ();
474+ request .body ().writeTo (buffer );
475+ return truncate (buffer .readUtf8 ());
476+ } catch (IOException error ) {
477+ return "<unavailable:" + error .getMessage () + ">" ;
478+ }
479+ }
480+
481+ private String truncate (String value ) {
482+ int limit = Math .max (0 , config .getMaxLoggedBodyLength ());
483+ return value .length () <= limit ? value : value .substring (0 , limit ) + "...<truncated>" ;
484+ }
485+
486+ private Headers redactHeaders (Headers headers ) {
487+ Headers .Builder safe = headers .newBuilder ();
488+ for (String name : headers .names ()) {
489+ String lowerName = name .toLowerCase ();
490+ if ("authorization" .equals (lowerName ) || lowerName .contains ("token" ) || lowerName .contains ("key" )) {
491+ safe .set (name , "██" );
492+ }
493+ }
494+ return safe .build ();
495+ }
496+
418497 private String toJson (Object body ) {
419498 try {
420499 return objectMapper .writeValueAsString (body );
0 commit comments