Skip to content

Commit 5a83b7e

Browse files
committed
feat: unify tiered diagnostic logging
1 parent 8bf9423 commit 5a83b7e

13 files changed

Lines changed: 176 additions & 47 deletions

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<junit.version>4.13.2</junit.version>
6767
<lombok.version>1.18.46</lombok.version>
6868
<okhttp.version>4.12.0</okhttp.version>
69+
<okhttp3-extension.version>1.0.x.20260630-SNAPSHOT</okhttp3-extension.version>
6970
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
7071
<slf4j.version>2.0.18</slf4j.version>
7172
<!-- Maven Plugin versions -->
@@ -89,6 +90,11 @@
8990
<!-- 依赖版本统一管理(dependencyManagement) -->
9091
<dependencyManagement>
9192
<dependencies>
93+
<dependency>
94+
<groupId>io.github.easy4j</groupId>
95+
<artifactId>okhttp3-extension</artifactId>
96+
<version>${okhttp3-extension.version}</version>
97+
</dependency>
9298
<!-- For Jackson BOM -->
9399
<dependency>
94100
<groupId>com.fasterxml.jackson</groupId>
@@ -155,6 +161,10 @@
155161

156162
<!-- 项目依赖(dependencies) -->
157163
<dependencies>
164+
<dependency>
165+
<groupId>io.github.easy4j</groupId>
166+
<artifactId>okhttp3-extension</artifactId>
167+
</dependency>
158168
<!-- For OkHttp -->
159169
<dependency>
160170
<groupId>com.squareup.okhttp3</groupId>

src/main/java/io/github/easy4j/opencode/OpenCodeCliConfig.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import lombok.Data;
44

5+
import java.util.Objects;
6+
57
/**
68
* Configuration for the local OpenCode CLI subsystem.
79
* <p>Covers local {@code opencode} executable path, timeouts, working directory,
@@ -16,6 +18,23 @@
1618
@Data
1719
public class OpenCodeCliConfig {
1820

21+
/** CLI 与 HTTP/SSE 通道共享的调试策略。 */
22+
private final OpenCodeDebugConfig debug;
23+
24+
/** 使用独立的默认调试策略创建配置。 */
25+
public OpenCodeCliConfig() {
26+
this(new OpenCodeDebugConfig());
27+
}
28+
29+
/**
30+
* 使用指定调试策略创建配置。
31+
*
32+
* @param debug 客户端共享调试策略
33+
*/
34+
public OpenCodeCliConfig(OpenCodeDebugConfig debug) {
35+
this.debug = Objects.requireNonNull(debug, "debug");
36+
}
37+
1938
/**
2039
* 是否启用本地 CLI 子系统。
2140
* <p>为 false 时跳过 CLI 相关初始化和检查。</p>

src/main/java/io/github/easy4j/opencode/OpenCodeClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ private void copyHttpConfig(OpenCodeHttpClientConfig src) {
323323
this.config.getHttp().setStreamKeepAliveMillis(src.getStreamKeepAliveMillis());
324324
this.config.getHttp().setStreamEventQueueCapacity(src.getStreamEventQueueCapacity());
325325
this.config.getHttp().setRetryOnConnectionFailure(src.isRetryOnConnectionFailure());
326+
this.config.getDebug().setEnabled(src.getDebug().isEnabled());
327+
this.config.getDebug().setLevel(src.getDebug().getLevel());
328+
this.config.getDebug().setMaxContentLength(src.getDebug().getMaxContentLength());
326329
this.config.getHttp().setVerifySsl(src.isVerifySsl());
327330
this.config.getHttp().setDefaultModel(src.getDefaultModel());
328331
this.config.getHttp().setDefaultAgent(src.getDefaultAgent());

src/main/java/io/github/easy4j/opencode/OpenCodeClientConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
@Data
1818
public class OpenCodeClientConfig {
1919

20+
/** 客户端所有通信通道共享的调试配置。 */
21+
private final OpenCodeDebugConfig debug = new OpenCodeDebugConfig();
22+
2023
/**
2124
* HTTP/Server 相关配置
2225
*/
23-
private final OpenCodeHttpClientConfig http = new OpenCodeHttpClientConfig();
26+
private final OpenCodeHttpClientConfig http = new OpenCodeHttpClientConfig(debug);
2427

