如何禁用play.api.Logger默认的NoMarker隐式MarkerContext,强制日志必须传入自定义MarkerContext以添加请求唯一ID
你这个思路其实已经找对方向了!要实现“强制日志必须带自定义MarkerContext(也就是请求唯一ID)”,核心就是让编译器找不到Play默认提供的NoMarker隐式实例,而你的MarkerContextWrapper封装刚好能做到这一点,我给你梳理下具体的落地细节和优化点:
为什么你的封装能生效?
Play的MarkerContext默认自带了一个implicit val NoMarker: MarkerContext,这就是为什么你没传隐式参数时日志还能正常跑的原因。但你定义的MarkerLogger里,日志方法要求的隐式参数是MarkerContextWrapper类型——和原生的MarkerContext不是一回事,编译器没法自动把NoMarker转换成Wrapper,所以只要你没在作用域里提供MarkerContextWrapper的隐式实例,调用日志方法就会直接报编译错误,完美达到强制要求的效果。
优化你的封装,让使用更顺手
给Wrapper加伴生对象,简化创建
可以给MarkerContextWrapper写个伴生对象,提供快捷生成方法,尤其是针对请求ID的场景:class MarkerContextWrapper(markerContext: MarkerContext) { def getMarkerContext: MarkerContext = markerContext } object MarkerContextWrapper { // 直接从Marker创建Wrapper def apply(marker: Marker): MarkerContextWrapper = new MarkerContextWrapper(MarkerContext(marker)) // 专门针对请求ID的快捷方法 def fromRequestId(requestId: String): MarkerContextWrapper = apply(Marker(s"requestId=$requestId")) }完善MarkerLogger的所有日志级别方法
你现在只写了debug方法,记得把info、warn、error等所有常用级别都补上,保持和原生Logger一致的API:class MarkerLogger(clazz: Class[_]){ private val logger = Logger(clazz) def debug(message: => String)(implicit wrapper: MarkerContextWrapper): Unit = { logger.debug(message)(wrapper.getMarkerContext) } def debug(message: => String, error: => Throwable)(implicit wrapper: MarkerContextWrapper): Unit = { logger.debug(message, error)(wrapper.getMarkerContext) } def info(message: => String)(implicit wrapper: MarkerContextWrapper): Unit = { logger.info(message)(wrapper.getMarkerContext) } def info(message: => String, error: => Throwable)(implicit wrapper: MarkerContextWrapper): Unit = { logger.info(message, error)(wrapper.getMarkerContext) } // 同理补上warn、error等方法... } // 再加个伴生对象,方便创建实例 object MarkerLogger { def apply[T](clazz: Class[T]): MarkerLogger = new MarkerLogger(clazz) def apply[T](implicit tag: ClassTag[T]): MarkerLogger = new MarkerLogger(tag.runtimeClass) }
项目中落地的关键步骤
在请求流程中注入隐式Wrapper
在每个Action里生成请求ID(可以从请求头取,或者自己生成UUID),然后把对应的MarkerContextWrapper放到隐式作用域里:class MyController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { private val logger = MarkerLogger(classOf[MyController]) def index = Action { implicit request => // 生成或获取请求ID val requestId = request.headers.get("X-Request-ID").getOrElse(java.util.UUID.randomUUID().toString) // 放入隐式作用域 implicit val markerWrapper = MarkerContextWrapper.fromRequestId(requestId) // 此时调用日志方法会自动使用这个Wrapper,没传的话直接编译报错 logger.debug("开始处理请求") logger.info("请求参数:{}", request.queryString) Ok("请求处理完成") } }确保全项目使用自定义MarkerLogger
一定要让团队成员都用你封装的MarkerLogger,而不是原生的play.api.Logger。可以通过依赖注入的方式统一提供,或者在项目的基础包中只导出你的MarkerLogger,避免不小心导入原生Logger。彻底屏蔽默认NoMarker
可以在项目的根包的package.scala里,只导入MarkerContext的必要部分,不导入NoMarker:package com.yourcompany import play.api.MarkerContext import play.api.MarkerContext.Marker // 不要导入play.api.MarkerContext.NoMarker这样整个项目里默认都不会有
NoMarker的隐式实例,进一步避免误用。
注意事项
- 如果有工具类、服务类需要日志,一定要把
MarkerContextWrapper通过隐式参数传递过去,比如:class UserService(markerLogger: MarkerLogger[UserService]) { def getUser(id: String)(implicit wrapper: MarkerContextWrapper): User = { markerLogger.info(s"查询用户ID:$id") // 业务逻辑... } } - 对于异步流程,要确保隐式的
MarkerContextWrapper能跟着上下文传递(比如用ExecutionContext或者专门的上下文载体),避免异步调用时丢失请求ID。
备注:内容来源于stack exchange,提问作者Stefan Stojkovski

