日誌
簡介
為了協助你瞭解程式中發生的大小事,Laravel 提供了強健的日誌服務,能讓你將訊息紀錄到檔案、系統錯誤日誌、甚至是紀錄到 Slack 中來通知整個團隊。
Laravel 的 Log 紀錄是基於「通道」的。每個通道都代表了一種寫入日誌資訊的特定方法。舉例來說,single
通道將日誌寫進單一日誌檔中,而 slack
通道則將日誌訊息傳送到 Slack。也可以依據日誌的嚴重性來將日誌訊息寫到多個通道。
在 Laravel 內部,我們使用了 Monolog 函式庫。Monolog 提供了多種強大的日誌處理程式。Laravel 還讓我們能輕鬆地設定這些 Monolog 的日誌處理程式,可以混合使用不同處理程式來為我們的程式處理日誌。
設定
All of the configuration options that control your application's logging behavior are housed in the config/logging.php
configuration file. This file allows you to configure your application's log channels, so be sure to review each of the available channels and their options. We'll review a few common options below.
預設情況下,Laravel 會使用 stack
通道來紀錄日誌訊息。stack
通道可用來將多個日誌通道彙總到單一通道內。有關建立 Stack 的詳細資訊,請參考下方的說明文件。
可用的通道 Driver
每個日誌通道都以一個「Driver」驅動。Driver 會判斷要如何紀錄日誌訊息、以及要將日誌訊息紀錄到哪裡。在所有的 Laravel 應用程式中都可使用下列日誌通道 Driver。在你專案中的 config/logging.php
設定檔內已經預先填好下表中大部分的 Driver,因此我們建議你可瞭解一下這個檔案以熟悉其內容:
Configuring the Channel Name
By default, Monolog is instantiated with a "channel name" that matches the current environment, such as production
or local
. To change this value, you may add a name
option to your channel's configuration:
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],
通道的前置需求
Configuring the Single and Daily Channels
single
(單一) 與 daily
(每日) 通道有三個可選的設定選項:bubble
、permission
、locking
。
Configuring the Papertrail Channel
The papertrail
channel requires host
and port
configuration options. These may be defined via the PAPERTRAIL_URL
and PAPERTRAIL_PORT
environment variables. You can obtain these values from Papertrail.
Configuring the Slack Channel
The slack
channel requires a url
configuration option. This value may be defined via the LOG_SLACK_WEBHOOK_URL
environment variable. This URL should match a URL for an incoming webhook that you have configured for your Slack team.
By default, Slack will only receive logs at the critical
level and above; however, you can adjust this using the LOG_LEVEL
environment variable or by modifying the level
configuration option within your Slack log channel's configuration array.
紀錄 Deprecation Warning
PHP, Laravel, and other libraries often notify their users that some of their features have been deprecated and will be removed in a future version. If you would like to log these deprecation warnings, you may specify your preferred deprecations
log channel using the LOG_DEPRECATIONS_CHANNEL
environment variable, or within your application's config/logging.php
configuration file:
1'deprecations' => [2 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),3 'trace' => env('LOG_DEPRECATIONS_TRACE', false),4],56'channels' => [7 // ...8]
1'deprecations' => [2 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),3 'trace' => env('LOG_DEPRECATIONS_TRACE', false),4],56'channels' => [7 // ...8]
或者,也可以定義一個名為 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 'ignore_exceptions' => false,6 ],78 'syslog' => [9 'driver' => 'syslog',10 'level' => env('LOG_LEVEL', 'debug'),11 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),12 'replace_placeholders' => true,13 ],1415 'slack' => [16 'driver' => 'slack',17 'url' => env('LOG_SLACK_WEBHOOK_URL'),18 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),19 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),20 'level' => env('LOG_LEVEL', 'critical'),21 'replace_placeholders' => true,22 ],23],
1'channels' => [2 'stack' => [3 'driver' => 'stack',4 'channels' => ['syslog', 'slack'],5 'ignore_exceptions' => false,6 ],78 'syslog' => [9 'driver' => 'syslog',10 'level' => env('LOG_LEVEL', 'debug'),11 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),12 'replace_placeholders' => true,13 ],1415 'slack' => [16 'driver' => 'slack',17 'url' => env('LOG_SLACK_WEBHOOK_URL'),18 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),19 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),20 'level' => env('LOG_LEVEL', 'critical'),21 'replace_placeholders' => true,22 ],23],
讓我們來逐步分析這個設定檔。首先,可以注意到 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}
If you would like to share contextual information across all logging channels, you may invoke the Log::shareContext()
method. This method will provide the contextual information to all created channels and any channels that are created subsequently:
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::shareContext([23 'request-id' => $requestId24 ]);2526 // ...27 }28}
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::shareContext([23 'request-id' => $requestId24 ]);2526 // ...27 }28}
If you need to share log context while processing queued jobs, you may utilize job middleware.
Writing to Specific Channels
有時候,我們可能會想將訊息寫到預設通道以外的其他通道。可以使用 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 通道
Customizing Monolog for Channels
有時候我們會因為現有通道而需要完整控制 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' => env('LOG_LEVEL', 'debug'),6 'replace_placeholders' => true,7],
1'single' => [2 'driver' => 'single',3 'tap' => [App\Logging\CustomizeFormatter::class],4 'path' => storage_path('logs/laravel.log'),5 'level' => env('LOG_LEVEL', 'debug'),6 'replace_placeholders' => true,7],
在通道上設定好 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 has a variety of available handlers and Laravel does not include a built-in channel for each one. In some cases, you may wish to create a custom channel that is merely an instance of a specific Monolog handler that does not have a corresponding Laravel log driver. These channels can be easily created using the 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 // Simple syntax...9 Monolog\Processor\MemoryUsageProcessor::class,1011 // With options...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 // Simple syntax...9 Monolog\Processor\MemoryUsageProcessor::class,1011 // With options...12 [13 'processor' => Monolog\Processor\PsrLogMessageProcessor::class,14 'with' => ['removeUsedContextFields' => true],15 ],16 ],17],
Creating Custom Channels via Factories
若想定義整個自訂通道來完整控制 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}
Tailing Log Messages Using Pail
Often you may need to tail your application's logs in real time. For example, when debugging an issue or when monitoring your application's logs for specific types of errors.
Laravel Pail is a package that allows you to easily dive into your Laravel application's log files directly from the command line. Unlike the standard tail
command, Pail is designed to work with any log driver, including Sentry or Flare. In addition, Pail provides a set of useful filters to help you quickly find what you're looking for.
Installation
To get started, install Pail into your project using the Composer package manager:
1composer require laravel/pail
1composer require laravel/pail
Usage
To start tailing logs, run the pail
command:
1php artisan pail
1php artisan pail
To increase the verbosity of the output and avoid truncation (…), use the -v
option:
1php artisan pail -v
1php artisan pail -v
For maximum verbosity and to display exception stack traces, use the -vv
option:
1php artisan pail -vv
1php artisan pail -vv
To stop tailing logs, press Ctrl+C
at any time.
Filtering Logs
--filter
You may use the --filter
option to filter logs by their type, file, message, and stack trace content:
1php artisan pail --filter="QueryException"
1php artisan pail --filter="QueryException"
--message
To filter logs by only their message, you may use the --message
option:
1php artisan pail --message="User created"
1php artisan pail --message="User created"
--level
The --level
option may be used to filter logs by their log level:
1php artisan pail --level=error
1php artisan pail --level=error
--user
To only display logs that were written while a given user was authenticated, you may provide the user's ID to the --user
option:
1php artisan pail --user=1
1php artisan pail --user=1