Skip to content

Commit 590e52f

Browse files
committed
feat: unify tiered diagnostic logging
1 parent 61cdd59 commit 590e52f

15 files changed

Lines changed: 183 additions & 55 deletions

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@
471471
<junit.version>5.11.4</junit.version>
472472
<lombok.version>1.18.46</lombok.version>
473473
<okhttp3.version>4.12.0</okhttp3.version>
474+
<okhttp3-extension.version>1.0.x.20260630-SNAPSHOT</okhttp3-extension.version>
474475
<slf4j.version>2.0.18</slf4j.version>
475476
<!-- Maven Plugin versions -->
476477
<maven-central-publishing-plugin.version>0.11.0</maven-central-publishing-plugin.version>
@@ -493,6 +494,11 @@
493494
<!-- 依赖版本统一管理(dependencyManagement) -->
494495
<dependencyManagement>
495496
<dependencies>
497+
<dependency>
498+
<groupId>io.github.easy4j</groupId>
499+
<artifactId>okhttp3-extension</artifactId>
500+
<version>${okhttp3-extension.version}</version>
501+
</dependency>
496502
<!-- For OkHttp -->
497503
<dependency>
498504
<groupId>com.squareup.okhttp3</groupId>
@@ -553,6 +559,10 @@
553559

554560
<!-- 项目依赖(dependencies) -->
555561
<dependencies>
562+
<dependency>
563+
<groupId>io.github.easy4j</groupId>
564+
<artifactId>okhttp3-extension</artifactId>
565+
</dependency>
556566
<!-- For OkHttp -->
557567
<dependency>
558568
<groupId>com.squareup.okhttp3</groupId>