2528
/**
2629
* 本地 CLI 相关配置
2730
*/
28-
private final OpenCodeCliConfig cli = new OpenCodeCliConfig();
31+
private final OpenCodeCliConfig cli = new OpenCodeCliConfig(debug);
2932
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.easy4j.opencode;
2+
3+
import lombok.Data;
4+
import okhttp3.extension.logging.HttpLogLevel;
5+
6+
/**
7+
* OpenCode 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 OpenCodeDebugConfig {
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/opencode/OpenCodeHttpClientConfig.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import lombok.Data;
44

5+
import java.util.Objects;
6+
57
/**
68
* Configuration for the OpenCode HTTP Server client.
79
* <p>Covers server base URL, Basic Auth, TLS, HTTP timeouts, connection pool sizing,
@@ -15,6 +17,23 @@
1517
@Data
1618
public class OpenCodeHttpClientConfig {
1719

20+
/** HTTP 与 SSE 通道共享的调试配置。 */
21+
private final OpenCodeDebugConfig debug;
22+
23+
/** 使用默认关闭的调试配置创建 HTTP 配置。 */
24+
public OpenCodeHttpClientConfig() {
25+
this(new OpenCodeDebugConfig());
26+
}
27+
28+
/**
29+
* 使用客户端级共享调试配置创建 HTTP 配置。
30+
*
31+
* @param debug 客户端级调试配置
32+
*/
33+
public OpenCodeHttpClientConfig(OpenCodeDebugConfig debug) {
34+
this.debug = Objects.requireNonNull(debug, "debug");
35+
}
36+
1837
/**
1938
* 对话响应模式,默认保持兼容的完整响应模式。
2039
*/
@@ -124,17 +143,6 @@ public class OpenCodeHttpClientConfig {
124143
*/
125144
private boolean retryOnConnectionFailure = true;
126145

127-
/**
128-
* 是否输出请求头、请求体及响应体等详细诊断信息。
129-
* <p>默认关闭;基础请求生命周期仍使用 DEBUG 日志。</p>
130-
*/
131-
private boolean detailedLoggingEnabled = false;
132-
133-
/**
134-
* 详细日志中请求体、响应体的最大字符数。
135-
*/
136-
private int maxLoggedBodyLength = 2_000;
137-
138146
/**
139147
* 是否校验 HTTPS 证书;为 false 时关闭校验(仅建议开发环境)。
140148
*/

src/main/java/io/github/easy4j/opencode/api/OpenCodeHttpClient.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.github.easy4j.opencode.exception.OpenCodeHttpException;
1111
import lombok.extern.slf4j.Slf4j;
1212
import okhttp3.*;
13+
import okhttp3.extension.logging.HttpLogLevel;
1314
import org.slf4j.Logger;
1415
import org.slf4j.LoggerFactory;
1516

@@ -78,11 +79,13 @@ public OpenCodeHttpClient(OpenCodeHttpClientConfig config, ObjectMapper objectMa
7879
this.objectMapper = Objects.isNull(objectMapper) ? new ObjectMapper()
7980
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false): objectMapper;
8081
this.httpClient = Objects.isNull(httpClient) ? buildOkHttpClient(config) : httpClient;
81-
log.debug("OpenCode HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
82-
+ "callTimeoutMs={}, retryOnConnectionFailure={}, detailedLoggingEnabled={}",
83-
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getReadTimeoutMillis(),
84-
config.getCallTimeoutMillis(), config.isRetryOnConnectionFailure(),
85-
config.isDetailedLoggingEnabled());
82+
if (allows(HttpLogLevel.BASIC)) {
83+
log.debug("OpenCode HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
84+
+ "callTimeoutMs={}, retryOnConnectionFailure={}, debugLevel={}",
85+
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getReadTimeoutMillis(),
86+
config.getCallTimeoutMillis(), config.isRetryOnConnectionFailure(),
87+
config.getDebug().getLevel());
88+
}
8689
}
8790

