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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy