2020import java .util .Map ;
2121import java .util .Objects ;
2222import 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 );
0 commit comments