Skip to content

Commit a9d5676

Browse files
committed
feat: unify tiered diagnostic logging
1 parent 4fc02cb commit a9d5676

13 files changed

Lines changed: 196 additions & 43 deletions

pom.xml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@
353353
<junit.version>4.13.2</junit.version>
354354
<slf4j.version>2.0.18</slf4j.version>
355355
<lombok.version>1.18.46</lombok.version>
356-
<jackson.version>2.22.0</jackson.version>
356+
<jackson.version>2.17.2</jackson.version>
357357
<junit-jupiter.version>6.1.0</junit-jupiter.version>
358358
<commons-lang.version>2.6</commons-lang.version>
359359
</properties>
@@ -504,6 +504,7 @@
504504
<junit.version>5.11.4</junit.version>
505505
<lombok.version>1.18.46</lombok.version>
506506
<okhttp3.version>5.4.0</okhttp3.version>
507+
<okhttp3-extension.version>3.0.x.20260630-SNAPSHOT</okhttp3-extension.version>
507508
<slf4j.version>2.0.18</slf4j.version>
508509

509510
<!-- Maven Plugin versions -->
@@ -528,6 +529,11 @@
528529
<!-- 依赖版本统一管理(dependencyManagement) -->
529530
<dependencyManagement>
530531
<dependencies>
532+
<dependency>
533+
<groupId>io.github.easy4j</groupId>
534+
<artifactId>okhttp3-extension</artifactId>
535+
<version>${okhttp3-extension.version}</version>
536+
</dependency>
531537
<!-- For OkHttp -->
532538
<dependency>
533539
<groupId>com.squareup.okhttp3</groupId>
@@ -546,6 +552,16 @@
546552
<version>${okhttp3.version}</version>
547553
</dependency>
548554
<!-- For Jackson -->
555+
<dependency>
556+
<groupId>com.fasterxml.jackson.core</groupId>
557+
<artifactId>jackson-annotations</artifactId>
558+
<version>${jackson.version}</version>
559+
</dependency>
560+
<dependency>
561+
<groupId>com.fasterxml.jackson.core</groupId>
562+
<artifactId>jackson-core</artifactId>
563+
<version>${jackson.version}</version>
564+
</dependency>
549565
<dependency>
550566
<groupId>com.fasterxml.jackson.core</groupId>
551567
<artifactId>jackson-databind</artifactId>
@@ -582,6 +598,10 @@
582598

583599
<!-- 项目依赖(dependencies) -->
584600
<dependencies>
601+
<dependency>
602+
<groupId>io.github.easy4j</groupId>
603+
<artifactId>okhttp3-extension</artifactId>
604+
</dependency>
585605
<!-- For OkHttp -->
586606
<dependency>
587607
<groupId>com.squareup.okhttp3</groupId>

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import io.github.easy4j.hermes.api.HermesApiConstants;
33
import lombok.Data;
44

