日誌
簡介
為了協助你瞭解程式中發生的大小事,Laravel 提供了強健的日誌服務,能讓你將訊息紀錄到檔案、系統錯誤日誌、甚至是紀錄到 Slack 中來通知整個團隊。
Laravel 的 Log 紀錄是基於「通道」的。每個通道都代表了一種寫入日誌資訊的特定方法。舉例來說,single
通道將日誌寫進單一日誌檔中,而 slack
通道則將日誌訊息傳送到 Slack。也可以依據日誌的嚴重性來將日誌訊息寫到多個通道。
在 Laravel 內部,我們使用了 Monolog 函式庫。Monolog 提供了多種強大的日誌處理程式。Laravel 還讓我們能輕鬆地設定這些 Monolog 的日誌處理程式,可以混合使用不同處理程式來為我們的程式處理日誌。
設定
用於設定程式日誌行為的設定選項都放在 config/logging.php
設定檔中。我們可以使用這個檔案來設定專案的日誌通道,因此建議你瞭解一下各個可用的通道與其對應的選項。稍後我們會來討論一些常見的選項。
預設情況下,Laravel 會使用 stack
通道來紀錄日誌訊息。stack
通道可用來將多個日誌通道彙總到單一通道內。有關建立 Stack 的詳細資訊,請參考下方的說明文件。
設定通道名稱
預設情況下,在初始化 Monologo 時,會使用目前環境的名稱來作為「通道名稱」,如 production
或 local
。若要更改通道名稱,請在通道設定中加上 name
選項:
1'stack' => [2 'driver' => 'stack',3 'name' => 'channel-name',4 'channels' => ['single', 'slack'],5],
1'stack' => [2 'driver' => 'stack',3 'name' => 'channel-name',4 'channels' => ['single', 'slack'],5],
可用的通道 Driver
每個日誌通道都以一個「Driver」驅動。Driver 會判斷要如何紀錄日誌訊息、以及要將日誌訊息紀錄到哪裡。在所有的 Laravel 應用程式中都可使用下列日誌通道 Driver。在你專案中的 config/logging.php
設定檔內已經預先填好下表中大部分的 Driver,因此我們建議你可瞭解一下這個檔案以熟悉其內容:
名稱 | 說明 |
---|---|
custom | 會呼叫特定 Factory 建立通道的 Driver |
daily | 會每日重置之一個基於 RotatingFileHandler 的 Monolog Driver |
errorlog | 基於 ErrorLogHandler 的 Monolog Driver |
monolog | 可使用任意支援的 Monolog Handler 之 Monolog Factory Driver |
papertrail | 基於 SyslogUdpHandler 的 Monolog Driver |
single | 基於單一檔案或路徑的 Logger 通道 (StreamHandler ) |
slack | 基於 SlackWebhookHandler 的 Monolog Driver |
stack | 會建立「多通道」通道的包裝 |
syslog | 基於 SyslogHandler 的 Monolog Driver |
請閱讀進階的通道客製化以瞭解更多有關 monolog
與 custom
Driver 的資訊。
通道的前置需求
設定 Single 與 Daily 通道
single
(單一) 與 daily
(每日) 通道有三個可選的設定選項:bubble
、permission
、locking
。
名稱 | 說明 | 預設 |
---|---|---|
bubble | 代表該訊息被處理後是否應向上傳遞給其他通道 | true |
locking | 在寫入 Log 檔前嘗試鎖定該檔案 | false |
permission | Log 檔的權限 | 0644 |
此外,也可以使用 days
選項來設定 daily
通道的保留政策:
名稱 | 說明 | 預設 |
---|---|---|
days | 要保留的每日日誌檔天數 | 7 |
設定 Papertrail 通道
papertrail
通道有 host
與 port
兩個必填的設定選項。可以從 Papertrail 上取得這些值。
設定 Slack 通道
slack
通道有一個 url
必填設定選項。該 URL 應為在 Slack 團隊上設定之傳入的 Webhook URL。
預設情況下,Slack 只會接收等級為 critical
或以上的日誌。不過,也可以在 config/logging.php
設定檔中更改 Slack 日誌通道的 level
選項來調整要接收的等級。
紀錄 Deprecation Warning
PHP、Laravel、或是其他函式庫等,通常會通知使用者其部分功能已棄用,且將在未來的版本中移除這些功能。若想收到這些棄用警告,可在 config/logging.php
設定檔中設定要用於記錄 deprecations
日誌的通道:
1'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),23'channels' => [4 ...5]
1'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),23'channels' => [4 ...5]
或者,也可以定義一個名為 deprecations
的日誌通道。若有該名稱的通道,Laravel 會使用該通道來紀錄 Deprecation 日誌:
1'channels' => [2 'deprecations' => [3 'driver' => 'single',4 'path' => storage_path('logs/php-deprecation-warnings.log'),5 ],6],
1'channels' => [2 'deprecations' => [3 'driver' => 'single',4 'path' => storage_path('logs/php-deprecation-warnings.log'),5 ],6],
建立日誌 Stack
剛才也提到過,stack
Driver 能讓我們將多個通道組合為單一日誌通道來更方便地使用。為了說明如何使用日誌的 Stack,我們先來看看下面這個可能出現在正式專案中的範例設定檔:
1'channels' => [2 'stack' => [3 'driver' => 'stack',4 'channels' => ['syslog', 'slack'],5 ],67 'syslog' => [8 'driver' => 'syslog',9 'level' => 'debug',10 ],1112 'slack' => [13 'driver' => 'slack',14 'url' => env('LOG_SLACK_WEBHOOK_URL'),15 'username' => 'Laravel Log',16 'emoji' => ':boom:',17 'level' => 'critical',18 ],19],
1'channels' => [2 'stack' => [3 'driver' => 'stack',4 'channels' => ['syslog', 'slack'],5 ],67 'syslog' => [8 'driver' => 'syslog',9 'level' => 'debug',10 ],1112 'slack' => [13 'driver' => 'slack',14 'url' => env('LOG_SLACK_WEBHOOK_URL'),15 'username' => 'Laravel Log',16 'emoji' => ':boom:',17 'level' => 'critical',18 ],19],
讓我們來逐步分析這個設定檔。首先,可以注意到 stack
通道使用 channels
選項來彙總了另外兩個通道:syslog
與 slack
。所以,在紀錄日誌訊息時,這兩個頻道都可能會去紀錄該訊息。不過,我們稍後會看到,實際上這兩個通道會依照訊息的嚴重程度 (「等級」) 來判斷是否要紀錄訊息。
日誌的等級
來看看上述範例中 syslog
與 slack
通道設定中的 level
設定。這個選項用來判斷該通道所要紀錄的最小訊息「等級」。Monolog —— 負責提供 Laravel Log 服務的函式庫 —— 提供了所有在 RFC 5424 規格中定義的所有日誌等級。這些 Log 等級按照嚴重程度由重到輕排序分別為:emergency, alert, critical, error, warning, notice, info, 與 debug。
所以,假設我們使用 debug
方法來紀錄訊息:
1Log::debug('An informational message.');
1Log::debug('An informational message.');
在我們的設定檔中,syslog
通道會將該訊息寫到系統日誌中。不過,因為這個訊息不是 critical
或以上的等級,因此這個訊息不會被傳送到 Slack。不過,若我們紀錄 emergency
等級的訊息,則該訊息就會被送到系統日誌與 Slack 兩個地方,因為 emergency
等級大於我們為這兩個通道設定的最小等級門檻:
1Log::emergency('The system is down!');
1Log::emergency('The system is down!');
寫入日誌訊息
可以使用 Log
[Facade] 來將訊息寫入到日誌中。剛才也提到過,日誌程式提供了八個等級,這八個等級定義在 RFC 5424 規格中:emergency, alert, critical, error, warning, notice, info 與 debug。
1use Illuminate\Support\Facades\Log;23Log::emergency($message);4Log::alert($message);5Log::critical($message);6Log::error($message);7Log::warning($message);8Log::notice($message);9Log::info($message);10Log::debug($message);
1use Illuminate\Support\Facades\Log;23Log::emergency($message);4Log::alert($message);5Log::critical($message);6Log::error($message);7Log::warning($message);8Log::notice($message);9Log::info($message);10Log::debug($message);
可以呼叫這些方法來以對應等級紀錄訊息。預設情況下,這些訊息會被寫入到 logging
設定檔中預設的日誌通道中。
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\User;7use Illuminate\Support\Facades\Log;8use Illuminate\View\View;910class UserController extends Controller11{12 /**13 * Show the profile for the given user.14 */15 public function show(string $id): View16 {17 Log::info('Showing the user profile for user: {id}', ['id' => $id]);1819 return view('user.profile', [20 'user' => User::findOrFail($id)21 ]);22 }23}
1<?php23namespace App\Http\Controllers;45use App\Http\Controllers\Controller;6use App\Models\User;7use Illuminate\Support\Facades\Log;8use Illuminate\View\View;910class UserController extends Controller11{12 /**13 * Show the profile for the given user.14 */15 public function show(string $id): View16 {17 Log::info('Showing the user profile for user: {id}', ['id' => $id]);1819 return view('user.profile', [20 'user' => User::findOrFail($id)21 ]);22 }23}
有上下文的資訊
可以傳入一組包含上下文資料的陣列給日誌方法。這些上下文資料會被格式化並與日誌訊息一起顯示:
1use Illuminate\Support\Facades\Log;23Log::info('User {id} failed to login.', ['id' => $user->id]);
1use Illuminate\Support\Facades\Log;23Log::info('User {id} failed to login.', ['id' => $user->id]);
有時候,我們可能會希望在特定通道上,應在接下來的日誌項目中包含一些上下文資訊。舉例來說,我們紀錄能關聯上連入 Request 的 Request ID。為此,可呼叫 Log
Facade 的 withContext
方法:
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Illuminate\Support\Facades\Log;8use Illuminate\Support\Str;9use Symfony\Component\HttpFoundation\Response;1011class AssignRequestId12{13 /**14 * Handle an incoming request.15 *16 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next17 */18 public function handle(Request $request, Closure $next): Response19 {20 $requestId = (string) Str::uuid();2122 Log::withContext([23 'request-id' => $requestId24 ]);2526 $response = $next($request);2728 $response->headers->set('Request-Id', $requestId);2930 return $response;31 }32}
1<?php23namespace App\Http\Middleware;45use Closure;6use Illuminate\Http\Request;7use Illuminate\Support\Facades\Log;8use Illuminate\Support\Str;9use Symfony\Component\HttpFoundation\Response;1011class AssignRequestId12{13 /**14 * Handle an incoming request.15 *16 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next17 */18 public function handle(Request $request, Closure $next): Response19 {20 $requestId = (string) Str::uuid();2122 Log::withContext([23 'request-id' => $requestId24 ]);2526 $response = $next($request);2728 $response->headers->set('Request-Id', $requestId);2930 return $response;31 }32}
若想在 所有 日誌通道上共享上下文資訊,可呼叫 Log::shareContext()
方法。該方法會將上下文資訊提供給所有已建立的通道,以及接下來所建立的任何通道。一般來說,應在專案的某個 Service Provider 中呼叫 shareContext
方法:
1use Illuminate\Support\Facades\Log;2use Illuminate\Support\Str;34class AppServiceProvider5{6 /**7 * Bootstrap any application services.8 */9 public function boot(): void10 {11 Log::shareContext([12 'invocation-id' => (string) Str::uuid(),13 ]);14 }15}
1use Illuminate\Support\Facades\Log;2use Illuminate\Support\Str;34class AppServiceProvider5{6 /**7 * Bootstrap any application services.8 */9 public function boot(): void10 {11 Log::shareContext([12 'invocation-id' => (string) Str::uuid(),13 ]);14 }15}
寫入指定的通道
有時候,我們可能會想將訊息寫到預設通道以外的其他通道。可以使用 Log
Facade 上的 channel
方法來取得並紀錄到設定檔中定義的任何通道:
1use Illuminate\Support\Facades\Log;23Log::channel('slack')->info('Something happened!');
1use Illuminate\Support\Facades\Log;23Log::channel('slack')->info('Something happened!');
若想視需要建立由多個通道組合成的日誌 Stack,可使用 stack
方法:
1Log::stack(['single', 'slack'])->info('Something happened!');
1Log::stack(['single', 'slack'])->info('Something happened!');
視需要建立的通道
也可以在不寫入設定檔的情況下在執行階段視需要建立通道。要建立視需要時建立的通道,請傳入一個設定用陣列給 Log
Facade 的 build
方法:
1use Illuminate\Support\Facades\Log;23Log::build([4 'driver' => 'single',5 'path' => storage_path('logs/custom.log'),6])->info('Something happened!');
1use Illuminate\Support\Facades\Log;23Log::build([4 'driver' => 'single',5 'path' => storage_path('logs/custom.log'),6])->info('Something happened!');
也可以在視需要建立的日誌 Stack 中包含一個視需要建立的通道。只要在傳給 stack
方法的陣列中包含一個視需要建立的通道實體即可:
1use Illuminate\Support\Facades\Log;23$channel = Log::build([4 'driver' => 'single',5 'path' => storage_path('logs/custom.log'),6]);78Log::stack(['slack', $channel])->info('Something happened!');
1use Illuminate\Support\Facades\Log;23$channel = Log::build([4 'driver' => 'single',5 'path' => storage_path('logs/custom.log'),6]);78Log::stack(['slack', $channel])->info('Something happened!');
自訂 Monolog 通道
為通道自訂 Monolog
有時候我們會因為現有通道而需要完整控制 Monolog 的設定方式。舉例來說,我們可能需要為 Laravel 的內建 single
通道設定自訂的 Monolog FormatterInterface
實作。
要開始自訂 Monolog,請在通道設定中定義一個 tap
陣列。tap
陣列中應包含一組要用來在 Monolog 實體建立完畢後自訂 (或「監聽」進) Monologo 實體的類別。對於要將這些類別放在哪裡,Laravel 並沒有相關規範。因此,我們可以隨意在專案內建立目錄來放置這些類別:
1'single' => [2 'driver' => 'single',3 'tap' => [App\Logging\CustomizeFormatter::class],4 'path' => storage_path('logs/laravel.log'),5 'level' => 'debug',6],
1'single' => [2 'driver' => 'single',3 'tap' => [App\Logging\CustomizeFormatter::class],4 'path' => storage_path('logs/laravel.log'),5 'level' => 'debug',6],
在通道上設定好 tap
選項後,就可以開始定義用來自訂 Monolog 實體的類別了。這個類別只需要有一個方法即可:__invoke
。該方法會收到 Illuminate\Log\Logger
實體,該實體會將所有的方法呼叫代理到底層的 Monolog 實體:
1<?php23namespace App\Logging;45use Illuminate\Log\Logger;6use Monolog\Formatter\LineFormatter;78class CustomizeFormatter9{10 /**11 * Customize the given logger instance.12 */13 public function __invoke(Logger $logger): void14 {15 foreach ($logger->getHandlers() as $handler) {16 $handler->setFormatter(new LineFormatter(17 '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'18 ));19 }20 }21}
1<?php23namespace App\Logging;45use Illuminate\Log\Logger;6use Monolog\Formatter\LineFormatter;78class CustomizeFormatter9{10 /**11 * Customize the given logger instance.12 */13 public function __invoke(Logger $logger): void14 {15 foreach ($logger->getHandlers() as $handler) {16 $handler->setFormatter(new LineFormatter(17 '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'18 ));19 }20 }21}
所有的「Tap」類別都會由 Service Container 解析,所以在 Constructor 中要求的相依性都會自動被插入。
建立 Monolog Handler 通道
Monolog 中有多個可用的 Handler,Laravel 並未為每個 Handler 都提供一個內建的通道。在某些情況下,我們可能會想給一些沒有對應 Laravel 日誌 Driver 的 Monolog Handler 建立實體作為自訂通道。只要使用 monolog
Driver 就可以輕鬆地建立這類通道。
使用 monolog
Driver 時,handler
設定選項可用來指定要初始化哪個 Handler。然後,也可以選擇性地使用 with
設定選項來指定該 Handler 的 Constructor 所需要的參數:
1'logentries' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\SyslogUdpHandler::class,4 'with' => [5 'host' => 'my.logentries.internal.datahubhost.company.com',6 'port' => '10000',7 ],8],
1'logentries' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\SyslogUdpHandler::class,4 'with' => [5 'host' => 'my.logentries.internal.datahubhost.company.com',6 'port' => '10000',7 ],8],
Monolog 格式
使用 monolog
Driver 時,會使用 Monolog 的 LineFormatter
來作為預設的格式化工具。不過,我們也可以使用 formatter
與 formatter_with
設定選項來自訂要傳給該 Handler 的格式化工具:
1'browser' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\BrowserConsoleHandler::class,4 'formatter' => Monolog\Formatter\HtmlFormatter::class,5 'formatter_with' => [6 'dateFormat' => 'Y-m-d',7 ],8],
1'browser' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\BrowserConsoleHandler::class,4 'formatter' => Monolog\Formatter\HtmlFormatter::class,5 'formatter_with' => [6 'dateFormat' => 'Y-m-d',7 ],8],
若使用的 Monolog Handler 本身就有提供格式化工具,則可以將 formatter
設定選項設為 default
:
1'newrelic' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\NewRelicHandler::class,4 'formatter' => 'default',5],
1'newrelic' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\NewRelicHandler::class,4 'formatter' => 'default',5],
Monolog Processor
Monolog 也可以在訊息被寫入 Log 前先處理訊息。你可以自行建立 Processor,或是使用 Monolog 提供的現有 Processor。
若想為 monolog
Driver 自定 Processor,請在 Channel 的設定中新增 processors
設定值:
1'memory' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\StreamHandler::class,4 'with' => [5 'stream' => 'php://stderr',6 ],7 'processors' => [8 // 簡易語法...9 Monolog\Processor\MemoryUsageProcessor::class,1011 // 包含選項...12 [13 'processor' => Monolog\Processor\PsrLogMessageProcessor::class,14 'with' => ['removeUsedContextFields' => true],15 ],16 ],17],
1'memory' => [2 'driver' => 'monolog',3 'handler' => Monolog\Handler\StreamHandler::class,4 'with' => [5 'stream' => 'php://stderr',6 ],7 'processors' => [8 // 簡易語法...9 Monolog\Processor\MemoryUsageProcessor::class,1011 // 包含選項...12 [13 'processor' => Monolog\Processor\PsrLogMessageProcessor::class,14 'with' => ['removeUsedContextFields' => true],15 ],16 ],17],
使用 Factory 來建立自訂通道
若想定義整個自訂通道來完整控制 Monolog 的初始化與設定,則可在 config/logging.php
設定檔中使用 custom
Driver。設定中應包含一個 via
選項來包含建立 Monolog 實體時要叫用的 Factory 類別名稱:
1'channels' => [2 'example-custom-channel' => [3 'driver' => 'custom',4 'via' => App\Logging\CreateCustomLogger::class,5 ],6],
1'channels' => [2 'example-custom-channel' => [3 'driver' => 'custom',4 'via' => App\Logging\CreateCustomLogger::class,5 ],6],
設定好 custom
Driver 通道後,就可以開始定義用來建立 Monolog 實體的類別了。這個類別只需要有一個 __invoke
方法就好了,該方法應回傳 Monolog Logger的實體。__invoke
方法會收到一個引數,即為該通道的設定陣列:
1<?php23namespace App\Logging;45use Monolog\Logger;67class CreateCustomLogger8{9 /**10 * Create a custom Monolog instance.11 */12 public function __invoke(array $config): Logger13 {14 return new Logger(/* ... */);15 }16}
1<?php23namespace App\Logging;45use Monolog\Logger;67class CreateCustomLogger8{9 /**10 * Create a custom Monolog instance.11 */12 public function __invoke(array $config): Logger13 {14 return new Logger(/* ... */);15 }16}