§記錄 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、「幾乎」錯誤,以及其他不理想或意外的執行時期狀況,但未必是「錯誤」。資訊
- 有趣的執行階段事件,例如應用程式啟動和關閉。偵錯
- 系統流程的詳細資訊。追蹤
- 最詳細的資訊。
除了分類訊息之外,記錄等級還用於設定記錄器和附加元件的嚴重性閾值。例如,設定為 資訊
等級的記錄器將記錄 資訊
等級或更高(資訊
、警告
、錯誤
)的任何要求,但會忽略較低嚴重性(偵錯
、追蹤
)的要求。使用 關閉
將忽略所有記錄要求。
§附加元件
記錄 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。有問題或建議要分享?前往 我們的社群論壇 與社群展開對話。