日誌

簡介

為了協助你瞭解程式中發生的大小事,Laravel 提供了強健的日誌(Log)服務,能讓你將訊息紀錄到檔案、系統錯誤日誌(Error Log)、甚至是紀錄到 Slack 中來通知整個團隊。

Laravel 的 Log 紀錄是基於「通道(Channel)」的。每個通道都代表了一種寫入日誌資訊的特定方法。舉例來說,single 通道將日誌寫進單一日誌檔中,而 slack 通道則將日誌訊息傳送到 Slack。也可以依據日誌的嚴重性(Severity)來將日誌訊息寫到多個通道。

在 Laravel 內部,我們使用了 Monolog 函式庫。Monolog 提供了多種強大的日誌處理程式(Handler)。Laravel 還讓我們能輕鬆地設定這些 Monolog 的日誌處理程式,可以混合使用不同處理程式來為我們的程式處理日誌。

設定

用於設定程式日誌行為的設定選項都放在 config/logging.php 設定檔中。我們可以使用這個檔案來設定專案的日誌通道,因此建議你瞭解一下各個可用的通道與其對應的選項。稍後我們會來討論一些常見的選項。

預設情況下,Laravel 會使用 stack 通道來紀錄日誌訊息。stack 通道可用來將多個日誌通道彙總到單一通道內。有關建立 Stack 的詳細資訊,請參考下方的說明文件

設定通道名稱

預設情況下,在初始化 Monologo 時,會使用目前環境的名稱來作為「通道名稱」,如 productionlocal。若要更改通道名稱,請在通道設定中加上 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會每日重置(Rotate)之一個基於 RotatingFileHandler 的 Monolog Driver
errorlog基於 ErrorLogHandler 的 Monolog Driver
monolog可使用任意支援的 Monolog Handler 之 Monolog Factory Driver
null會取消所有 Log 訊息的 Driver
papertrail基於 SyslogUdpHandler 的 Monolog Driver
single基於單一檔案或路徑的 Logger 通道 (StreamHandler)
slack基於 SlackWebhookHandler 的 Monolog Driver
stack會建立「多通道」通道的包裝
syslog基於 SyslogHandler 的 Monolog Driver
lightbulb

請閱讀進階的通道客製化以瞭解更多有關 monologcustom Driver 的資訊。

通道的前置需求

設定 Single 與 Daily 通道

single (單一) 與 daily (每日) 通道有三個可選的設定選項:bubblepermissionlocking

名稱說明預設
bubble代表該訊息被處理後是否應向上傳遞(Bubble)給其他通道true
locking在寫入 Log 檔前嘗試鎖定該檔案false
permissionLog 檔的權限0644

此外,也可以使用 days 選項來設定 daily 通道的保留政策:

名稱說明預設
days要保留的每日日誌檔天數7

設定 Papertrail 通道

papertrail 通道有 hostport 兩個必填的設定選項。可以從 Papertrail 上取得這些值。

設定 Slack 通道

slack 通道有一個 url 必填設定選項。該 URL 應為在 Slack 團隊上設定之傳入的 Webhook URL。

預設情況下,Slack 只會接收等級為 critical 或以上的日誌。不過,也可以在 config/logging.php 設定檔中更改 Slack 日誌通道的 level 選項來調整要接收的等級。

紀錄 Deprecation Warning

PHP、Laravel、或是其他函式庫等,通常會通知使用者其部分功能已棄用(Deprecated),且將在未來的版本中移除這些功能。若想收到這些棄用警告,可在 config/logging.php 設定檔中設定要用於記錄 deprecations 日誌的通道:

1'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
2 
3'channels' => [
4 ...
5]
1'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
2 
3'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 ],
6 
7 'syslog' => [
8 'driver' => 'syslog',
9 'level' => 'debug',
10 ],
11 
12 '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 ],
6 
7 'syslog' => [
8 'driver' => 'syslog',
9 'level' => 'debug',
10 ],
11 
12 '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 選項來彙總了另外兩個通道:syslogslack。所以,在紀錄日誌訊息時,這兩個頻道都可能會去紀錄該訊息。不過,我們稍後會看到,實際上這兩個通道會依照訊息的嚴重程度 (「等級(Level)」) 來判斷是否要紀錄訊息。

