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

impl: improved logging and error collection for the http client#165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
fioan89 merged 9 commits intomainfromimpl-lenient-http-client
Aug 6, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
9 commits
Select commitHold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionsCHANGELOG.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
### Changed

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

## 0.6.0 - 2025-07-25

Expand Down
58 changes: 58 additions & 0 deletionsREADME.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -257,6 +257,64 @@ via Toolbox App Menu > About > Show log files.
Alternatively, you can generate a ZIP file using the Workspace action menu, available either on the main
Workspaces page in Coder or within the individual workspace view, under the option labeled _Collect logs_.

### HTTP Request Logging

The Coder Toolbox plugin includes comprehensive HTTP request logging capabilities to help diagnose API communication
issues with Coder deployments.
This feature allows you to monitor all HTTP requests and responses made by the plugin.

#### Configuring HTTP Logging

You can configure HTTP logging verbosity through the Coder Settings page:

1. Navigate to the Coder Workspaces page
2. Click on the deployment action menu (three dots)
3. Select "Settings"
4. Find the "HTTP logging level" dropdown

#### Available Logging Levels

The plugin supports four levels of HTTP logging verbosity:

- **None**: No HTTP request/response logging (default)
- **Basic**: Logs HTTP method, URL, and response status code
- **Headers**: Logs basic information plus sanitized request and response headers
- **Body**: Logs headers plus request and response body content

#### Log Output Format

HTTP logs follow this format:

```
request --> GET https://your-coder-deployment.com/api/v2/users/me
User-Agent: Coder Toolbox/1.0.0 (darwin; amd64)
Coder-Session-Token: <redacted>

response <-- 200 https://your-coder-deployment.com/api/v2/users/me
Content-Type: application/json
Content-Length: 245

{"id":"12345678-1234-1234-1234-123456789012","username":"coder","email":"coder@example.com"}
```

#### Use Cases

HTTP logging is particularly useful for:

- **API Debugging**: Diagnosing issues with Coder API communication
- **Authentication Problems**: Troubleshooting token or certificate authentication issues
- **Network Issues**: Identifying connectivity problems with Coder deployments
- **Performance Analysis**: Monitoring request/response times and payload sizes

#### Troubleshooting with HTTP Logs

When reporting issues, include HTTP logs to help diagnose:

1. **Authentication Failures**: Check for 401/403 responses and token headers
2. **Network Connectivity**: Look for connection timeouts or DNS resolution issues
3. **API Compatibility**: Verify request/response formats match expected API versions
4. **Proxy Issues**: Monitor proxy authentication and routing problems

## Coder Settings

The Coder Settings allows users to control CLI download behavior, SSH configuration, TLS parameters, and data
Expand Down
14 changes: 11 additions & 3 deletionssrc/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,11 @@ package com.coder.toolbox.sdk
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.sdk.convertors.ArchConverter
import com.coder.toolbox.sdk.convertors.InstantConverter
import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
import com.coder.toolbox.sdk.convertors.OSConverter
import com.coder.toolbox.sdk.convertors.UUIDConverter
import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.sdk.interceptors.LoggingInterceptor
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
import com.coder.toolbox.sdk.v2.models.BuildInfo
Expand DownExpand Up@@ -74,10 +76,10 @@ open class CoderRestClient(
var builder = OkHttpClient.Builder()

if (context.proxySettings.getProxy() != null) {
context.logger.debug("proxy: ${context.proxySettings.getProxy()}")
context.logger.info("proxy: ${context.proxySettings.getProxy()}")
builder.proxy(context.proxySettings.getProxy())
} else if (context.proxySettings.getProxySelector() != null) {
context.logger.debug("proxy selector: ${context.proxySettings.getProxySelector()}")
context.logger.info("proxy selector: ${context.proxySettings.getProxySelector()}")
builder.proxySelector(context.proxySettings.getProxySelector()!!)
}

Expand DownExpand Up@@ -129,11 +131,17 @@ open class CoderRestClient(
}
it.proceed(request)
}
.addInterceptor(LoggingInterceptor(context))
.build()

retroRestClient =
Retrofit.Builder().baseUrl(url.toString()).client(httpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addConverterFactory(
LoggingConverterFactory.wrap(
context,
MoshiConverterFactory.create(moshi)
)
)
.build().create(CoderV2RestFacade::class.java)
}

Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
package com.coder.toolbox.sdk.convertors

import com.coder.toolbox.CoderToolboxContext
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