src/main/java/io/github/easy4j/openclaw/OpenClawCliConfig.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
* 本地 CLI 通道配置,定义可执行文件、工作目录、启动探测、命令超时和最大并发子进程数。
79
*
@@ -11,6 +13,23 @@
1113
@Data
1214
public class OpenClawCliConfig {
1315

16+
/** CLI 与 HTTP/SSE 通道共享的调试策略。 */
17+
private final OpenClawDebugConfig debug;
18+
19+
/** 使用独立的默认调试策略创建配置。 */
20+
public OpenClawCliConfig() {
21+
this(new OpenClawDebugConfig());
22+
}
23+
24+
/**
25+
* 使用指定调试策略创建配置。
26+
*
27+
* @param debug 客户端共享调试策略
28+
*/
29+
public OpenClawCliConfig(OpenClawDebugConfig debug) {
30+
this.debug = Objects.requireNonNull(debug, "debug");
31+
}
32+
1433
/**
1534
* 是否创建并开放对应通信通道;关闭后门面不会初始化该子系统。
1635
*/

src/main/java/io/github/easy4j/openclaw/OpenClawClientConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
@Data
1212
public class OpenClawClientConfig {
1313

14+
/** 客户端所有通信通道共享的调试配置。 */
15+
private final OpenClawDebugConfig debug = new OpenClawDebugConfig();
16+
1417
/**
1518
* HTTP、SSE 和 WebSocket 通道共享的配置对象。
1619
*/
17-
private final OpenClawHttpClientConfig http = new OpenClawHttpClientConfig();
20+
private final OpenClawHttpClientConfig http = new OpenClawHttpClientConfig(debug);
1821

1922
/**
2023
* 本地 CLI 通道配置对象。
2124
*/
22-
private final OpenClawCliConfig cli = new OpenClawCliConfig();
25+
private final OpenClawCliConfig cli = new OpenClawCliConfig(debug);
2326

2427
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.easy4j.openclaw;
2+
3+
import lombok.Data;
4+
import okhttp3.extension.logging.HttpLogLevel;
5+
6+
/**
7+
* OpenClaw 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 OpenClawDebugConfig {
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/openclaw/OpenClawHttpClientConfig.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import io.github.easy4j.openclaw.util.OpenClawStrings;
44
import lombok.Data;
55

6+
import java.util.Objects;
7+
68
/**
79
* HTTP、SSE 与 WebSocket 通道配置,定义 Gateway 地址、认证优先级、连接池、并发、超时、重试和日志开关。
810
*
@@ -12,6 +14,25 @@
1214
@Data
1315
public class OpenClawHttpClientConfig {
1416

17+
/** HTTP、SSE 与 WebSocket 通道共享的调试配置。 */
18+
private final OpenClawDebugConfig debug;
19+
20+
/**
21+
* 使用默认关闭的调试配置创建 HTTP 配置。
22+
*/
23+
public OpenClawHttpClientConfig() {
24+
this(new OpenClawDebugConfig());
25+
}
26+
27+
/**
28+
* 使用客户端级共享调试配置创建 HTTP 配置。
29+
*
30+
* @param debug 客户端级调试配置
31+
*/
32+
public OpenClawHttpClientConfig(OpenClawDebugConfig debug) {
33+
this.debug = Objects.requireNonNull(debug, "debug");
34+
}
35+
1536
/**
1637
* 同步、流式或自动选择的 HTTP 响应消费模式。
1738
*/
@@ -117,16 +138,6 @@ public class OpenClawHttpClientConfig {
117138
*/
118139
private boolean retryOnConnectionFailure = true;
119140

120-
/**
121-
* 是否输出脱敏后的请求头和截断响应体;默认关闭以控制日志量和泄露风险。
122-
*/
123-
private boolean detailedLoggingEnabled = false;
124-
125-
/**
126-
* 详细日志中响应体允许记录的最大字符数,超出部分截断。
127-
*/
128-
private int maxLoggedBodyLength = 2_000;
129-
130141
/**
131142
* Webhook 端点的基础路径,具体 Hook 名称在其后追加。
132143
*/

src/main/java/io/github/easy4j/openclaw/api/OpenClawChatClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.github.easy4j.openclaw.api.sse.StreamingChatResponse;
1010
import lombok.extern.slf4j.Slf4j;
1111
import okhttp3.OkHttpClient;
12+
import okhttp3.extension.logging.HttpLogLevel;
1213

1314
import java.net.URLEncoder;
1415
import java.util.HashMap;
@@ -137,7 +138,7 @@ public CompletableFuture<ChatResponse> chatCompletionAsync(ChatRequest request,
137138
String bodyModel = resolveModel(request);
138139

139140
debug("Resolved body model (agent routing): {}", bodyModel);
140-
debug("Headers to send: {}", headers);
141+
debug(HttpLogLevel.HEADERS, "Headers to send: {}", headers);
141142

142143
ChatRequest normalized = ChatRequest.builder()
143144
.model(bodyModel)

src/main/java/io/github/easy4j/openclaw/api/OpenClawEmbeddingsClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.github.easy4j.openclaw.api.model.EmbeddingsResponse;
88
import lombok.extern.slf4j.Slf4j;
99
import okhttp3.OkHttpClient;
10+
import okhttp3.extension.logging.HttpLogLevel;
1011

1112
import java.util.HashMap;
1213
import java.util.Map;
@@ -61,7 +62,7 @@ public CompletableFuture<EmbeddingsResponse> createEmbeddingsAsync(EmbeddingsReq
6162
debug("=== Embeddings Request ===");
6263
debug("agent: {}", request.getAgent());
6364
debug("model: {}", request.getModel());
64-
debug("input: {}", request.getInput());
65+
debug(HttpLogLevel.BODY, "input: {}", request.getInput());
6566

6667
// 验证请求
6768
validateRequest(request);

src/main/java/io/github/easy4j/openclaw/api/OpenClawHttpClient.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import lombok.Getter;
1111
import lombok.extern.slf4j.Slf4j;
1212
import okhttp3.*;
13+
import okhttp3.extension.logging.HttpLogLevel;
1314

1415
import java.io.IOException;
1516
import java.util.Map;
@@ -82,11 +83,11 @@ private OpenClawHttpClient(OpenClawHttpClientConfig config, ObjectMapper objectM
8283
this.objectMapper = objectMapper != null ? objectMapper : createObjectMapper();
8384
this.httpClient = Objects.requireNonNull(httpClient, "httpClient");
8485
this.ownsHttpClient = ownsHttpClient;
85-
debug("OpenClaw HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
86-
+ "callTimeoutMs={}, retryOnConnectionFailure={}, detailedLoggingEnabled={}",
86+
debug(HttpLogLevel.BASIC, "OpenClaw HTTP client initialized: baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}, "
87+
+ "callTimeoutMs={}, retryOnConnectionFailure={}, debugLevel={}",
8788
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getReadTimeoutMillis(),
8889
config.getCallTimeoutMillis(), config.isRetryOnConnectionFailure(),
89-
config.isDetailedLoggingEnabled());
90+
config.getDebug().getLevel());
9091
}
9192

9293
/**
@@ -138,9 +139,7 @@ protected Request.Builder authedBuilder(String url, Map<String, String> headers)
138139
headers.forEach((k, v) -> {
139140
if (k != null && v != null) {
140141
builder.header(k, v);
141-
if (config.isDetailedLoggingEnabled()) {
142-
debug("Added header: {}={}", k, redactHeader(k, v));
143-
}
142+
debug(HttpLogLevel.HEADERS, "Added header: {}={}", k, redactHeader(k, v));
144143
}
145144
});
146145
}
@@ -201,9 +200,7 @@ protected CompletableFuture<String> postJsonAsync(String path, Object body, Map<
201200

202201
try {
203202
String json = objectMapper.writeValueAsString(body);
204-
if (config.isDetailedLoggingEnabled()) {
205-
debug("Request body: {}", truncate(json));
206-
}
203+
debug(HttpLogLevel.BODY, "Request body: {}", truncate(json));
207204

208205
Request request = authedBuilder(url, headers)
209206
.post(RequestBody.create(json, JSON))
@@ -284,10 +281,10 @@ protected CompletableFuture<String> executeAsync(Request request, String url,
284281
HttpCallCancellation cancellation) {
285282
long requestId = REQUEST_SEQUENCE.incrementAndGet();
286283
long startedAt = System.nanoTime();
287-
debug("HTTP request started: requestId={}, method={}, url={}", requestId, request.method(), request.url());
288-
if (config.isDetailedLoggingEnabled()) {
289-
debug("HTTP request details: requestId={}, headers={}", requestId, redactHeaders(request.headers()));
290-
}
284+
debug(HttpLogLevel.BASIC, "HTTP request started: requestId={}, method={}, url={}",
285+
requestId, request.method(), request.url());
286+
debug(HttpLogLevel.HEADERS, "HTTP request headers: requestId={}, headers={}",
287+
requestId, redactHeaders(request.headers()));
291288

292289
// 传输层只读取状态码和响应体;此处统一把非 2xx 响应转换为携带诊断信息的 SDK 异常。
293290
CompletableFuture<String> result = executeResponseAsync(request, cancellation).thenApply(response -> {
@@ -303,11 +300,9 @@ protected CompletableFuture<String> executeAsync(Request request, String url,
303300
requestId, request.method(), request.url(), elapsedMillis(startedAt), unwrap(error).getMessage());
304301
return;
305302
}
306-
debug("HTTP request completed: requestId={}, method={}, url={}, bodyLength={}, elapsedMs={}",
303+
debug(HttpLogLevel.BASIC, "HTTP request completed: requestId={}, method={}, url={}, bodyLength={}, elapsedMs={}",
307304
requestId, request.method(), request.url(), respBody.length(), elapsedMillis(startedAt));
308-
if (config.isDetailedLoggingEnabled()) {
309-
debug("HTTP response body: requestId={}, body={}", requestId, truncate(respBody));
310-
}
305+
debug(HttpLogLevel.BODY, "HTTP response body: requestId={}, body={}", requestId, truncate(respBody));
311306
});
312307
}
313308

@@ -450,11 +445,11 @@ private long elapsedMillis(long startedAt) {
450445
return (System.nanoTime() - startedAt) / 1_000_000L;
451446
}
452447

453-
private String truncate(String value) {
448+
protected String truncate(String value) {
454449
if (Objects.isNull(value)) {
455450
return "";
456451
}
457-
int limit = Math.max(0, config.getMaxLoggedBodyLength());
452+
int limit = config.getDebug().resolveMaxContentLength();
458453
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
459454
}
460455

@@ -469,7 +464,7 @@ private Headers redactHeaders(Headers headers) {
469464
private String redactHeader(String name, String value) {
470465
if ("authorization".equalsIgnoreCase(name) || name.toLowerCase().contains("token")
471466
|| name.toLowerCase().contains("key")) {
472-
return "██";
467+
return "<redacted>";
473468
}
474469
return value;
475470
}
@@ -567,7 +562,18 @@ public CompletableFuture<Void> healthAsync() {
567562
* @param args 填充消息模板占位符的参数
568563
*/
569564
protected void debug(String msg, Object... args) {
570-
if (log.isDebugEnabled()) {
565+
debug(HttpLogLevel.BASIC, msg, args);
566+
}
567+
568+
/**
569+
* 在统一调试配置允许指定级别且 SLF4J 开启 DEBUG 时记录日志。
570+
*
571+
* @param level 信息要求的最低 HTTP 日志级别
572+
* @param msg 包含 SLF4J 占位符的日志消息模板
573+
* @param args 填充消息模板占位符的参数
574+
*/
575+
protected void debug(HttpLogLevel level, String msg, Object... args) {
576+
if (config.getDebug().allows(level) && log.isDebugEnabled()) {
571577
log.debug(msg, args);
572578
}
573579
}

src/main/java/io/github/easy4j/openclaw/api/OpenClawSseClient.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import okhttp3.Request;
1616
import okhttp3.RequestBody;
1717
import okhttp3.Response;
18+
import okhttp3.extension.logging.HttpLogLevel;
1819

1920
import java.io.IOException;
2021
import java.util.HashMap;
@@ -188,7 +189,7 @@ private void consume(Response response, SseSubscription subscription, SseEventHa
188189
handler.onError(new OpenClawHttpException("SSE response body is null", null));
189190
return;
190191
}
191-
new SseStreamReader(objectMapper)
192+
new SseStreamReader(objectMapper, config.getDebug())
192193
.readChatCompletionStream(completed.body().byteStream(), handler);
193194
} catch (Exception error) {
194195
if (subscription.isActive()) {
@@ -268,11 +269,12 @@ private static ExecutorService createStreamExecutor(OpenClawHttpClientConfig con
268269
}
269270

270271
private void logInitialization(OpenClawHttpClientConfig config) {
271-
log.debug("OpenClaw SSE client initialized: baseUrl={}, maxRequests={}, "
272-
+ "maxRequestsPerHost={}, eventQueueCapacity={}, reconnectPolicy=none, "
273-
+ "detailedLoggingEnabled={}",
274-
config.getBaseUrl(), config.getMaxRequests(), config.getMaxRequestsPerHost(),
275-
config.getStreamQueueCapacity(), config.isDetailedLoggingEnabled());
272+
if (config.getDebug().allows(HttpLogLevel.BASIC)) {
273+
log.debug("OpenClaw SSE client initialized: baseUrl={}, maxRequests={}, "
274+
+ "maxRequestsPerHost={}, eventQueueCapacity={}, reconnectPolicy=none, debugLevel={}",
275+
config.getBaseUrl(), config.getMaxRequests(), config.getMaxRequestsPerHost(),
276+
config.getStreamQueueCapacity(), config.getDebug().getLevel());
277+
}
276278
}
277279

278280
/**

src/main/java/io/github/easy4j/openclaw/api/OpenClawToolInvokeClient.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.github.easy4j.openclaw.api.model.ToolInvokeResult;
1010
import lombok.extern.slf4j.Slf4j;
1111
import okhttp3.*;
12+
import okhttp3.extension.logging.HttpLogLevel;
1213

1314
import java.util.Objects;
1415
import java.util.concurrent.CompletableFuture;
@@ -79,7 +80,7 @@ public CompletableFuture<ToolInvokeResult> invokeAsync(ToolInvokeRequest request
7980
debug("=== Tool Invoke Request ===");
8081
debug("tool: {}", request.getTool());
8182
debug("action: {}", request.getAction());
82-
debug("args: {}", request.getArgs());
83+
debug(HttpLogLevel.BODY, "args: {}", request.getArgs());
8384

8485
if (OpenClawStrings.isBlank(request.getTool())) {
8586
String msg = "Tool name is required";
@@ -96,7 +97,7 @@ public CompletableFuture<ToolInvokeResult> invokeAsync(ToolInvokeRequest request
9697
int status = response.getStatusCode();
9798
String respBody = response.getBody();
9899
debug("Tool invoke response status: {}", status);
99-
debug("Tool invoke response body: {}", respBody);
100+
debug(HttpLogLevel.BODY, "Tool invoke response body: {}", truncate(respBody));
100101

101102
if (status == 404) {
102103
String msg = "Tool not available: " + request.getTool();

0 commit comments

Comments
 (0)