Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5af07af

Browse files
authored
impl: improved logging and error collection for the http client (#165)
For some clients, workspace polling fails due to the following error:```com.squareup.moshi.JsonEncodingException: Use JsonReader.setLenient(true) to accept malformed JSON at path $```Although I’ve been unable to reproduce this issue — even using the exactversion deployed at the client (2.20.2) — I've introduced a loggingmechanism to improve diagnostics in such cases.This PR introduces a configurable HTTP logging interceptor. Users canchoose from various levels via the plugin UI:- None- Basic (method, URL, response code)- Headers (sanitized)- Body (full content)Importantly, the logging converter remains in place to capture criticalinformation during JSON deserialization failures, even when users havedisabled detailed logging (e.g., to avoid logging full bodies).To address the original error more effectively, I wrapped the Moshiconverter with a custom Converter that logs the raw response body,content type, and exception details when a deserialization failureoccurs. This helps debug malformed JSON responses, particularly duringworkspace polling.This implementation only logs when deserialization fails. In the successpath, the performance impact is minimal: the response body is convertedto a string for potential logging, then re-wrapped as a stream for theMoshi converter. Users can opt in to always provide extra loggingdetails but the constom converter ensures us that we have some minimumdetails regardless of user's choice.<img width="972" height="1492" alt="image"src="https://github.com/user-attachments/assets/f08551e5-2b47-4848-80c3-67f5e5437cd9"/>
1 parent0ad31dd commit5af07af

File tree

11 files changed

+352
-3
lines changed

11 files changed

+352
-3
lines changed

‎CHANGELOG.md‎

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

1111
- URL validation is stricter in the connection screen and URI protocol handler
12+
- support for verbose logging a sanitized version of the REST API request and responses
1213

1314
##0.6.0 - 2025-07-25
1415

‎README.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,64 @@ via Toolbox App Menu > About > Show log files.
257257
Alternatively, you can generate a ZIP file using the Workspace action menu, available either on the main
258258
Workspaces page in Coder or within the individual workspace view, under the option labeled_Collect logs_.
259259

260+
###HTTP Request Logging
261+
262+
The Coder Toolbox plugin includes comprehensive HTTP request logging capabilities to help diagnose API communication
263+
issues with Coder deployments.
264+
This feature allows you to monitor all HTTP requests and responses made by the plugin.
265+
266+
####Configuring HTTP Logging
267+
268+
You can configure HTTP logging verbosity through the Coder Settings page:
269+
270+
1. Navigate to the Coder Workspaces page
271+
2. Click on the deployment action menu (three dots)
272+
3. Select "Settings"
273+
4. Find the "HTTP logging level" dropdown
274+
275+
####Available Logging Levels
276+
277+
The plugin supports four levels of HTTP logging verbosity:
278+
279+
-**None**: No HTTP request/response logging (default)
280+
-**Basic**: Logs HTTP method, URL, and response status code
281+
-**Headers**: Logs basic information plus sanitized request and response headers
282+
-**Body**: Logs headers plus request and response body content
283+
284+
####Log Output Format
285+
286+
HTTP logs follow this format:
287+
288+
```
289+
request --> GET https://your-coder-deployment.com/api/v2/users/me
290+
User-Agent: Coder Toolbox/1.0.0 (darwin; amd64)
291+
Coder-Session-Token: <redacted>
292+
293+
response <-- 200 https://your-coder-deployment.com/api/v2/users/me
294+
Content-Type: application/json
295+
Content-Length: 245
296+
297+
{"id":"12345678-1234-1234-1234-123456789012","username":"coder","email":"coder@example.com"}
298+
```
299+
300+
####Use Cases
301+
302+
HTTP logging is particularly useful for:
303+
304+
-**API Debugging**: Diagnosing issues with Coder API communication
305+
-**Authentication Problems**: Troubleshooting token or certificate authentication issues
306+
-**Network Issues**: Identifying connectivity problems with Coder deployments
307+
-**Performance Analysis**: Monitoring request/response times and payload sizes
308+
309+
####Troubleshooting with HTTP Logs
310+
311+
When reporting issues, include HTTP logs to help diagnose:
312+
313+
1.**Authentication Failures**: Check for 401/403 responses and token headers
314+
2.**Network Connectivity**: Look for connection timeouts or DNS resolution issues
315+
3.**API Compatibility**: Verify request/response formats match expected API versions
316+
4.**Proxy Issues**: Monitor proxy authentication and routing problems
317+
260318
##Coder Settings
261319

262320
The Coder Settings allows users to control CLI download behavior, SSH configuration, TLS parameters, and data

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package com.coder.toolbox.sdk
33
importcom.coder.toolbox.CoderToolboxContext
44
importcom.coder.toolbox.sdk.convertors.ArchConverter
55
importcom.coder.toolbox.sdk.convertors.InstantConverter
6+
importcom.coder.toolbox.sdk.convertors.LoggingConverterFactory
67
importcom.coder.toolbox.sdk.convertors.OSConverter
78
importcom.coder.toolbox.sdk.convertors.UUIDConverter
89
importcom.coder.toolbox.sdk.ex.APIResponseException
10+
importcom.coder.toolbox.sdk.interceptors.LoggingInterceptor
911
importcom.coder.toolbox.sdk.v2.CoderV2RestFacade
1012
importcom.coder.toolbox.sdk.v2.models.ApiErrorResponse
1113
importcom.coder.toolbox.sdk.v2.models.BuildInfo
@@ -74,10 +76,10 @@ open class CoderRestClient(
7476
var builder=OkHttpClient.Builder()
7577

7678
if (context.proxySettings.getProxy()!=null) {
77-
context.logger.debug("proxy:${context.proxySettings.getProxy()}")
79+
context.logger.info("proxy:${context.proxySettings.getProxy()}")
7880
builder.proxy(context.proxySettings.getProxy())
7981
}elseif (context.proxySettings.getProxySelector()!=null) {
80-
context.logger.debug("proxy selector:${context.proxySettings.getProxySelector()}")
82+
context.logger.info("proxy selector:${context.proxySettings.getProxySelector()}")
8183
builder.proxySelector(context.proxySettings.getProxySelector()!!)
8284
}
8385

@@ -129,11 +131,17 @@ open class CoderRestClient(
129131
}
130132
it.proceed(request)
131133
}
134+
.addInterceptor(LoggingInterceptor(context))
132135
.build()
133136

134137
retroRestClient=
135138
Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
136-
.addConverterFactory(MoshiConverterFactory.create(moshi))
139+
.addConverterFactory(
140+
LoggingConverterFactory.wrap(
141+
context,
142+
MoshiConverterFactory.create(moshi)
143+
)
144+
)
137145
.build().create(CoderV2RestFacade::class.java)
138146
}
139147

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
packagecom.coder.toolbox.sdk.convertors
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importokhttp3.RequestBody
5+
importokhttp3.ResponseBody
6+
importretrofit2.Converter
7+
importretrofit2.Retrofit
8+
importjava.lang.reflect.Type
9+
10+
classLoggingConverterFactory private constructor(
11+
privatevalcontext:CoderToolboxContext,
12+
privatevaldelegate:Converter.Factory,
13+
) : Converter.Factory() {
14+
15+
overridefunresponseBodyConverter(
16+
type:Type,
17+
annotations:Array<Annotation>,
18+
retrofit:Retrofit
19+
):Converter<ResponseBody,*>? {
20+
// Get the delegate converter
21+
val delegateConverter= delegate.responseBodyConverter(type, annotations, retrofit)
22+
?:returnnull
23+
24+
@Suppress("UNCHECKED_CAST")
25+
returnLoggingMoshiConverter(context, delegateConverterasConverter<ResponseBody,Any?>)
26+
}
27+
28+
overridefunrequestBodyConverter(
29+
type:Type,
30+
parameterAnnotations:Array<Annotation>,
31+
methodAnnotations:Array<Annotation>,
32+
retrofit:Retrofit
33+
):Converter<*,RequestBody>? {
34+
return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
35+
}
36+
37+
overridefunstringConverter(
38+
type:Type,
39+
annotations:Array<Annotation>,
40+
retrofit:Retrofit
41+
):Converter<*,String>? {
42+
return delegate.stringConverter(type, annotations, retrofit)
43+
}
44+
45+
companionobject {
46+
funwrap(
47+
context:CoderToolboxContext,
48+
delegate:Converter.Factory,
49+
):LoggingConverterFactory {
50+
returnLoggingConverterFactory(context, delegate)
51+
}
52+
}
53+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
packagecom.coder.toolbox.sdk.convertors
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importokhttp3.ResponseBody
5+
importokhttp3.ResponseBody.Companion.toResponseBody
6+
importretrofit2.Converter
7+
8+
classLoggingMoshiConverter(
9+
privatevalcontext:CoderToolboxContext,
10+
privatevaldelegate:Converter<ResponseBody,Any?>
11+
) : Converter<ResponseBody, Any> {
12+
13+
overridefunconvert(value:ResponseBody):Any? {
14+
val bodyString= value.string()
15+
16+
returntry {
17+
// Parse with Moshi
18+
delegate.convert(bodyString.toResponseBody(value.contentType()))
19+
}catch (e:Exception) {
20+
// Log the raw content that failed to parse
21+
context.logger.error(
22+
"""
23+
|Moshi parsing failed:
24+
|Content-Type:${value.contentType()}
25+
|Content:$bodyString
26+
|Error:${e.message}
27+
""".trimMargin()
28+
)
29+
30+
// Re-throw so the onFailure callback still gets called
31+
throw e
32+
}
33+
}
34+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
packagecom.coder.toolbox.sdk.interceptors
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importcom.coder.toolbox.settings.HttpLoggingVerbosity
5+
importokhttp3.Headers
6+
importokhttp3.Interceptor
7+
importokhttp3.MediaType
8+
importokhttp3.Request
9+
importokhttp3.RequestBody
10+
importokhttp3.Response
11+
importokhttp3.ResponseBody
12+
importokio.Buffer
13+
importjava.nio.charset.StandardCharsets
14+
15+
privatevalSENSITIVE_HEADERS=setOf("Coder-Session-Token","Proxy-Authorization")
16+
17+
classLoggingInterceptor(privatevalcontext:CoderToolboxContext) : Interceptor {
18+
19+
overridefunintercept(chain:Interceptor.Chain):Response {
20+
val logLevel= context.settingsStore.httpClientLogLevel
21+
if (logLevel==HttpLoggingVerbosity.NONE) {
22+
return chain.proceed(chain.request())
23+
}
24+
25+
val request= chain.request()
26+
logRequest(request, logLevel)
27+
28+
val response= chain.proceed(request)
29+
logResponse(response, request, logLevel)
30+
31+
return response
32+
}
33+
34+
privatefunlogRequest(request:Request,logLevel:HttpLoggingVerbosity) {
35+
val log= buildString {
36+
append("request -->${request.method}${request.url}")
37+
38+
if (logLevel>=HttpLoggingVerbosity.HEADERS) {
39+
append("\n${request.headers.sanitized()}")
40+
}
41+
42+
if (logLevel==HttpLoggingVerbosity.BODY) {
43+
request.body?.let { body->
44+
append("\n${body.toPrintableString()}")
45+
}
46+
}
47+
}
48+
49+
context.logger.info(log)
50+
}
51+
52+
privatefunlogResponse(response:Response,request:Request,logLevel:HttpLoggingVerbosity) {
53+
val log= buildString {
54+
append("response <--${response.code}${response.message}${request.url}")
55+
56+
if (logLevel>=HttpLoggingVerbosity.HEADERS) {
57+
append("\n${response.headers.sanitized()}")
58+
}
59+
60+
if (logLevel==HttpLoggingVerbosity.BODY) {
61+
response.body?.let { body->
62+
append("\n${body.toPrintableString()}")
63+
}
64+
}
65+
}
66+
67+
context.logger.info(log)
68+
}
69+
}
70+
71+
// Extension functions for cleaner code
72+
privatefun Headers.sanitized():String= buildString {
73+
this@sanitized.forEach { (name, value)->
74+
val displayValue=if (nameinSENSITIVE_HEADERS)"<redacted>"else value
75+
append("$name:$displayValue\n")
76+
}
77+
}
78+
79+
privatefun RequestBody.toPrintableString():String {
80+
if (!contentType().isPrintable()) {
81+
return"[Binary body:${contentLength().formatBytes()},${contentType()}]"
82+
}
83+
84+
returntry {
85+
val buffer=Buffer()
86+
writeTo(buffer)
87+
buffer.readString(contentType()?.charset()?:StandardCharsets.UTF_8)
88+
}catch (e:Exception) {
89+
"[Error reading body:${e.message}]"
90+
}
91+
}
92+
93+
privatefun ResponseBody.toPrintableString():String {
94+
if (!contentType().isPrintable()) {
95+
return"[Binary body:${contentLength().formatBytes()},${contentType()}]"
96+
}
97+
98+
returntry {
99+
val source= source()
100+
source.request(Long.MAX_VALUE)
101+
source.buffer.clone().readString(contentType()?.charset()?:StandardCharsets.UTF_8)
102+
}catch (e:Exception) {
103+
"[Error reading body:${e.message}]"
104+
}
105+
}
106+
107+
privatefun MediaType?.isPrintable():Boolean=when {
108+
this==null->false
109+
type=="text"->true
110+
subtype=="json"|| subtype.endsWith("+json")->true
111+
else->false
112+
}
113+
114+
privatefun Long.formatBytes():String=when {
115+
this<0->"unknown"
116+
this<1024->"${this}B"
117+
this<1024*1024->"${this/1024}KB"
118+
this<1024*1024*1024->"${this/ (1024*1024)}MB"
119+
else->"${this/ (1024*1024*1024)}GB"
120+
}

‎src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ interface ReadOnlyCoderSettings {
3838
*/
3939
val fallbackOnCoderForSignatures:SignatureFallbackStrategy
4040

41+
/**
42+
* Controls the logging for the rest client.
43+
*/
44+
val httpClientLogLevel:HttpLoggingVerbosity
45+
4146
/**
4247
* Default CLI binary name based on OS and architecture
4348
*/
@@ -216,4 +221,32 @@ enum class SignatureFallbackStrategy {
216221
else->NOT_CONFIGURED
217222
}
218223
}
224+
}
225+
226+
enumclassHttpLoggingVerbosity {
227+
NONE,
228+
229+
/**
230+
* Logs URL, method, and status
231+
*/
232+
BASIC,
233+
234+
/**
235+
* Logs BASIC + sanitized headers
236+
*/
237+
HEADERS,
238+
239+
/**
240+
* Logs HEADERS + body content
241+
*/
242+
BODY;
243+
244+
companionobject {
245+
funfromValue(value:String?):HttpLoggingVerbosity=when (value?.lowercase(getDefault())) {
246+
"basic"->BASIC
247+
"headers"->HEADERS
248+
"body"->BODY
249+
else->NONE
250+
}
251+
}
219252
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp