文件

§記錄 API

在應用程式中使用記錄對於監控、除錯、錯誤追蹤和商業智慧很有用。Play 提供了一個記錄 API,可透過 Logger 物件存取,並使用 Logback 作為預設記錄引擎。

§記錄架構

記錄 API 使用一組元件來協助您實作有效的記錄策略。

§記錄器

您的應用程式可以定義 Logger 執行個體,以傳送記錄訊息要求。每個 Logger 都有一個名稱,會顯示在記錄訊息中,並用於組態。Logger API 是基於 SLF4J,因此 Logger 是基於 org.slf4j.Logger 介面。

記錄器會依據其命名遵循階層繼承結構。如果記錄器的名稱加上一個點是後代記錄器名稱的前綴,則記錄器會被視為另一個記錄器的祖先。例如,名為「com.foo」的記錄器是名為「com.foo.bar.Baz」的記錄器的祖先。所有記錄器都繼承自根記錄器。記錄器繼承讓您可以透過組態共同祖先來組態一組記錄器。

我們建議為每個類別建立名稱不同的記錄器。遵循此慣例,Play 函式庫使用「play」下的記錄器名稱空間,許多第三方函式庫的記錄器會基於其類別名稱。

§記錄層級

記錄層級用於分類記錄訊息的嚴重性。當您撰寫記錄要求陳述式時,您會指定嚴重性,這會顯示在產生的記錄訊息中。

以下是可用記錄層級的集合,依嚴重性遞減排列。

除了分類訊息之外,記錄等級還用於設定記錄器和附加元件的嚴重性閾值。例如,設定為 資訊 等級的記錄器將記錄 資訊 等級或更高(資訊警告錯誤)的任何要求,但會忽略較低嚴重性(偵錯追蹤)的要求。使用 關閉 將忽略所有記錄要求。

§附加元件

記錄 API 允許記錄要求列印到一個或多個稱為「附加元件」的輸出目的地。附加元件在設定中指定,並存在主控台、檔案、資料庫和其他輸出的選項。

附加元件與記錄器結合使用,可以幫助您路由和篩選記錄訊息。例如,您可以使用一個附加元件來記錄分析的有用資料,並使用另一個附加元件來記錄由操作團隊監控的錯誤。

注意:有關架構的更多資訊,請參閱 Logback 文件

§使用記錄器

首先匯入 記錄器 類別和伴隨物件

import play.api.Logger

§建立記錄器

您可以使用 Logger.apply 工廠方法,並搭配 名稱 參數來建立新的記錄器

val accessLogger: Logger = Logger("access")

記錄應用程式事件的常見策略是使用每個類別的特定記錄器,並使用類別名稱。記錄 API 支援這項功能,並提供一個採用類別參數的工廠方法

val logger: Logger = Logger(this.getClass())

還有一個 記錄 特性,它會自動為您執行這項工作,並公開 受保護的 val 記錄器

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 可以於看到標記時傳送電子郵件,或將特定流程標示到自己的附加程式。

記錄器 API 可透過 play.api.MarkerContext 特質存取標記。

您可以使用 MarkerContext.apply 方法,透過記錄器建立 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 作為方法的隱式參數,來傳遞跨執行緒的內容相關資訊,以提供記錄環境。例如,使用 Logstash Logback Encoder隱式轉換鏈,可以自動將請求資訊編碼到記錄陳述式中

trait RequestMarkerContext {
  // Adding 'implicit request' enables implicit conversion chaining
  // See https://scala-docs.dev.org.tw/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] 完成時記錄,將回應狀態新增到記錄請求中。

§組態

請參閱 組態記錄,以取得組態的詳細資料。

下一步:進階主題


在此文件發現錯誤?此頁面的原始程式碼可在此處找到 here。在閱讀完 文件指南 後,請隨時貢獻一個 pull request。有問題或建議要分享?前往 我們的社群論壇 與社群展開對話。