如何在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内容,修改插件的响应拦截逻辑即可:
- 首先调整响应拦截阶段的插入位置,确保拦截发生在ContentNegotiation序列化处理之后
- 对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
相关产品推荐
相关产品推荐

