Skip to content

Commit 4cab216

Browse files
committed
impl: log the rest api client request and response
A new interceptor is now available in the rest client that is able to log different level of details regarding the request/response: - if None is configured by user we skip logging - Basic level prints the method + url + response code - Headers prints in addition the request and response headers sanitized first - Body also prints the request/response body
1 parent f8606a0 commit 4cab216

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
- URL validation is stricter in the connection screen and URI protocol handler
1212
- the http client has relaxed syntax rules when deserializing JSON responses
13+
- support for verbose logging a sanitized version of the REST API request and responses
1314

1415
## 0.6.0 - 2025-07-25
1516

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
77
import com.coder.toolbox.sdk.convertors.OSConverter
88
import com.coder.toolbox.sdk.convertors.UUIDConverter
99
import com.coder.toolbox.sdk.ex.APIResponseException
10+
import com.coder.toolbox.sdk.interceptors.LoggingInterceptor
1011
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
1112
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
1213
import com.coder.toolbox.sdk.v2.models.BuildInfo
@@ -130,6 +131,7 @@ open class CoderRestClient(
130131
}
131132
it.proceed(request)
132133
}
134+
.addInterceptor(LoggingInterceptor(context))
133135
.build()
134136

135137
retroRestClient =
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.coder.toolbox.sdk.interceptors
2+
3+
import com.coder.toolbox.CoderToolboxContext
4+
import com.coder.toolbox.settings.HttpLoggingVerbosity
5+
import okhttp3.Headers
6+
import okhttp3.Interceptor
7+
import okhttp3.MediaType
8+
import okhttp3.RequestBody
9+
import okhttp3.Response
10+
import okhttp3.ResponseBody
11+
import okio.Buffer
12+
import java.nio.charset.StandardCharsets
13+
14+
class LoggingInterceptor(private val context: CoderToolboxContext) : Interceptor {
15+
override fun intercept(chain: Interceptor.Chain): Response {
16+
val logLevel = context.settingsStore.httpClientLogLevel
17+
if (logLevel == HttpLoggingVerbosity.NONE) {
18+
return chain.proceed(chain.request())
19+
}
20+
val request = chain.request()
21+
val requestLog = StringBuilder()
22+
requestLog.append("request --> ${request.method} ${request.url}\n")
23+
if (logLevel == HttpLoggingVerbosity.HEADERS) {
24+
requestLog.append(request.headers.toSanitizedString())
25+
}
26+
if (logLevel == HttpLoggingVerbosity.BODY) {
27+
request.body.toPrintableString()?.let {
28+
requestLog.append(it)
29+
}
30+
}
31+
context.logger.info(requestLog.toString())
32+
33+
val response = chain.proceed(request)
34+
val responseLog = StringBuilder()
35+
responseLog.append("response <-- ${response.code} ${response.message} ${request.url}\n")
36+
if (logLevel == HttpLoggingVerbosity.HEADERS) {
37+
responseLog.append(response.headers.toSanitizedString())
38+
}
39+
if (logLevel == HttpLoggingVerbosity.BODY) {
40+
response.body.toPrintableString()?.let {
41+
responseLog.append(it)
42+
}
43+
}
44+
45+
context.logger.info(responseLog.toString())
46+
return response
47+
}
48+
49+
private fun Headers.toSanitizedString(): String {
50+
val result = StringBuilder()
51+
this.forEach {
52+
if (it.first == "Coder-Session-Token" || it.first == "Proxy-Authorization") {
53+
result.append("${it.first}: <redacted>\n")
54+
} else {
55+
result.append("${it.first}: ${it.second}\n")
56+
}
57+
}
58+
return result.toString()
59+
}
60+
61+
/**
62+
* Converts a RequestBody to a printable string representation.
63+
* Handles different content types appropriately.
64+
*
65+
* @return String representation of the body, or metadata if not readable
66+
*/
67+
fun RequestBody?.toPrintableString(): String? {
68+
if (this == null) {
69+
return null
70+
}
71+
72+
if (!contentType().isPrintable()) {
73+
return "[Binary request body: ${contentLength().formatBytes()}, Content-Type: ${contentType()}]\n"
74+
}
75+
76+
return try {
77+
val buffer = Buffer()
78+
writeTo(buffer)
79+
80+
val charset = contentType()?.charset() ?: StandardCharsets.UTF_8
81+
buffer.readString(charset)
82+
} catch (e: Exception) {
83+
"[Error reading request body: ${e.message}]\n"
84+
}
85+
}
86+
87+
/**
88+
* Converts a ResponseBody to a printable string representation.
89+
* Handles different content types appropriately.
90+
*
91+
* @return String representation of the body, or metadata if not readable
92+
*/
93+
fun ResponseBody?.toPrintableString(): String? {
94+
if (this == null) {
95+
return null
96+
}
97+
98+
if (!contentType().isPrintable()) {
99+
return "[Binary response body: ${contentLength().formatBytes()}, Content-Type: ${contentType()}]\n"
100+
}
101+
102+
return try {
103+
val source = source()
104+
source.request(Long.MAX_VALUE)
105+
val charset = contentType()?.charset() ?: StandardCharsets.UTF_8
106+
source.buffer.clone().readString(charset)
107+
} catch (e: Exception) {
108+
"[Error reading response body: ${e.message}]\n"
109+
}
110+
}
111+
112+
/**
113+
* Checks if a MediaType represents printable/readable content
114+
*/
115+
private fun MediaType?.isPrintable(): Boolean {
116+
if (this == null) return false
117+
118+
return when {
119+
// Text types
120+
type == "text" -> true
121+
122+
// JSON variants
123+
subtype == "json" -> true
124+
subtype.endsWith("+json") -> true
125+
126+
// Default to non-printable for safety
127+
else -> false
128+
}
129+
}
130+
131+
/**
132+
* Formats byte count in human-readable format
133+
*/
134+
private fun Long.formatBytes(): String {
135+
return when {
136+
this < 0 -> "unknown size"
137+
this < 1024 -> "${this}B"
138+
this < 1024 * 1024 -> "${this / 1024}KB"
139+
this < 1024 * 1024 * 1024 -> "${this / (1024 * 1024)}MB"
140+
else -> "${this / (1024 * 1024 * 1024)}GB"
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)