Ktor Server/Application request/response body logging

Viewed 1072

Is there any way to log the request and response body from the ktor server communication? The buildin CallLogging feature only logs the metadata of a call. I tried writing my own logging feature like in this example: https://github.com/Koriit/ktor-logging/blob/master/src/main/kotlin/korrit/kotlin/ktor/features/logging/Logging.kt

class Logging(private val logger: Logger) {

    class Configuration {
        var logger: Logger = LoggerFactory.getLogger(Logging::class.java)
    }

    private suspend fun logRequest(call: ApplicationCall) {
        logger.info(StringBuilder().apply {
            appendLine("Received request:")
            val requestURI = call.request.path()
            appendLine(call.request.origin.run { "${method.value} $scheme://$host:$port$requestURI $version" })

            call.request.headers.forEach { header, values ->
                appendLine("$header: ${values.firstOrNull()}")
            }

            try {
                appendLine()
                appendLine(String(call.receive<ByteArray>()))
            } catch (e: RequestAlreadyConsumedException) {
                logger.error("Logging payloads requires DoubleReceive feature to be installed with receiveEntireContent=true", e)
            }
        }.toString())
    }

    private suspend fun logResponse(call: ApplicationCall, subject: Any) {
        logger.info(StringBuilder().apply {
            appendLine("Sent response:")
            appendLine("${call.request.httpVersion} ${call.response.status()}")
            call.response.headers.allValues().forEach { header, values ->
                appendLine("$header: ${values.firstOrNull()}")
            }
            when (subject) {
                is TextContent -> appendLine(subject.text)
                is OutputStreamContent -> appendLine() // ToDo: How to get response body??
                else -> appendLine("unknown body type")
            }
        }.toString())
    }

    /**
     * Feature installation.
     */
    fun install(pipeline: Application) {
        pipeline.intercept(ApplicationCallPipeline.Monitoring) {
            logRequest(call)
            proceedWith(subject)
        }
        pipeline.sendPipeline.addPhase(responseLoggingPhase)
        pipeline.sendPipeline.intercept(responseLoggingPhase) {
            logResponse(call, subject)
        }
    }

    companion object Feature : ApplicationFeature<Application, Configuration, Logging> {

        override val key = AttributeKey<Logging>("Logging Feature")
        val responseLoggingPhase = PipelinePhase("ResponseLogging")

        override fun install(pipeline: Application, configure: Configuration.() -> Unit): Logging {
            val configuration = Configuration().apply(configure)

            return Logging(configuration.logger).apply { install(pipeline) }
        }
    }
}

It works fine for logging the request body using the DoubleReceive plugin. And if the response is plain text i can log the response as the subject in the sendPipeline interception will be of type TextContent or like in the example ByteArrayContent.

But in my case i am responding a data class instance with Jackson ContentNegotiation. In this case the subject is of type OutputStreamContent and i see no options to geht the serialized body from it.

Any idea how to log the serialized response json in my logging feature? Or maybe there is another option using the ktor server? I mean i could serialize my object manually and respond plain text, but thats an ugly way to do it.

1 Answers

I'm not shure about if this is the best way to do it, but here it is:

public fun ApplicationResponse.toLogString(subject: Any): String = when(subject) {
is TextContent -> subject.text
is OutputStreamContent -> {
    val channel = ByteChannel(true)
    runBlocking {
        (subject as OutputStreamContent).writeTo(channel)
        val buffer = StringBuilder()
        while (!channel.isClosedForRead) {
            channel.readUTF8LineTo(buffer)
        }
        buffer.toString()
    }
}
else -> String()

}

Related