class LoggingConverterFactory private constructor(
private val context: CoderToolboxContext,
private val delegate: Converter.Factory,
) : Converter.Factory() {

override fun responseBodyConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *>? {
// Get the delegate converter
val delegateConverter = delegate.responseBodyConverter(type, annotations, retrofit)
?: return null

@Suppress("UNCHECKED_CAST")
return LoggingMoshiConverter(context, delegateConverter as Converter<ResponseBody, Any?>)
}

override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<Annotation>,
methodAnnotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody>? {
return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
}

override fun stringConverter(
type: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, String>? {
return delegate.stringConverter(type, annotations, retrofit)
}

companion object {
fun wrap(
context: CoderToolboxContext,
delegate: Converter.Factory,
): LoggingConverterFactory {
return LoggingConverterFactory(context, delegate)
}
}
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
package com.coder.toolbox.sdk.convertors

import com.coder.toolbox.CoderToolboxContext
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import retrofit2.Converter

class LoggingMoshiConverter(
private val context: CoderToolboxContext,
private val delegate: Converter<ResponseBody, Any?>
) : Converter<ResponseBody, Any> {

override fun convert(value: ResponseBody): Any? {
val bodyString = value.string()

return try {
// Parse with Moshi
delegate.convert(bodyString.toResponseBody(value.contentType()))
} catch (e: Exception) {
// Log the raw content that failed to parse
context.logger.error(
"""
|Moshi parsing failed:
|Content-Type: ${value.contentType()}
|Content: $bodyString
|Error: ${e.message}
""".trimMargin()
)

// Re-throw so the onFailure callback still gets called
throw e
}
}
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
package com.coder.toolbox.sdk.interceptors

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.settings.HttpLoggingVerbosity
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import okio.Buffer
import java.nio.charset.StandardCharsets

private val SENSITIVE_HEADERS = setOf("Coder-Session-Token", "Proxy-Authorization")

class LoggingInterceptor(private val context: CoderToolboxContext) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val logLevel = context.settingsStore.httpClientLogLevel
if (logLevel == HttpLoggingVerbosity.NONE) {
return chain.proceed(chain.request())
}

val request = chain.request()
logRequest(request, logLevel)

val response = chain.proceed(request)
logResponse(response, request, logLevel)

return response
}

private fun logRequest(request: Request, logLevel: HttpLoggingVerbosity) {
val log = buildString {
append("request --> ${request.method} ${request.url}")

if (logLevel >= HttpLoggingVerbosity.HEADERS) {
append("\n${request.headers.sanitized()}")
}

if (logLevel == HttpLoggingVerbosity.BODY) {
request.body?.let { body ->
append("\n${body.toPrintableString()}")
}
}
}

context.logger.info(log)
}

private fun logResponse(response: Response, request: Request, logLevel: HttpLoggingVerbosity) {
val log = buildString {
append("response <-- ${response.code} ${response.message} ${request.url}")

if (logLevel >= HttpLoggingVerbosity.HEADERS) {
append("\n${response.headers.sanitized()}")
}

if (logLevel == HttpLoggingVerbosity.BODY) {
response.body?.let { body ->
append("\n${body.toPrintableString()}")
}
}
}

context.logger.info(log)
}
}

// Extension functions for cleaner code
private fun Headers.sanitized(): String = buildString {
this@sanitized.forEach { (name, value) ->
val displayValue = if (name in SENSITIVE_HEADERS) "<redacted>" else value
append("$name: $displayValue\n")
}
}

private fun RequestBody.toPrintableString(): String {
if (!contentType().isPrintable()) {
return "[Binary body: ${contentLength().formatBytes()}, ${contentType()}]"
}

return try {
val buffer = Buffer()
writeTo(buffer)
buffer.readString(contentType()?.charset() ?: StandardCharsets.UTF_8)
} catch (e: Exception) {
"[Error reading body: ${e.message}]"
}
}

private fun ResponseBody.toPrintableString(): String {
if (!contentType().isPrintable()) {
return "[Binary body: ${contentLength().formatBytes()}, ${contentType()}]"
}

return try {
val source = source()
source.request(Long.MAX_VALUE)
source.buffer.clone().readString(contentType()?.charset() ?: StandardCharsets.UTF_8)
} catch (e: Exception) {
"[Error reading body: ${e.message}]"
}
}

private fun MediaType?.isPrintable(): Boolean = when {
this == null -> false
type == "text" -> true
subtype == "json" || subtype.endsWith("+json") -> true
else -> false
}

private fun Long.formatBytes(): String = when {
this < 0 -> "unknown"
this < 1024 -> "${this}B"
this < 1024 * 1024 -> "${this / 1024}KB"
this < 1024 * 1024 * 1024 -> "${this / (1024 * 1024)}MB"
else -> "${this / (1024 * 1024 * 1024)}GB"
}
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,6 +38,11 @@ interface ReadOnlyCoderSettings {
*/
val fallbackOnCoderForSignatures: SignatureFallbackStrategy

/**
* Controls the logging for the rest client.
*/
val httpClientLogLevel: HttpLoggingVerbosity

/**
* Default CLI binary name based on OS and architecture
*/
Expand DownExpand Up@@ -216,4 +221,32 @@ enum class SignatureFallbackStrategy {
else -> NOT_CONFIGURED
}
}
}

enum class HttpLoggingVerbosity {
NONE,

/**
* Logs URL, method, and status
*/
BASIC,

/**
* Logs BASIC + sanitized headers
*/
HEADERS,

/**
* Logs HEADERS + body content
*/
BODY;

companion object {
fun fromValue(value: String?): HttpLoggingVerbosity = when (value?.lowercase(getDefault())) {
"basic" -> BASIC
"headers" -> HEADERS
"body" -> BODY
else -> NONE
}
}
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp