Skip to content

Commit c338c67

Browse files
committed
feat: add safe HTTP trace logging
1 parent 73ffa9b commit c338c67

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ public class OpenCodeHttpClientConfig {
113113
*/
114114
private boolean retryOnConnectionFailure = true;
115115

116+
/**
117+
* 是否输出请求头、请求体及响应体等详细诊断信息。
118+
* <p>默认关闭;基础请求生命周期仍使用 DEBUG 日志。</p>
119+
*/
120+
private boolean detailedLoggingEnabled = false;
121+
122+
/** 详细日志中请求体、响应体的最大字符数。 */
123+
private int maxLoggedBodyLength = 2_000;
124+
116125
/**
117126
* 是否校验 HTTPS 证书;为 false 时关闭校验(仅建议开发环境)。
118127
*/

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.Map;
2121
import java.util.Objects;
2222
import java.util.Optional;
23+
import java.util.concurrent.atomic.AtomicLong;
24+
import okio.Buffer;
2325

2426
/**
2527
* HTTP client for the OpenCode Server REST API.
@@ -39,6 +41,7 @@ public class OpenCodeHttpClient implements AutoCloseable {
3941

4042
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
4143
private static final String HEADER_OPENCODE_DIRECTORY = "X-OpenCode-Directory";
44+
private static final AtomicLong REQUEST_SEQUENCE = new AtomicLong();
4245

4346
private final OpenCodeHttpClientConfig config;
4447
private final OkHttpClient httpClient;
@@ -49,6 +52,11 @@ public OpenCodeHttpClient(OpenCodeHttpClientConfig config, ObjectMapper objectMa
4952
this.objectMapper = Objects.isNull(objectMapper) ? new ObjectMapper()
5053
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false): objectMapper;
5154
this.httpClient = Objects.isNull(httpClient) ? buildOkHttpClient(config) : httpClient;
55+
log.debug("OpenCode 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
private static OkHttpClient buildOkHttpClient(OpenCodeHttpClientConfig config) {
@@ -880,16 +888,23 @@ private <T> T execute(Request request, Class<T> type) {
880888
}
881889

882890
private <T> T execute(Request request, Class<T> type, HttpCallCancellation cancellation) {
891+
long requestId = beginTrace(request);
892+
long startedAt = System.nanoTime();
883893
Call call = httpClient.newCall(request);
884894
AutoCloseable registration = cancellation != null ? cancellation.onCancel(call::cancel) : null;
885895
try (Response response = call.execute()) {
886896
String respBody = response.body() != null ? response.body().string() : "";
897+
logResponse(requestId, request, response.code(), respBody, startedAt);
887898
if (!response.isSuccessful()) {
888899
throw new OpenCodeHttpException(response.code(), respBody);
889900
}
890901
return objectMapper.readValue(respBody, type);
891902
} catch (IOException e) {
903+
logFailure(requestId, request, startedAt, e);
892904
throw new OpenCodeHttpException("HTTP request failed: " + e.getMessage(), e);
905+
} catch (RuntimeException e) {
906+
logFailure(requestId, request, startedAt, e);
907+
throw e;
893908
} finally {
894909
closeRegistration(registration);
895910
}
@@ -912,21 +927,85 @@ private <T> T executeList(Request request, TypeReference<T> typeRef) {
912927

913928
private <T> T executeList(Request request, TypeReference<T> typeRef,
914929
HttpCallCancellation cancellation) {
930+
long requestId = beginTrace(request);
931+
long startedAt = System.nanoTime();
915932
Call call = httpClient.newCall(request);
916933
AutoCloseable registration = cancellation != null ? cancellation.onCancel(call::cancel) : null;
917934
try (Response response = call.execute()) {
918935
String respBody = response.body() != null ? response.body().string() : "";
936+
logResponse(requestId, request, response.code(), respBody, startedAt);
919937
if (!response.isSuccessful()) {
920938
throw new OpenCodeHttpException(response.code(), respBody);
921939
}
922940
return objectMapper.readValue(respBody, typeRef);
923941
} catch (IOException e) {
942+
logFailure(requestId, request, startedAt, e);
924943
throw new OpenCodeHttpException("HTTP request failed: " + e.getMessage(), e);
944+
} catch (RuntimeException e) {
945+
logFailure(requestId, request, startedAt, e);
946+
throw e;
925947
} finally {
926948
closeRegistration(registration);
927949
}
928950
}
929951

952+
private long beginTrace(Request request) {
953+
long requestId = REQUEST_SEQUENCE.incrementAndGet();
954+
log.debug("HTTP request started: requestId={}, method={}, url={}",
955+
requestId, request.method(), request.url());
956+
if (config.isDetailedLoggingEnabled()) {
957+
log.debug("HTTP request details: requestId={}, headers={}, body={}", requestId,
958+
redactHeaders(request.headers()), requestBody(request));
959+
}
960+
return requestId;
961+
}
962+
963+
private void logResponse(long requestId, Request request, int status, String body, long startedAt) {
964+
log.debug("HTTP request completed: requestId={}, method={}, url={}, status={}, bodyLength={}, elapsedMs={}",
965+
requestId, request.method(), request.url(), status, body.length(), elapsedMillis(startedAt));
966+
if (config.isDetailedLoggingEnabled()) {
967+
log.debug("HTTP response body: requestId={}, body={}", requestId, truncate(body));
968+
}
969+
}
970+
971+
private void logFailure(long requestId, Request request, long startedAt, Exception error) {
972+
log.warn("HTTP request failed: requestId={}, method={}, url={}, elapsedMs={}, error={}",
973+
requestId, request.method(), request.url(), elapsedMillis(startedAt), error.getMessage());
974+
}
975+
976+
private long elapsedMillis(long startedAt) {
977+
return (System.nanoTime() - startedAt) / 1_000_000L;
978+
}
979+
980+
private String requestBody(Request request) {
981+
if (Objects.isNull(request.body())) {
982+
return "";
983+
}
984+
try {
985+
Buffer buffer = new Buffer();
986+
request.body().writeTo(buffer);
987+
return truncate(buffer.readUtf8());
988+
} catch (IOException error) {
989+
return "<unavailable:" + error.getMessage() + ">";
990+
}
991+
}
992+
993+
private String truncate(String value) {
994+
int limit = Math.max(0, config.getMaxLoggedBodyLength());
995+
return value.length() <= limit ? value : value.substring(0, limit) + "...<truncated>";
996+
}
997+
998+
private Headers redactHeaders(Headers headers) {
999+
Headers.Builder safe = headers.newBuilder();
1000+
for (String name : headers.names()) {
1001+
String lowerName = name.toLowerCase();
1002+
if ("authorization".equals(lowerName) || lowerName.contains("token") || lowerName.contains("key")) {
1003+
safe.set(name, "██");
1004+
}
1005+
}
1006+
return safe.build();
1007+
}
1008+
9301009
private String toJson(Object body) {
9311010
try {
9321011
return objectMapper.writeValueAsString(body);

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public OpenCodeSseClient(OpenCodeHttpClientConfig config, ObjectMapper objectMap
4848
this.config = config;
4949
this.mapper = Objects.isNull(objectMapper) ? new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) : objectMapper;
5050
this.httpClient = Objects.isNull(httpClient) ? buildOkHttpClient(config) : httpClient;
51+
log.debug("OpenCode SSE client initialized: baseUrl={}, connectTimeoutMs={}, eventQueueCapacity={}, detailedLoggingEnabled={}",
52+
config.getBaseUrl(), config.getConnectTimeoutMillis(), config.getStreamEventQueueCapacity(),
53+
config.isDetailedLoggingEnabled());
5154
}
5255

5356
private static OkHttpClient buildOkHttpClient(OpenCodeHttpClientConfig config) {
@@ -74,10 +77,13 @@ public EventSource subscribe(Consumer<Event> consumer) {
7477

7578
public EventSource subscribe(Consumer<Event> consumer, OpenCodeRequestContext context) {
7679
Request request = buildRequest(context);
80+
long startedAt = System.nanoTime();
81+
log.debug("SSE subscription started: url={}", request.url());
7782
EventSourceListener listener = new EventSourceListener() {
7883
@Override
7984
public void onOpen(EventSource es, Response response) {
80-
log.info("SSE connected to {}/event", config.getBaseUrl());
85+
log.info("SSE connected: url={}, status={}, elapsedMs={}", request.url(), response.code(),
86+
(System.nanoTime() - startedAt) / 1_000_000L);
8187
}
8288

8389
@Override
@@ -87,7 +93,11 @@ public void onEvent(EventSource es, String id, String type, String data) {
8793
Event event = mapper.readValue(data, Event.class);
8894
consumer.accept(event);
8995
} catch (Exception e) {
90-
log.debug("Failed to parse SSE event: {}", data, e);
96+
if (config.isDetailedLoggingEnabled()) {
97+
log.debug("Failed to parse SSE event: {}", data, e);
98+
} else {
99+
log.debug("Failed to parse SSE event: dataLength={}, error={}", data.length(), e.getMessage());
100+
}
91101
}
92102
}
93103
}

src/test/java/io/github/easy4j/opencode/OpenCodeHttpClientConfigTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ void shouldHaveCorrectDefaults() {
3333
assertEquals(60_000L, config.getStreamKeepAliveMillis());
3434
assertEquals(1_024, config.getStreamEventQueueCapacity());
3535
assertTrue(config.isRetryOnConnectionFailure());
36+
assertFalse(config.isDetailedLoggingEnabled());
37+
assertEquals(2_000, config.getMaxLoggedBodyLength());
3638
assertTrue(config.isVerifySsl());
3739
assertNull(config.getDefaultModel());
3840
assertNull(config.getDefaultAgent());

0 commit comments

Comments
 (0)