8891
private static OkHttpClient buildOkHttpClient(OpenCodeHttpClientConfig config) {
@@ -443,8 +446,10 @@ public CompletableFuture<String> ensureSessionAsync(String sessionKey, OpenCodeR
443446
}
444447
throw new CompletionException(cause);
445448
}
446-
log.debug("findSessionByTitle failed, sessionKey={}, error={}",
447-
sessionKey, error.getMessage());
449+
if (allows(HttpLogLevel.BASIC)) {
450+
log.debug("findSessionByTitle failed, sessionKey={}, error={}",
451+
sessionKey, error.getMessage());
452+
}
448453
return Optional.<Session>empty();
449454
}
450455
return sessions.stream().filter(session -> Objects.equals(sessionKey, session.getTitle()))
@@ -1448,7 +1453,9 @@ private void closeRegistration(AutoCloseable registration) {
14481453
try {
14491454
registration.close();
14501455
} catch (Exception error) {
1451-
log.debug("Failed to unregister HTTP cancellation callback: {}", error.getMessage());
1456+
if (allows(HttpLogLevel.BASIC)) {
1457+
log.debug("Failed to unregister HTTP cancellation callback: {}", error.getMessage());
1458+
}
14521459
}
14531460
}
14541461

@@ -1543,20 +1550,25 @@ private boolean isSuccessful() {
15431550

15441551
private long beginTrace(Request request) {
15451552
long requestId = REQUEST_SEQUENCE.incrementAndGet();
1546-
log.debug("HTTP request started: requestId={}, method={}, url={}",
1547-
requestId, request.method(), request.url());
1548-
if (config.isDetailedLoggingEnabled()) {
1549-
// 详细日志默认关闭;开启后仍对认证头、token 和 key 脱敏,并限制正文长度。
1550-
log.debug("HTTP request details: requestId={}, headers={}, body={}", requestId,
1551-
redactHeaders(request.headers()), requestBody(request));
1553+
if (allows(HttpLogLevel.BASIC)) {
1554+
log.debug("HTTP request started: requestId={}, method={}, url={}",
1555+
requestId, request.method(), request.url());
1556+
}
1557+
if (allows(HttpLogLevel.HEADERS)) {
1558+
log.debug("HTTP request headers: requestId={}, headers={}", requestId, redactHeaders(request.headers()));
1559+
}
1560+
if (allows(HttpLogLevel.BODY)) {
1561+
log.debug("HTTP request body: requestId={}, body={}", requestId, requestBody(request));
15521562
}
15531563
return requestId;
15541564
}
15551565

15561566
private void logResponse(long requestId, Request request, int status, String body, long startedAt) {
1557-
log.debug("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}",
1558-
requestId, request.method(), request.url(), status, body.length(), elapsedMillis(startedAt));
1559-
if (config.isDetailedLoggingEnabled()) {
1567+
if (allows(HttpLogLevel.BASIC)) {
1568+
log.debug("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}",
1569+
requestId, request.method(), request.url(), status, body.length(), elapsedMillis(startedAt));
1570+
}
1571+
if (allows(HttpLogLevel.BODY)) {
15601572
log.debug("HTTP response body: requestId={}, body={}", requestId, truncate(body));
15611573
}
15621574
}
@@ -1584,16 +1596,20 @@ private String requestBody(Request request) {
15841596
}
15851597

15861598
private String truncate(String value) {
1587-
int limit = Math.max(0, config.getMaxLoggedBodyLength());
1599+
int limit = config.getDebug().resolveMaxContentLength();
15881600
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
15891601
}
15901602

1603+
private boolean allows(HttpLogLevel level) {
1604+
return config.getDebug().allows(level);
1605+
}
1606+
15911607
private Headers redactHeaders(Headers headers) {
15921608
Headers.Builder safe = headers.newBuilder();
15931609
for (String name : headers.names()) {
15941610
String lowerName = name.toLowerCase();
15951611
if ("authorization".equals(lowerName) || lowerName.contains("token") || lowerName.contains("key")) {
1596-
safe.set(name, "██");
1612+
safe.set(name, "<redacted>");
15971613
}
15981614
}
15991615
return safe.build();

src/main/java/io/github/easy4j/opencode/api/OpenCodeSseClient.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import okhttp3.sse.EventSource;
1717
import okhttp3.sse.EventSourceListener;
1818
import okhttp3.sse.EventSources;
19+
import okhttp3.extension.logging.HttpLogLevel;
1920

2021
import java.util.Objects;
2122
import java.util.Set;
@@ -81,11 +82,10 @@ public OpenCodeSseClient(OpenCodeHttpClientConfig config, ObjectMapper objectMap
8182
OkHttpClient baseClient = ownsHttpClient
8283
? OpenCodeOkHttpClientFactory.create(config) : httpClient;
8384
this.httpClient = baseClient.newBuilder().readTimeout(0, TimeUnit.MILLISECONDS).build();
84-
log.debug("OpenCode SSE client initialized: baseUrl={}, maxRequests={}, "
85-
+ "maxRequestsPerHost={}, eventQueueCapacity={}, reconnectPolicy=none, "
86-
+ "detailedLoggingEnabled={}",
85+
debug(HttpLogLevel.BASIC, "OpenCode SSE client initialized: baseUrl={}, maxRequests={}, "
86+
+ "maxRequestsPerHost={}, eventQueueCapacity={}, reconnectPolicy=none, debugLevel={}",
8787
config.getBaseUrl(), config.getMaxRequests(), config.getMaxRequestsPerHost(),
88-
config.getStreamEventQueueCapacity(), config.isDetailedLoggingEnabled());
88+
config.getStreamEventQueueCapacity(), config.getDebug().getLevel());
8989
}
9090

9191
/**
@@ -130,7 +130,7 @@ public SseSubscription subscribeEvents(Consumer<SseEvent> consumer,
130130
EventSourceListener listener = new EventSourceListener() {
131131
@Override
132132
public void onOpen(EventSource eventSource, Response response) {
133-
log.info("OpenCode SSE connected: streamType=events, url={}, status={}, elapsedMs={}",
133+
debug(HttpLogLevel.BASIC, "OpenCode SSE connected: streamType=events, url={}, status={}, elapsedMs={}",
134134
request.url(), response.code(), elapsedMillis(startedAt));
135135
}
136136

@@ -144,10 +144,10 @@ public void onEvent(EventSource eventSource, String id, String type, String data
144144
try {
145145
consumer.accept(mapper.readValue(data, SseEvent.class));
146146
} catch (Exception error) {
147-
if (config.isDetailedLoggingEnabled()) {
148-
log.debug("Failed to parse OpenCode SSE event: data={}", data, error);
147+
if (config.getDebug().allows(HttpLogLevel.BODY)) {
148+
log.debug("Failed to parse OpenCode SSE event: data={}", truncate(data), error);
149149
} else {
150-
log.debug("Failed to parse OpenCode SSE event: dataLength={}, error={}",
150+
debug(HttpLogLevel.BASIC, "Failed to parse OpenCode SSE event: dataLength={}, error={}",
151151
data.length(), error.getMessage());
152152
}
153153
}
@@ -156,7 +156,7 @@ public void onEvent(EventSource eventSource, String id, String type, String data
156156
@Override
157157
public void onClosed(EventSource eventSource) {
158158
closeSubscription(subscriptionRef);
159-
log.info("OpenCode SSE closed: streamType=events, url={}", request.url());
159+
debug(HttpLogLevel.BASIC, "OpenCode SSE closed: streamType=events, url={}", request.url());
160160
}
161161

162162
@Override
@@ -336,6 +336,17 @@ private long elapsedMillis(long startedAt) {
336336
return (System.nanoTime() - startedAt) / 1_000_000L;
337337
}
338338

339+
private void debug(HttpLogLevel level, String message, Object... arguments) {
340+
if (config.getDebug().allows(level)) {
341+
log.debug(message, arguments);
342+
}
343+
}
344+
345+
private String truncate(String value) {
346+
int limit = config.getDebug().resolveMaxContentLength();
347+
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
348+
}
349+
339350
/**
340351
* 取消全部订阅并释放 SSE 客户端自有资源。
341352
*/

src/main/java/io/github/easy4j/opencode/api/mapper/OpenCodeCallbackParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public Map<String, Object> parseFromText(String text) {
8787
try {
8888
return MAPPER.readValue(json, new TypeReference<Map<String, Object>>() {});
8989
} catch (Exception e) {
90-
log.debug("Failed to parse JSON from code block: {}", json, e);
90+
log.debug("Failed to parse JSON from code block: contentLength={}", json.length(), e);
9191
}
9292
}
9393

0 commit comments

Comments
 (0)