如何在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.
相关产品推荐
相关产品推荐

