How can I override logRequest/logResponse to log custom message in Ktor client logging?

1.1k Views Asked by At

Currently, the ktor client logging implementation is as below, and it works as intended but not what I wanted to have.


public class Logging(
    public val logger: Logger,
    public var level: LogLevel,
    public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList()
)
....
   private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
        if (level.info) {
            logger.log("REQUEST: ${Url(request.url)}")
            logger.log("METHOD: ${request.method}")
        }

        val content = request.body as OutgoingContent

        if (level.headers) {
            logger.log("COMMON HEADERS")
            logHeaders(request.headers.entries())

            logger.log("CONTENT HEADERS")
            logHeaders(content.headers.entries())
        }

        return if (level.body) {
            logRequestBody(content)
        } else null
    }

Above creates a nightmare while looking at the logs because it's logging in each line. Since I'm a beginner in Kotlin and Ktor, I'd love to know the way to change the behaviour of this. Since in Kotlin, all classes are final unless opened specifically, I don't know how to approach on modifying the logRequest function behaviour. What I ideally wanted to achieve is something like below for an example.

....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {

        ...
        if (level.body) {
            val content = request.body as OutgoingContent
        return logger.log(value("url", Url(request.url)),
                          value("method", request.method),
                          value("body", content))


    }

Any help would be appreciative

2

There are 2 best solutions below

0
On

No way to actually override a private method in a non-open class, but if you just want your logging to work differently, you're better off with a custom interceptor of the same stage in the pipeline:

       val client = HttpClient(CIO) {
            install("RequestLogging") {
                sendPipeline.intercept(HttpSendPipeline.Monitoring) {
                    logger.info(
                        "Request: {} {} {} {}",
                        context.method,
                        Url(context.url),
                        context.headers.entries(),
                        context.body
                    )
                }
            }
        }
        runBlocking {
            client.get<String>("https://google.com")
        }

This will produce the logging you want. Of course, to properly log POST you will need to do some extra work.

0
On

Maybe this will be useful for someone:

HttpClient() {
    install("RequestLogging") {
        responsePipeline.intercept(HttpResponsePipeline.After) {
            val request = context.request
            val response = context.response
            kermit.d(tag = "Network") {
                "${request.method} ${request.url} ${response.status}"
            }
            GlobalScope.launch(Dispatchers.Unconfined) {
                val responseBody =
                    response.content.tryReadText(response.contentType()?.charset() ?: Charsets.UTF_8)
                        ?: "[response body omitted]"
                kermit.d(tag = "Network") {
                    "${request.method} ${request.url} ${response.status}\nBODY START" +
                            "\n$responseBody" +
                            "\nBODY END"
                }
            }
        }
    }
}

You also need to add a method from the Ktor Logger.kt class to your calss with HttpClient:

internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
    readRemaining().readText(charset = charset)
} catch (cause: Throwable) {
    null
}