5+
import java.util.Objects;
6+
57
/**
68
* <p>Hermes 本地 CLI 配置。</p>
79
*
@@ -13,6 +15,23 @@
1315
@Data
1416
public class HermesCliConfig {
1517

18+
/** CLI 与 HTTP/SSE 通道共享的调试策略。 */
19+
private final HermesDebugConfig debug;
20+
21+
/** 使用独立的默认调试策略创建配置。 */
22+
public HermesCliConfig() {
23+
this(new HermesDebugConfig());
24+
}
25+
26+
/**
27+
* 使用指定调试策略创建配置。
28+
*
29+
* @param debug 客户端共享调试策略
30+
*/
31+
public HermesCliConfig(HermesDebugConfig debug) {
32+
this.debug = Objects.requireNonNull(debug, "debug");
33+
}
34+
1635
/**
1736
* 是否启用对应客户端通道。
1837
*/

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,8 +337,9 @@ private static void copyHttpConfig(HermesHttpClientConfig src, HermesHttpClientC
337337
target.setStreamReconnectInitialDelayMillis(src.getStreamReconnectInitialDelayMillis());
338338
target.setStreamReconnectMaxDelayMillis(src.getStreamReconnectMaxDelayMillis());
339339
target.setRetryOnConnectionFailure(src.isRetryOnConnectionFailure());
340-
target.setDetailedLoggingEnabled(src.isDetailedLoggingEnabled());
341-
target.setMaxLoggedBodyLength(src.getMaxLoggedBodyLength());
340+
target.getDebug().setEnabled(src.getDebug().isEnabled());
341+
target.getDebug().setLevel(src.getDebug().getLevel());
342+
target.getDebug().setMaxContentLength(src.getDebug().getMaxContentLength());
342343
target.setVerifySsl(src.isVerifySsl());
343344
target.setDefaultModel(src.getDefaultModel());
344345
target.setDefaultInstructions(src.getDefaultInstructions());

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
@Data
1313
public class HermesClientConfig {
1414

15+
/** 客户端所有通信通道共享的调试配置。 */
16+
private final HermesDebugConfig debug = new HermesDebugConfig();
17+
1518
/**
1619
* HTTP 与 SSE 通道配置。
1720
*/
18-
private final HermesHttpClientConfig http = new HermesHttpClientConfig();
21+
private final HermesHttpClientConfig http = new HermesHttpClientConfig(debug);
1922

2023
/**
2124
* 本地 Hermes CLI 客户端。
2225
*/
23-
private final HermesCliConfig cli = new HermesCliConfig();
26+
private final HermesCliConfig cli = new HermesCliConfig(debug);
2427
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.easy4j.hermes;
2+
3+
import lombok.Data;
4+
import okhttp3.extension.logging.HttpLogLevel;
5+
6+
/**
7+
* Hermes SDK 统一调试配置,用于控制生命周期、请求头和正文日志。
8+
*
9+
* <p>调试默认关闭。正文日志始终受长度限制,认证头和敏感令牌仍由客户端脱敏。</p>
10+
*
11+
* @author <a href="https://github.com/loong10k">Loong Wan</a>
12+
* @since 1.0.0
13+
*/
14+
@Data
15+
public class HermesDebugConfig {
16+
17+
/** 是否允许 SDK 输出调试诊断信息。 */
18+
private boolean enabled;
19+
20+
/** 启用调试后的详细程度。 */
21+
private HttpLogLevel level = HttpLogLevel.BASIC;
22+
23+
/** BODY 级别单项正文允许记录的最大字符数。 */
24+
private int maxContentLength = 2_000;
25+
26+
/**
27+
* 判断指定级别的日志是否允许输出。
28+
*
29+
* @param required 待输出信息要求的最低级别
30+
* @return 调试已启用且当前级别满足要求时返回 {@code true}
31+
*/
32+
public boolean allows(HttpLogLevel required) {
33+
return enabled && level != null && level.allows(required);
34+
}
35+
36+
/**
37+
* 返回经过下限保护的正文日志长度。
38+
*
39+
* @return 至少为 1 的最大正文字符数
40+
*/
41+
public int resolveMaxContentLength() {
42+
return Math.max(1, maxContentLength);
43+
}
44+
}

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@
1515
@Data
1616
public class HermesHttpClientConfig {
1717

18+
/** HTTP 与 SSE 通道共享的调试配置。 */
19+
private final HermesDebugConfig debug;
20+
21+
/** 使用默认关闭的调试配置创建 HTTP 配置。 */
22+
public HermesHttpClientConfig() {
23+
this(new HermesDebugConfig());
24+
}
25+
26+
/**
27+
* 使用客户端级共享调试配置创建 HTTP 配置。
28+
*
29+
* @param debug 客户端级调试配置
30+
*/
31+
public HermesHttpClientConfig(HermesDebugConfig debug) {
32+
this.debug = Objects.requireNonNull(debug, "debug");
33+
}
34+
1835
/**
1936
* 对话响应模式,默认返回完整响应。
2037
*/
@@ -130,16 +147,6 @@ public class HermesHttpClientConfig {
130147
*/
131148
private boolean retryOnConnectionFailure = true;
132149

133-
/**
134-
* 是否输出脱敏后的请求头及截断后的请求响应体。
135-
*/
136-
private boolean detailedLoggingEnabled = false;
137-
138-
/**
139-
* 详细日志允许输出的正文最大字符数。
140-
*/
141-
private int maxLoggedBodyLength = 2_000;
142-
143150
/**
144151
* 是否校验 HTTPS 主机名和证书。
145152
*/

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

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import lombok.Getter;
1313
import lombok.extern.slf4j.Slf4j;
1414
import okhttp3.*;
15+
import okhttp3.extension.logging.HttpLogLevel;
1516

1617
import java.io.IOException;
1718
import java.nio.charset.StandardCharsets;
@@ -94,11 +95,13 @@ private HermesHttpClient(HermesHttpClientConfig config, ObjectMapper objectMappe
9495
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) : objectMapper;
9596
this.httpClient = Objects.requireNonNull(httpClient, "httpClient");
9697
this.ownsHttpClient = ownsHttpClient;
97-
log.debug("Hermes HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
98-
+ "callTimeoutMs={}, retryOnConnectionFailure={}, detailedLoggingEnabled={}",
99-
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getReadTimeoutMillis(),
100-
config.getCallTimeoutMillis(), config.isRetryOnConnectionFailure(),
101-
config.isDetailedLoggingEnabled());
98+
if (allows(HttpLogLevel.BASIC)) {
99+
log.debug("Hermes HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
100+
+ "callTimeoutMs={}, retryOnConnectionFailure={}, debugLevel={}",
101+
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getReadTimeoutMillis(),
102+
config.getCallTimeoutMillis(), config.isRetryOnConnectionFailure(),
103+
config.getDebug().getLevel());
104+
}
102105
}
103106

104107
// ============================================================
@@ -906,7 +909,9 @@ private void closeRegistration(AutoCloseable registration) {
906909
try {
907910
registration.close();
908911
} catch (Exception error) {
909-
log.debug("Failed to unregister HTTP cancellation callback: {}", error.getMessage());
912+
if (allows(HttpLogLevel.BASIC)) {
913+
log.debug("Failed to unregister HTTP cancellation callback: {}", error.getMessage());
914+
}
910915
}
911916
}
912917

@@ -984,19 +989,25 @@ private boolean isSuccessful() {
984989
private long beginTrace(Request request) {
985990
// 原子序列只用于 JVM 内关联请求生命周期日志,不改变协议与重试语义。
986991
long requestId = REQUEST_SEQUENCE.incrementAndGet();
987-
log.debug("HTTP request started: requestId={}, method={}, url={}",
988-
requestId, request.method(), request.url());
989-
if (config.isDetailedLoggingEnabled()) {
990-
log.debug("HTTP request details: requestId={}, headers={}, body={}", requestId,
991-
redactHeaders(request.headers()), requestBody(request));
992+
if (allows(HttpLogLevel.BASIC)) {
993+
log.debug("HTTP request started: requestId={}, method={}, url={}",
994+
requestId, request.method(), request.url());
995+
}
996+
if (allows(HttpLogLevel.HEADERS)) {
997+
log.debug("HTTP request headers: requestId={}, headers={}", requestId, redactHeaders(request.headers()));
998+
}
999+
if (allows(HttpLogLevel.BODY)) {
1000+
log.debug("HTTP request body: requestId={}, body={}", requestId, requestBody(request));
9921001
}
9931002
return requestId;
9941003
}
9951004

9961005
private void logResponse(long requestId, Request request, int status, String body, long startedAt) {
997-
log.debug("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}",
998-
requestId, request.method(), request.url(), status, body.length(), elapsedMillis(startedAt));
999-
if (config.isDetailedLoggingEnabled()) {
1006+
if (allows(HttpLogLevel.BASIC)) {
1007+
log.debug("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}",
1008+
requestId, request.method(), request.url(), status, body.length(), elapsedMillis(startedAt));
1009+
}
1010+
if (allows(HttpLogLevel.BODY)) {
10001011
log.debug("HTTP response body: requestId={}, body={}", requestId, truncate(body));
10011012
}
10021013
}
@@ -1024,16 +1035,20 @@ private String requestBody(Request request) {
10241035
}
10251036

10261037
private String truncate(String value) {
1027-
int limit = Math.max(0, config.getMaxLoggedBodyLength());
1038+
int limit = config.getDebug().resolveMaxContentLength();
10281039
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
10291040
}
10301041

1042+
private boolean allows(HttpLogLevel level) {
1043+
return config.getDebug().allows(level);
1044+
}
1045+
10311046
private Headers redactHeaders(Headers headers) {
10321047
Headers.Builder safe = headers.newBuilder();
10331048
for (String name : headers.names()) {
10341049
String lowerName = name.toLowerCase();
10351050
if ("authorization".equals(lowerName) || lowerName.contains("token") || lowerName.contains("key")) {
1036-
safe.set(name, "██");
1051+
safe.set(name, "<redacted>");
10371052
}
10381053
}
10391054
return safe.build();

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import okhttp3.sse.EventSource;
1818
import okhttp3.sse.EventSourceListener;
1919
import okhttp3.sse.EventSources;
20+
import okhttp3.extension.logging.HttpLogLevel;
2021

2122
import java.io.IOException;
2223
import java.util.Collections;
@@ -128,12 +129,11 @@ public HermesSseClient(HermesHttpClientConfig config, ObjectMapper objectMapper,
128129
thread.setDaemon(true);
129130
return thread;
130131
});
131-
log.debug("Hermes SSE client initialized: baseUrl={}, reconnectMaxAttempts={}, "
132-
+ "reconnectInitialDelayMs={}, reconnectMaxDelayMs={}, eventQueueCapacity={}, "
133-
+ "detailedLoggingEnabled={}",
132+
debug(HttpLogLevel.BASIC, "Hermes SSE client initialized: baseUrl={}, reconnectMaxAttempts={}, "
133+
+ "reconnectInitialDelayMs={}, reconnectMaxDelayMs={}, eventQueueCapacity={}, debugLevel={}",
134134
config.getBaseUrl(), config.getStreamReconnectMaxAttempts(),
135135
config.getStreamReconnectInitialDelayMillis(), config.getStreamReconnectMaxDelayMillis(),
136-
config.getStreamEventQueueCapacity(), config.isDetailedLoggingEnabled());
136+
config.getStreamEventQueueCapacity(), config.getDebug().getLevel());
137137
}
138138

139139
/**
@@ -251,7 +251,7 @@ private void connect(SubscriptionState subscription, RequestFactory requestFacto
251251
*/
252252
@Override
253253
public void onOpen(EventSource eventSource, Response response) {
254-
log.info("Hermes SSE connected: label={}, url={}, status={}, elapsedMs={}",
254+
debug(HttpLogLevel.BASIC, "Hermes SSE connected: label={}, url={}, status={}, elapsedMs={}",
255255
label, request.url(), response.code(), elapsedMillis(startedAt));
256256
}
257257

@@ -280,7 +280,12 @@ public void onEvent(EventSource eventSource, String id, String type, String data
280280
event.setEvent(type);
281281
consumer.accept(event);
282282
} catch (Exception error) {
283-
log.debug("Hermes SSE parse failed: label={}, data={}", label, data, error);
283+
if (config.getDebug().allows(HttpLogLevel.BODY)) {
284+
log.debug("Hermes SSE parse failed: label={}, data={}", label, truncate(data), error);
285+
} else {
286+
debug(HttpLogLevel.BASIC, "Hermes SSE parse failed: label={}, dataLength={}, error={}",
287+
label, data.length(), error.getMessage());
288+
}
284289
}
285290
}
286291

@@ -435,6 +440,17 @@ private long elapsedMillis(long startedAt) {
435440
return (System.nanoTime() - startedAt) / 1_000_000L;
436441
}
437442

443+
private void debug(HttpLogLevel level, String message, Object... arguments) {
444+
if (config.getDebug().allows(level)) {
445+
log.debug(message, arguments);
446+
}
447+
}
448+
449+
private String truncate(String value) {
450+
int limit = config.getDebug().resolveMaxContentLength();
451+
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
452+
}
453+
438454
private void finish(SubscriptionState subscription) {
439455
subscription.handle.close();
440456
}

0 commit comments

Comments
 (0)