Laravel 的日誌系統 - monolog

qiuyuhome發表於2018-02-14

日誌的重要程度不言而喻, 不管是在開發過程中, 還是部署到生產環境後, 都是經常使用的.
隨著 psr-3 的出現, 終於統一了 php 中日誌的風格.
但是, 好用的記錄日誌系統, 也很重要.
monolog 是我遇到的最好的日誌系統.
而且, laravel 中也是用的 monolog.

參考

psr-3

github.com/Seldaek/monolog

phpfudao 這可能是php世界中最好的日誌庫——monolog

Using Monolog

安裝 (Install)

透過 composer 或者 github.

composer require monolog/monolog

文件

核心概念 (Core Concepts)

Every Logger instance has a channel (name) and a stack of handlers. Whenever you add a record to the logger, it traverses the handler stack. Each handler decides whether it fully handled the record, and if so, the propagation of the record ends there.

This allows for flexible logging setups, for example having a StreamHandler at the bottom of the stack that will log anything to disk, and on top of that add a MailHandler that will send emails only when an error message is logged. Handlers also have a $bubble property which defines whether they block the record or not if they handled it. In this example, setting the MailHandler's $bubble argument to false means that records handled by the MailHandler will not propagate to the StreamHandler anymore.

You can create many Loggers, each defining a channel (e.g.: db, request, router, ..) and each of them combining various handlers, which can be shared or not. The channel is reflected in the logs and allows you to easily see or filter records.

Each Handler also has a Formatter, a default one with settings that make sense will be created if you don't set one. The formatters normalize and format incoming records so that they can be used by the handlers to output useful information.

Custom severity levels are not available. Only the eight RFC 5424 levels (debug, info, notice, warning, error, critical, alert, emergency) are present for basic filtering purposes, but for sorting and other use cases that would require flexibility, you should add Processors to the Logger that can add extra information (tags, user ip, ..) to the records before they are handled.

每一個日誌服務例項 (Logger) 都有一個通道(名稱),並有一個處理器 (Handler)棧. 無論何時你新增一條 記錄 到對應的日誌服務例項,這個處理器棧將被遍歷一遍:每個處理器都將依次決定是否要處理這條記錄,而如果要處理,則遍歷結束(譯註:類似DOM事件冒泡)。

這樣子可以建立非常靈活的日誌配置。比如一個 StreamHandler 可以把所有日誌都寫入磁碟,而上面加個MailHandler 可以把錯誤日誌作為郵件傳送出去。處理器還有一個 $bubble 屬性定義了是否遮蔽某條記錄或者處理了某條記錄。在這個示例中,配置 MailHandler 的 $bubble 引數為 false 則意味著 MailHandler 將不會把自己已處理過的記錄繼續冒泡給 StreamHandler.

你可以建立許多日誌服務例項(Logger),每一個則定義一個通道(比如資料庫、請求、路由...)。而每一個日誌服務例項都可以組合各種各樣的處理器,可以共享處理器也可以不共享。這個通道將會在日誌中反映出來,從而允許你可以很容易地檢視或者篩選記錄。

每一個處理還會有一個格式化器(Formatter)。如果你沒有配置一個,則一個有意義的預設的格式化器將被建立。格式化器用來規範化並格式化輸入的記錄,以便處理器能輸出一些有用的資訊。

不支援自定義的嚴重性級別。只支援使用RFC 5424中定義的八個級別(除錯/Debug、資訊/Info、提示/Notice、警告/Warning、錯誤/Error、嚴重/Critical、警報/Alert、緊急/Emergency)來作為基本的篩選目的。不過,如果為了排序或者其他需要靈活性的使用場景,你可以新增加工程式(Processor)從而可以在(處理器)處理前新增額外的資訊(標籤、使用者IP...)。

給日誌新增額外的資訊

using the logging context

The first way is the context, allowing to pass an array of data along the record:

<?php
$logger->info('Adding a new user', array('username' => 'Seldaek'));

Simple handlers (like the StreamHandler for instance) will simply format the array to a string but richer handlers can take advantage of the context (FirePHP is able to display arrays in pretty way for instance).

using processors

The second way is to add extra data for all records by using a processor. Processors can be any callable. They will get the record as parameter and must return it after having eventually changed the extra part of it. Let's write a processor adding some dummy data in the record:

<?php

$logger->pushProcessor(function ($record) {
   $record['extra']['dummy'] = 'Hello world!';

   return $record;
});

Monolog provides some built-in processors that can be used in your project. Look at the dedicated chapter for the list.

Tip: processors can also be registered on a specific handler instead of the logger to apply only for this handler.

  • 使用上下文(context)

第一種方式是使用上下文(context),這允許你在傳遞記錄的時候傳遞一個陣列格式的資料:

簡單的處理器(比如StreamHandler)將只是把陣列轉換成字串。而複雜的處理器則可以利用上下文的優點(如 FirePHP 則將以一種優美的方式顯示陣列)。

  • 使用加工程式(Processor)

第二種方式是使用加工程式來為所有的記錄新增額外資料。加工程式可以是任何可以呼叫的函式。

加工程式接收日誌記錄作為引數,並且需要在修改了extra欄位後再返回日誌記錄。

