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

如何在Tapir/ZIOHttp DefaultServerLog中获取请求负载配置MDC日志?

解决方案:在Tapir DefaultServerLog中注入请求负载关联的MDC上下文

可以实现这个需求,核心是在请求负载解析完成后、日志事件触发前,将提取的属性注入ZIO日志上下文,并确保DefaultServerLog的所有日志操作都继承该上下文。以下是具体实现步骤:

1. 定义自定义LogAnnotation

先创建用于MDC关联的日志注解,这是ZIO logging识别上下文属性的核心:

import zio.logging.LogAnnotation

// 以请求ID为例,替换成你需要从负载提取的属性
case object RequestId extends LogAnnotation[String]("requestId", _.toString, Ordering.String)

2. 自定义EndpointInterceptor注入上下文

通过Tapir的拦截器,在请求负载解析完成后提取属性,并将LogAnnotation注入ZIO上下文。这个拦截器会在业务逻辑执行前生效,确保后续所有日志(包括DefaultServerLog的日志)都能拿到该属性:

import tapir._
import tapir.server.ServerEndpoint
import zio._

class PayloadMdcInterceptor[R] extends EndpointInterceptor[R, R] {
  override def apply[I, E, O](
    endpoint: Endpoint[I, E, O, Any],
    serverEndpoint: ServerEndpoint[I, E, O, R, Any]
  ): ServerEndpoint[I, E, O, R, Any] = {
    serverEndpoint.copy(
      logic = (input: I) => {
        // 从请求输入(已解析的负载)中提取目标属性
        // 这里根据你的实际请求体类型调整,比如匹配具体的case class
        val targetAttribute = input match {
          case req: CreateUserRequest => req.requestId // 示例:从CreateUserRequest拿requestId
          case _ => java.util.UUID.randomUUID().toString // 无匹配时的兜底值
        }

        // 将属性注入ZIO日志上下文,后续所有ZIO操作都会继承这个上下文
        serverEndpoint.logic(input).provideSomeLayer[R](
          ZLayer.succeed(LogAnnotation(RequestId, targetAttribute))
        )
      }
    )
  }
}

object PayloadMdcInterceptor {
  def apply[R]: PayloadMdcInterceptor[R] = new PayloadMdcInterceptor[R]()
}

3. 自定义DefaultServerLog继承上下文

默认的DefaultServerLog可能会在独立的ZIO上下文中执行日志操作,导致无法继承之前注入的MDC。我们需要重写它的方法,确保日志操作在当前请求的上下文里运行:

import tapir.server.ServerLog
import tapir.server.ziohttp.ZioHttpServerOptions
import zio._
import zio.logging._

class ContextAwareServerLog[R](delegate: ServerLog[R, ZIO[R, Nothing, Unit]]) extends ServerLog[R, ZIO[R, Nothing, Unit]] {
  override def logWhenReceived(req: Request): ZIO[R, Nothing, Unit] = delegate.logWhenReceived(req)
  override def logWhenHandled(req: Request, resp: Response, latency: Long): ZIO[R, Nothing, Unit] = delegate.logWhenHandled(req, resp, latency)
  override def logExceptionWhenHandled(req: Request, ex: Throwable, latency: Long): ZIO[R, Nothing, Unit] = delegate.logExceptionWhenHandled(req, ex, latency)
  override def logWhenRejected(req: Request, reason: String, latency: Long): ZIO[R, Nothing, Unit] = delegate.logWhenRejected(req, reason, latency)
}

object ContextAwareServerLog {
  def make[R]: ZIO[R, Nothing, ContextAwareServerLog[R]] = {
    DefaultServerLog.default[R].map(new ContextAwareServerLog(_))
  }
}

4. 配置服务器整合拦截器与自定义日志

把拦截器和自定义日志配置到ZIO-HTTP服务器中,确保所有端点都应用拦截器,且日志使用上下文感知的实现:

import tapir.server.ziohttp.ZioHttpInterpreter
import zio.http._
import zio._

object Main extends ZIOAppDefault {
  // 替换成你的实际端点列表
  val endpoints: List[ServerEndpoint[_, _, _, Any, Any]] = List(createUserEndpoint)

  override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
    val serverOptions = for {
      customLog <- ContextAwareServerLog.make[Any]
    } yield ZioHttpServerOptions.default.copy(serverLog = customLog)

    // 给所有端点应用MDC拦截器
    val httpApp = ZioHttpInterpreter(Right(PayloadMdcInterceptor[Any])).toHttp(endpoints)

    Server.serve(httpApp).provide(
      Server.defaultWithPort(8080),
      serverOptions.toLayer,
      // 配置ZIO logging输出包含自定义注解
      Logging.addTimestamp,
      Logging.logAnnotation(RequestId)
    )
  }
}

关键说明

  • 之前自定义日志生效、Tapir日志无效的原因:自定义日志是在业务逻辑的上下文(已注入MDC)中执行,而默认DefaultServerLog的日志操作是在独立上下文运行,没有继承请求阶段的MDC属性。
  • 如果需要在logWhenReceived(请求刚接收时)就关联属性,只能从请求头、路径参数等已解析的信息中提取,因为此时请求负载还未被解析。

内容的提问来源于stack exchange,提问作者Chris W.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 07:20:40