You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Ktor Server中记录请求响应体及序列化后的JSON响应内容

方案1:使用Ktor官方CallLogging特性(2.x及以上版本推荐)

Ktor 2.x开始内置的CallLogging已经原生支持记录请求和响应正文,无需自行实现完整插件,配合DoubleReceive插件即可实现需求,参考配置如下:

// 必须先安装DoubleReceive,允许请求/响应体被多次读取
install(DoubleReceive) {
    receiveEntireContent = true
}

install(CallLogging) {
    level = Level.INFO
    // 开启请求体记录
    watchRequestBody = true
    // 开启响应体记录
    watchResponseBody = true
    // 自定义日志输出格式
    format { call ->
        buildString {
            appendLine("=== 请求信息 ===")
            appendLine("${call.request.httpMethod.value} ${call.request.uri} ${call.request.httpVersion}")
            appendLine("请求头:${call.request.headers.entries()}")
            appendLine("请求体:${call.request.receiveChannel().readRemaining().readText()}")
            appendLine("=== 响应信息 ===")
            appendLine("状态码:${call.response.status()}")
            appendLine("响应头:${call.response.headers.entries()}")
            appendLine("响应体:${call.response.content}")
        }
    }
}

该方案为官方原生实现,兼容性和稳定性最优,不需要自行处理不同响应类型的兼容逻辑。

方案2:自定义插件适配OutputStreamContent

如果需要保留现有自定义插件的逻辑,可以通过代理响应输出流的方式捕获Jackson序列化后的JSON内容,修改插件的响应拦截逻辑即可:

  1. 首先调整响应拦截阶段的插入位置,确保拦截发生在ContentNegotiation序列化处理之后
  2. 对OutputStreamContent类型的响应主体,先将内容写入内存缓冲,记录日志后再构造新的响应主体传递给后续流程
    参考代码实现:
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)
        val feature = Logging(configuration.logger)
        
        // 将响应日志阶段插入到内容编码阶段之后,保证拿到的是序列化后的内容
        pipeline.sendPipeline.insertPhaseAfter(ApplicationSendPipeline.ContentEncoding, responseLoggingPhase)
        pipeline.sendPipeline.intercept(responseLoggingPhase) { subject ->
            when (subject) {
                is TextContent -> {
                    feature.logResponse(call, subject.text)
                    proceed()
                }
                is OutputStreamContent -> {
                    // 捕获OutputStreamContent的内容
                    val baos = ByteArrayOutputStream()
                    subject.writeTo(baos)
                    val responseBody = baos.toString(StandardCharsets.UTF_8)
                    // 记录日志
                    feature.logResponse(call, responseBody)
                    // 构造新的响应主体继续传递,避免响应内容丢失
                    proceedWith(object : OutgoingContent.WriteChannelContent() {
                        override val contentType = subject.contentType
                        override val status = subject.status
                        override val headers = subject.headers
                        override suspend fun writeTo(channel: ByteWriteChannel) {
                            channel.writeFully(baos.toByteArray())
                        }
                    })
                }
                else -> {
                    feature.logResponse(call, "unknown body type")
                    proceed()
                }
            }
        }

        // 原有请求拦截逻辑保持不变
        pipeline.intercept(ApplicationCallPipeline.Monitoring) {
            feature.logRequest(call)
            proceed()
        }
        
        return feature
    }
}

注意事项

  • 两种方案都必须安装DoubleReceive插件,否则会抛出请求/响应体已被消费的异常
  • 若服务存在大文件/二进制流响应,建议根据Content-Type增加过滤逻辑,仅记录application/json等文本类型的响应,避免大内容读取到内存导致OOM
  • 生产环境建议增加敏感字段过滤和日志开关,避免用户隐私、密钥等信息被泄露到日志中

内容的提问来源于stack exchange,提问作者Momo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 12:45:03