Skip to content

Commit c19ac13

Browse files
committed
feat: add safe HTTP trace logging
1 parent 16ab7ed commit c19ac13

5 files changed

Lines changed: 101 additions & 0 deletions

File tree

src/main/java/io/github/easy4j/hermes/HermesClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ private static void copyHttpConfig(HermesHttpClientConfig src, HermesHttpClientC
264264
target.setStreamKeepAliveMillis(src.getStreamKeepAliveMillis());
265265
target.setStreamEventQueueCapacity(src.getStreamEventQueueCapacity());
266266
target.setRetryOnConnectionFailure(src.isRetryOnConnectionFailure());
267+
target.setDetailedLoggingEnabled(src.isDetailedLoggingEnabled());
268+
target.setMaxLoggedBodyLength(src.getMaxLoggedBodyLength());
267269
target.setVerifySsl(src.isVerifySsl());
268270
target.setDefaultModel(src.getDefaultModel());
269271
target.setDefaultInstructions(src.getDefaultInstructions());

src/main/java/io/github/easy4j/hermes/HermesHttpClientConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ public class HermesHttpClientConfig {
9494
/** 遇到失效连接等传输故障时是否允许 OkHttp 自动恢复。 */
9595
private boolean retryOnConnectionFailure = true;
9696

97+
/**
98+
* 是否输出请求头、请求体及响应体等详细诊断信息。
99+
* <p>默认关闭;基础请求生命周期仍使用 DEBUG 日志。</p>
100+
*/
101+
private boolean detailedLoggingEnabled = false;
102+
103+
/** 详细日志中请求体、响应体的最大字符数。 */
104+
private int maxLoggedBodyLength = 2_000;
105+
97106
/**
98107
* 是否校验 HTTPS 证书;为 false 时关闭校验(仅建议开发环境)。
99108
*/

src/main/java/io/github/easy4j/hermes/api/HermesHttpClient.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import java.io.IOException;
1919
import java.util.*;
20+
import java.util.concurrent.atomic.AtomicLong;
21+
import okio.Buffer;
2022

2123
/**
2224
* Hermes Server HTTP 客户端,封装 REST API。
@@ -26,6 +28,7 @@
2628
public 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);

src/main/java/io/github/easy4j/hermes/api/HermesSseClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public HermesSseClient(HermesHttpClientConfig config, ObjectMapper objectMapper,
5757
this.ownsHttpClient = Objects.isNull(httpClient);
5858
this.httpClient = this.ownsHttpClient ? HermesOkHttpClientFactory.create(config) : httpClient;
5959
this.streamExecutor = createStreamExecutor(config);
60+
log.debug("Hermes SSE client initialized: baseUrl={}, corePoolSize={}, maxPoolSize={}, queueCapacity={}, "
61+
+ "eventQueueCapacity={}, detailedLoggingEnabled={}", config.getBaseUrl(),
62+
config.getStreamCorePoolSize(), config.getStreamMaxPoolSize(), config.getStreamQueueCapacity(),
63+
config.getStreamEventQueueCapacity(), config.isDetailedLoggingEnabled());
6064
}
6165

6266
private static ExecutorService createStreamExecutor(HermesHttpClientConfig config) {
@@ -163,6 +167,8 @@ private void doSubscribePost(String url,
163167
};
164168
try {
165169
Request request = buildPostSseRequest(url, requestBody, headers);
170+
long startedAt = System.nanoTime();
171+
log.debug("SSE chat subscription started: url={}", url);
166172
Call call = httpClient.newCall(request);
167173
sub.callRef.set(call);
168174
try (Response response = call.execute()) {
@@ -171,6 +177,8 @@ private void doSubscribePost(String url,
171177
onError.accept(new HermesHttpException(response.code(), body));
172178
return;
173179
}
180+
log.info("SSE chat connected: url={}, status={}, elapsedMs={}", url, response.code(),
181+
(System.nanoTime() - startedAt) / 1_000_000L);
174182
if (response.body() != null) {
175183
parseSseSource(response.body().source(), consumer, completeOnce, sub);
176184
}

src/test/java/io/github/easy4j/hermes/HermesHttpClientConfigTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
import io.github.easy4j.hermes.api.sse.StreamingChatResponse;
88

99
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
1011

1112
class HermesHttpClientConfigTest {
1213

1314
@Test
1415
void shouldExposeUnifiedHttpProperties() {
1516
HermesHttpClientConfig config = new HermesHttpClientConfig();
1617
assertEquals(HttpResponseMode.BLOCKING, config.getMode());
18+
assertFalse(config.isDetailedLoggingEnabled());
19+
assertEquals(2_000, config.getMaxLoggedBodyLength());
1720
config.setBaseUrl("http://hermes");
1821
assertEquals("http://hermes", config.getBaseUrl());
1922

0 commit comments

Comments
 (0)