Monolog提供了一些內建的加工程式,你可以在你的專案中使用它們.

小技巧:加工程式可以被註冊到一個特定的處理器上而不是直接在日誌服務例項上,從而可以只在對應的處理器上生效.

mongolog中幾個重要的概念

handler 日誌處理器

存放 handler 的資料結構是一個“棧”,一個日誌例項可以有多個 handler,透過 Logger 例項的 pushHandler 方法壓入一個 handler,該方法接受一個 HandlerInterface 型別的引數.

如果你設定了多個 handler,當你新增一條日誌的時候,他會從棧頂開始往下傳播,關心這個級別日誌的 handler 將會處理這條日誌.

所有的 handler 都會繼承 AbstractProcessingHandler 這個抽象類,並且只需要實現裡面的抽象方法 write 就可以了.

同時這個抽象類會繼承 AbstractHandler 這個抽象類,這個抽象類的建構函式有兩個引數:levelbubble.

level:表示該 handler 關心的最低日誌級別,是個整型.

bubble:表示日誌被當前 handler 處理後是否接著向下傳遞.

formatter 設定日誌格式

每個 handler 可以單獨設定記錄的日誌格式,AbstractHandler 抽象類中有一個 setFormatter 方法,該引數接受一個 FormatterInterface 型別的引數.

可以看到 monolog 自帶的 formatter 都繼承自 ormalizerFormatter,該類實現了 format和formatBatch 方法。

processor 日誌加工程式,用來給日誌新增額外資訊

存放 processor 的結構也是一個“棧”,意味著你也可以透過 pushProcessor 方法給一個 Logger 例項配置多個 processor

我們注意到,這裡 pushProcesso 接受一個 callable ,也就是需要一個函式或者類方法,但是官方自帶的這些 processor 都是類,

隨便點進去一個原始碼就會發現,其實這些類都用到了 __invoke 魔術方法,所以在被當做 callable 呼叫的時候會自動呼叫 __invoke.

程式碼實踐

程式碼在我的test倉庫中可以下載.

github 測試程式碼

<?php
/**
 * monolog package usage
 * User: qiuyu
 * Date: 2018/2/14
 * Time: 下午1:44
 */

require dirname(__FILE__)."/../vendor/autoload.php";

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\UidProcessor;
use Monolog\Processor\ProcessIdProcessor;
use Monolog\Formatter\JsonFormatter;

// 例項化一個日誌例項, 引數是 channel name
$logger = new Logger('qiuyuhome');

// StreamHandler_1
$streamHander1 = new StreamHandler(__DIR__.'/testLog1.log', Logger::INFO);
// 設定日誌格式為json
$streamHander1->setFormatter(new JsonFormatter());
// 入棧, 往 handler stack 裡壓入 StreamHandler 的例項
$logger->pushHandler($streamHander1);

// StreamHandler_2
// 如果第三個引數為false, 則只會執行這個一個Handler. 預設是true
$streamHander2 = new StreamHandler(__DIR__.'/testLog2.log', Logger::INFO);
// 入棧, 往 handler stack 裡壓入 StreamHandler 的例項
$logger->pushHandler($streamHander2);

/**
 * processor 日誌加工程式,用來給日誌新增額外資訊.
 *
 * 這裡呼叫了內建的 UidProcessor 類和 ProcessIdProcessor 類.
 * 在生成的日誌檔案中, 會在最後面顯示這些額外資訊.
 */
$logger->pushProcessor(new UidProcessor());
$logger->pushProcessor(new ProcessIdProcessor());
$logger->pushProcessor(function ($record) {
    $record['message'] = 'Hello ' . $record['message'];
    return $record;
});

/**
 * 設定記錄到日誌的資訊.
 *
 * 開始遍歷 handler stack.
 * 先入後出, 後壓入的最先執行. 所以先執行 FirePHPHandler, 再執行 StreamHandler
 * 如果設定了 ErrorLogHandler 的 $bubble = false, 會停止冒泡, StreamHandler 不會執行.
 * 第二個引數為陣列格式, 透過使用使用上下文(context)新增了額外的資料.
 * 簡單的處理器(比如StreamHandler)將只是把陣列轉換成字串。而複雜的處理器則可以利用上下文的優點(如 FirePHP 則將以一種優美的方式顯示陣列).
 */
$logger->info('Welcome to QiuYu Blog.', ['username' => 'QiuYu']);

執行結果

執行檔案的同目錄下, 會生成 testLog1.logtestLog2.log 日誌檔案. 內容為:

testLog1.log

{"message":"Hello Welcome to QiuYu Blog.","context":{"username":"QiuYu"},"level":200,"level_name":"INFO","channel":"qiuyuhome","datetime":{"date":"2018-02-14 23:33:28.928112","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":{"process_id":21520,"uid":"54a8a7d"}}

testLog2.log

[2018-02-14 23:33:28] qiuyuhome.INFO: Hello Welcome to QiuYu Blog. {"username":"QiuYu"} {"process_id":21520,"uid":"54a8a7d"}
本作品採用《CC 協議》,轉載必須註明作者和本文連結
當才華還支援不起理想時,就應該靜下心來好好學習了。

相關文章