日誌的等級

來看看上述範例中 syslogslack 通道設定中的 level 設定。這個選項用來判斷該通道所要紀錄的最小訊息「等級(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 通道會將該訊息寫到系統日誌(System Log)中。不過,因為這個訊息不是 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, infodebug

1use Illuminate\Support\Facades\Log;
2 
3Log::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;
2 
3Log::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<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use App\Models\User;
7use Illuminate\Support\Facades\Log;
8use Illuminate\View\View;
9 
10class UserController extends Controller
11{
12 /**
13 * Show the profile for the given user.
14 */
15 public function show(string $id): View
16 {
17 Log::info('Showing the user profile for user: '.$id);
18 
19 return view('user.profile', [
20 'user' => User::findOrFail($id)
21 ]);
22 }
23}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use App\Models\User;
7use Illuminate\Support\Facades\Log;
8use Illuminate\View\View;
9 
10class UserController extends Controller
11{
12 /**
13 * Show the profile for the given user.
14 */
15 public function show(string $id): View
16 {
17 Log::info('Showing the user profile for user: '.$id);
18 
19 return view('user.profile', [
20 'user' => User::findOrFail($id)
21 ]);
22 }
23}

有上下文的資訊

可以傳入一組包含上下文資料(Contextual Data)的陣列給日誌方法。這些上下文資料會被格式化並與日誌訊息一起顯示:

1use Illuminate\Support\Facades\Log;
2 
3Log::info('User failed to login.', ['id' => $user->id]);
1use Illuminate\Support\Facades\Log;
2 
3Log::info('User failed to login.', ['id' => $user->id]);

有時候,我們可能會希望在特定通道上,應在接下來的日誌項目中包含一些上下文資訊。舉例來說,我們紀錄能關聯上連入 Request 的 Request ID。為此,可呼叫 Log Facade 的 withContext 方法:

1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Illuminate\Support\Facades\Log;
8use Illuminate\Support\Str;
9use Symfony\Component\HttpFoundation\Response;
10 
11class AssignRequestId
12{
13 /**
14 * Handle an incoming request.
15 *
16 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
17 */
18 public function handle(Request $request, Closure $next): Response
19 {
20 $requestId = (string) Str::uuid();
21 
22 Log::withContext([
23 'request-id' => $requestId
24 ]);
25 
26 return $next($request)->header('Request-Id', $requestId);
27 }
28}
1<?php
2 
3namespace App\Http\Middleware;
4 
5use Closure;
6use Illuminate\Http\Request;
7use Illuminate\Support\Facades\Log;
8use Illuminate\Support\Str;
9use Symfony\Component\HttpFoundation\Response;
10 
11class AssignRequestId
12{
13 /**
14 * Handle an incoming request.
15 *
16 * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
17 */
18 public function handle(Request $request, Closure $next): Response
19 {
20 $requestId = (string) Str::uuid();
21 
22 Log::withContext([
23 'request-id' => $requestId
24 ]);
25 
26 return $next($request)->header('Request-Id', $requestId);
27 }
28}

若想在 所有 日誌通道上共享上下文資訊,可呼叫 Log::shareContext() 方法。該方法會將上下文資訊提供給所有已建立的通道,以及接下來所建立的任何通道。一般來說,應在專案的某個 Service Provider 中呼叫 shareContext 方法:

1use Illuminate\Support\Facades\Log;
2use Illuminate\Support\Str;
3 
4class AppServiceProvider
5{
6 /**
7 * Bootstrap any application services.
8 */
9 public function boot(): void
10 {
11 Log::shareContext([
12 'invocation-id' => (string) Str::uuid(),
13 ]);
14 }
15}
1use Illuminate\Support\Facades\Log;
2use Illuminate\Support\Str;
3 
4class AppServiceProvider
5{
6 /**
7 * Bootstrap any application services.
8 */
9 public function boot(): void
10 {
11 Log::shareContext([
12 'invocation-id' => (string) Str::uuid(),
13 ]);
14 }
15}

寫入指定的通道

有時候,我們可能會想將訊息寫到預設通道以外的其他通道。可以使用 Log Facade 上的 channel 方法來取得並紀錄到設定檔中定義的任何通道:

1use Illuminate\Support\Facades\Log;
2 
3Log::channel('slack')->info('Something happened!');
1use Illuminate\Support\Facades\Log;
2 
3Log::channel('slack')->info('Something happened!');

若想視需要建立由多個通道組合成的日誌 Stack,可使用 stack 方法:

1Log::stack(['single', 'slack'])->info('Something happened!');
1Log::stack(['single', 'slack'])->info('Something happened!');

視需要建立的通道

也可以在不寫入設定檔的情況下在執行階段(Runtime)視需要建立通道。要建立視需要時建立的通道,請傳入一個設定用陣列給 Log Facade 的 build 方法:

1use Illuminate\Support\Facades\Log;
2 
3Log::build([
4 'driver' => 'single',
5 'path' => storage_path('logs/custom.log'),
6])->info('Something happened!');
1use Illuminate\Support\Facades\Log;
2 
3Log::build([
4 'driver' => 'single',
5 'path' => storage_path('logs/custom.log'),
6])->info('Something happened!');

也可以在視需要建立的日誌 Stack 中包含一個視需要建立的通道。只要在傳給 stack 方法的陣列中包含一個視需要建立的通道實體即可:

1use Illuminate\Support\Facades\Log;
2 
3$channel = Log::build([
4 'driver' => 'single',
5 'path' => storage_path('logs/custom.log'),
6]);
7 
8Log::stack(['slack', $channel])->info('Something happened!');
1use Illuminate\Support\Facades\Log;
2 
3$channel = Log::build([
4 'driver' => 'single',
5 'path' => storage_path('logs/custom.log'),
6]);
7 
8Log::stack(['slack', $channel])->info('Something happened!');

自訂 Monolog 通道

為通道自訂 Monolog

有時候我們會因為現有通道而需要完整控制 Monolog 的設定方式。舉例來說,我們可能需要為 Laravel 的內建 single 通道設定自訂的 Monolog FormatterInterface 實作。

要開始自訂 Monolog,請在通道設定中定義一個 tap 陣列。tap 陣列中應包含一組要用來在 Monolog 實體建立完畢後自訂 (或「監聽(Tap)」進) 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<?php
2 
3namespace App\Logging;
4 
5use Illuminate\Log\Logger;
6use Monolog\Formatter\LineFormatter;
7 
8class CustomizeFormatter
9{
10 /**
11 * Customize the given logger instance.
12 */
13 public function __invoke(Logger $logger): void
14 {
15 foreach ($logger->getHandlers() as $handler) {
16 $handler->setFormatter(new LineFormatter(
17 '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'
18 ));
19 }
20 }
21}
1<?php
2 
3namespace App\Logging;
4 
5use Illuminate\Log\Logger;
6use Monolog\Formatter\LineFormatter;
7 
8class CustomizeFormatter
9{
10 /**
11 * Customize the given logger instance.
12 */
13 public function __invoke(Logger $logger): void
14 {
15 foreach ($logger->getHandlers() as $handler) {
16 $handler->setFormatter(new LineFormatter(
17 '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'
18 ));
19 }
20 }
21}
lightbulb

所有的「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)。不過,我們也可以使用 formatterformatter_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],

使用 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<?php
2 
3namespace App\Logging;
4 
5use Monolog\Logger;
6 
7class CreateCustomLogger
8{
9 /**
10 * Create a custom Monolog instance.
11 */
12 public function __invoke(array $config): Logger
13 {
14 return new Logger(/* ... */);
15 }
16}
1<?php
2 
3namespace App\Logging;
4 
5use Monolog\Logger;
6 
7class CreateCustomLogger
8{
9 /**
10 * Create a custom Monolog instance.
11 */
12 public function __invoke(array $config): Logger
13 {
14 return new Logger(/* ... */);
15 }
16}
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:27:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.