§日志记录 API
在您的应用程序中使用日志记录对于监控、调试、错误跟踪和商业智能非常有用。Play 提供了一个日志记录 API,可以通过 Logger
对象访问,并使用 Logback 作为默认的日志记录引擎。
§日志记录架构
日志记录 API 使用一组组件来帮助您实施有效的日志记录策略。
§日志记录器
您的应用程序可以定义 Logger
实例来发送日志消息请求。每个 Logger
都有一个名称,该名称将出现在日志消息中,并用于配置。Logger API 基于 SLF4J,因此 Logger
基于 org.slf4j.Logger
接口。
日志记录器遵循基于其名称的分层继承结构。如果一个日志记录器的名称后跟一个点是后代日志记录器名称的前缀,则该日志记录器被称为另一个日志记录器的祖先。例如,名为“com.foo”的日志记录器是名为“com.foo.bar.Baz”的日志记录器的祖先。所有日志记录器都继承自根日志记录器。日志记录器继承允许您通过配置一个共同的祖先来配置一组日志记录器。
我们建议为每个类创建单独命名的日志记录器。遵循此约定,Play 库使用命名空间为“play”的日志记录器,许多第三方库将使用基于其类名的日志记录器。
§日志级别
日志级别用于对日志消息的严重性进行分类。当您编写日志请求语句时,您将指定严重性,这将显示在生成的日志消息中。
这是可用日志级别的集合,按严重性降序排列。
OFF
- 用于关闭日志记录,而不是作为消息分类。ERROR
- 运行时错误或意外情况。WARN
- 使用弃用 API、API 使用不当、“几乎”错误、其他运行时情况,这些情况不可取或意外,但不一定是“错误”。INFO
- 有趣的运行时事件,例如应用程序启动和关闭。DEBUG
- 有关系统流程的详细信息。TRACE
- 最详细的信息。
除了对消息进行分类外,日志级别还用于配置日志记录器和附加程序的严重性阈值。例如,设置为级别 INFO
的日志记录器将记录级别为 INFO
或更高(INFO
、WARN
、ERROR
)的任何请求,但会忽略级别较低的请求(DEBUG
、TRACE
)。使用 OFF
将忽略所有日志请求。
§附加程序
日志记录 API 允许将日志记录请求打印到一个或多个称为“附加程序”的输出目标。附加程序在配置中指定,并且存在用于控制台、文件、数据库和其他输出的选项。
附加程序与日志记录器相结合,可以帮助您路由和过滤日志消息。例如,您可以为记录用于分析的有用数据的日志记录器使用一个附加程序,而为由运维团队监控的错误使用另一个附加程序。
注意:有关体系结构的更多信息,请参阅 Logback 文档。
§使用日志记录器
首先导入 Logger
类和伴随对象
import play.api.Logger
§创建日志记录器
您可以使用带有name
参数的Logger.apply
工厂方法创建一个新的日志记录器。
val accessLogger: Logger = Logger("access")
记录应用程序事件的常见策略是使用每个类别的不同日志记录器,使用类名。日志记录 API 使用带有类参数的工厂方法支持此功能。
val logger: Logger = Logger(this.getClass())
还有一个Logging
特征,它会自动为您执行此操作并公开一个protected val logger
。
import play.api.Logging
class MyClassWithLogging extends Logging {
logger.info("Using the trait")
}
设置好Logger
后,您可以使用它来编写日志语句。
// Log some debug info
logger.debug("Attempting risky calculation.")
try {
val result = riskyCalculation
// Log result if successful
logger.debug(s"Result=$result")
} catch {
case t: Throwable => {
// Log error with message and Throwable.
logger.error("Exception with riskyCalculation", t)
}
}
使用 Play 的默认日志记录配置,这些语句将生成类似于以下内容的控制台输出。
[debug] c.e.s.MyClass - Attempting risky calculation.
[error] c.e.s.MyClass - Exception with riskyCalculation
java.lang.ArithmeticException: / by zero
at controllers.Application.riskyCalculation(Application.java:20) ~[classes/:na]
at controllers.Application.index(Application.java:11) ~[classes/:na]
at Routes$$anonfun$routes$1$$anonfun$applyOrElse$1$$anonfun$apply$1.apply(routes_routing.scala:69) [classes/:na]
at Routes$$anonfun$routes$1$$anonfun$applyOrElse$1$$anonfun$apply$1.apply(routes_routing.scala:69) [classes/:na]
at play.core.Router$HandlerInvoker$$anon$8$$anon$2.invocation(Router.scala:203) [play_2.10-2.3-M1.jar:2.3-M1]
请注意,消息包含日志级别、日志记录器名称(在本例中为类名,以缩写形式显示)、消息以及如果在日志请求中使用了Throwable
,则包含堆栈跟踪。
还有一个play.api.Logger
单例对象,允许您访问名为application
的日志记录器,但它在 Play 2.7.0 及更高版本中已弃用。您应该使用上面定义的策略之一声明自己的日志记录器实例。
§使用标记和标记上下文
SLF4J API 有一个标记的概念,它充当丰富日志消息和将消息标记为特别感兴趣的标记。标记对于触发和过滤特别有用 - 例如,OnMarkerEvaluator可以在看到标记时发送电子邮件,或者可以将特定流程标记到其自己的附加程序。
Logger API 通过play.api.MarkerContext
特征提供对标记的访问。
您可以使用MarkerContext.apply
方法通过 Logger 创建一个 MarkerContext。
val marker: org.slf4j.Marker = MarkerFactory.getMarker("SOMEMARKER")
val mc: MarkerContext = MarkerContext(marker)
您还可以通过从DefaultMarkerContext
扩展来提供类型化的 MarkerContext。
val someMarker: org.slf4j.Marker = MarkerFactory.getMarker("SOMEMARKER")
case object SomeMarkerContext extends play.api.DefaultMarkerContext(someMarker)
创建 MarkerContext 后,它可以与日志语句一起使用,无论是显式地
// use a typed marker as input
logger.info("log message with explicit marker context with case object")(SomeMarkerContext)
// Use a specified marker.
val otherMarker: Marker = MarkerFactory.getMarker("OTHER")
val otherMarkerContext: MarkerContext = MarkerContext(otherMarker)
logger.info("log message with explicit marker context")(otherMarkerContext)
还是隐式地
val marker: Marker = MarkerFactory.getMarker("SOMEMARKER")
implicit val mc: MarkerContext = MarkerContext(marker)
// Use the implicit MarkerContext in logger.info...
logger.info("log message with implicit marker context")
为了方便起见,有一个从Marker
到MarkerContext
的隐式转换可用。
val mc: MarkerContext = MarkerFactory.getMarker("SOMEMARKER")
// Use the marker that has been implicitly converted to MarkerContext
logger.info("log message with implicit marker context")(mc)
标记非常有用,因为它们可以通过使用 MarkerContext 作为方法的隐式参数来提供日志记录上下文,从而跨线程携带上下文信息,而 MDC 可能不可用。例如,使用Logstash Logback 编码器和隐式转换链,请求信息可以自动编码到日志语句中。
trait RequestMarkerContext {
// Adding 'implicit request' enables implicit conversion chaining
// See https://docs.scala-lang.org.cn/tutorials/FAQ/chaining-implicits.html
implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
import net.logstash.logback.marker.LogstashMarker
import net.logstash.logback.marker.Markers._
val requestMarkers: LogstashMarker = append("host", request.host)
.and(append("path", request.path))
MarkerContext(requestMarkers)
}
}
然后在控制器中使用,并通过可能使用不同执行上下文的Future
传递。
def asyncIndex = Action.async { implicit request =>
Future {
methodInOtherExecutionContext() // implicit conversion here
}(otherExecutionContext)
}
def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
logger.debug("index: ") // same as above
Ok("testing")
}
请注意,标记上下文对于“追踪子弹”式日志记录也非常有用,在这种情况下,您希望在特定请求上进行日志记录,而无需显式更改日志级别。例如,您可以在满足某些条件时才添加标记。
trait TracerMarker {
import TracerMarker._
implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic") // base do-nothing marker...
if (request.getQueryString("trace").nonEmpty) {
marker.add(tracerMarker)
}
marker
}
}
object TracerMarker {
private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}
class TracerBulletController @Inject() (cc: ControllerComponents) extends AbstractController(cc) with TracerMarker {
private val logger = play.api.Logger("application")
def index = Action { implicit request: Request[AnyContent] =>
logger.trace("Only logged if queryString contains trace=true")
Ok("hello world")
}
}
然后使用以下logback.xml
中的TurboFilter触发日志记录。
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Name>TRACER_FILTER</Name>
<Marker>TRACER</Marker>
<OnMatch>ACCEPT</OnMatch>
</turboFilter>
此时,您可以根据输入动态设置调试语句。
有关在日志记录中使用标记的更多信息,请参阅Logback 手册中的TurboFilters和基于标记的触发部分。
§日志记录模式
有效地使用日志记录器可以帮助您使用相同的工具实现许多目标。
import scala.concurrent.Future
import play.api.Logger
import play.api.mvc._
import javax.inject.Inject
class AccessLoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) {
val accessLogger = Logger("access")
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
accessLogger.info(s"method=${request.method} uri=${request.uri} remote-address=${request.remoteAddress}")
block(request)
}
}
class Application @Inject() (val accessLoggingAction: AccessLoggingAction, cc: ControllerComponents)
extends AbstractController(cc) {
val logger = Logger(this.getClass())
def index = accessLoggingAction {
try {
val result = riskyCalculation
Ok(s"Result=$result")
} catch {
case t: Throwable => {
logger.error("Exception with riskyCalculation", t)
InternalServerError("Error in calculation: " + t.getMessage())
}
}
}
}
此示例使用动作组合来定义一个AccessLoggingAction
,它将请求数据记录到名为“access”的日志记录器。Application
控制器使用此动作,并且它还使用自己的日志记录器(以其类命名)来记录应用程序事件。在配置中,您可以将这些日志记录器路由到不同的追加器,例如访问日志和应用程序日志。
如果您只想为特定动作记录请求数据,上述设计效果很好。要记录所有请求,最好使用过滤器
import javax.inject.Inject
import org.apache.pekko.stream.Materializer
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import play.api.Logger
import play.api.mvc._
import play.api._
class AccessLoggingFilter @Inject() (implicit val mat: Materializer) extends Filter {
val accessLogger = Logger("access")
def apply(next: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = {
val resultFuture = next(request)
resultFuture.foreach(result => {
val msg = s"method=${request.method} uri=${request.uri} remote-address=${request.remoteAddress}" +
s" status=${result.header.status}";
accessLogger.info(msg)
})
resultFuture
}
}
在过滤器版本中,我们通过在Future[Result]
完成时记录日志,将响应状态添加到日志请求中。
§配置
有关配置的详细信息,请参阅配置日志记录。
下一步:高级主题
发现此文档中的错误?此页面的源代码可以在此处找到。在阅读文档指南后,请随时贡献拉取请求。有疑问或建议要分享?前往我们的社区论坛与社区开始对话。