iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

我推的Laravel S2!系列 第 16

我推的Laravel S2|Day 15:Logging 日誌系統

  • 分享至 

  • xImage
  •  

一句話破題

blog-app 上線後某個使用者回報「文章存不進去」,你能不能在幾分鐘內從日誌裡找到當時發生了什麼事,往往決定了排查問題要花 5 分鐘還是 5 小時。今天把 Laravel 的日誌系統整理一遍——這個章節在 Laravel 10 到 13 之間幾乎沒有變動,底層都是基於知名的 PHP 日誌函式庫 Monolog。

通道(Channel)的概念

config/logging.php 定義了一組組「通道」,每個通道代表一種日誌輸出方式跟目的地。預設幾種常見通道:

通道 說明
single 全部寫進單一檔案 storage/logs/laravel.log
daily 每天一個檔案,自動輪替,避免單一檔案無限膨脹
slack 直接發送到 Slack 頻道,適合正式環境的重大錯誤即時通知
papertrail 送到 Papertrail 這類第三方日誌託管服務
syslog / errorlog 寫進系統層級的日誌
stack 把多個通道疊加,例如同時寫檔案又發 Slack 通知

Log::info() 呼叫 stack 通道,stack 通道再同時分發給 daily(寫檔案)跟 slack(推送通知)兩個實際輸出通道

.env 裡的 LOG_CHANNEL 決定預設用哪個通道,開發環境通常用 stack(疊加 single),正式環境常見的組合是 daily 疊加 slack(一般日誌寫檔案,緊急錯誤額外通知團隊):

// config/logging.php
'stack' => [
    'driver' => 'stack',
    'channels' => ['daily', 'slack'],
],

daily / single 通道的常用參數

'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'days' => 14,        // 保留最近 14 天的日誌檔案,超過自動清除
    'permission' => 0664, // 產生的日誌檔案權限
],

日誌等級:RFC 5424 的 8 個等級

從最嚴重到最輕微:emergencyalertcriticalerrorwarningnoticeinfodebug。寫日誌時對應呼叫同名方法:

use Illuminate\Support\Facades\Log;

Log::info('文章已建立', ['post_id' => $post->id]);
Log::warning('使用者嘗試存取未發布的文章', ['post_id' => $post->id, 'user_id' => auth()->id()]);
Log::error('儲存文章時發生資料庫錯誤', ['exception' => $e->getMessage()]);

config/logging.php 每個通道可以設定 level,只有等於或高於這個等級的訊息才會實際寫入,例如把正式環境的 slack 通道設成 level: 'critical',避免一般的 info 訊息洗版團隊的 Slack 頻道。

怎麼選對等級:一個實用的判斷準則

8 個等級聽起來多,但實務上多數專案只會用到其中 4-5 個,這裡提供一個簡化的判斷準則:

等級 什麼時候用 blog-app 範例
debug 只在開發階段想看的細節,正式環境通常不記錄 「進入了 PostService::listPosts(),篩選條件是 [...]
info 記錄正常的業務事件,之後可能想統計或查證 「文章 #42 已發布」「使用者 #7 已登入」
warning 不是錯誤,但值得注意的異常狀況 「使用者嘗試存取不存在的文章」「API 呼叫重試了 2 次才成功」
error 明確的錯誤,但應用程式還能繼續運作 「寄送通知信失敗」「呼叫 LINE API 逾時」
critical 以上 需要立刻有人處理的嚴重狀況 「資料庫連線完全失敗」「Queue Worker 全部掛掉」

一個簡單的自我檢查方式:如果這則訊息半夜三點跳出來,會不會有人需要立刻爬起來處理? 會的話至少是 critical;「只是想知道發生過什麼事,不急著處理」通常是 info/warning;「這是我除錯用的,跟正式環境的維運無關」是 debug

附加情境資訊:withContext / shareContext

一次請求裡如果會寫好幾筆日誌,通常會想要有個共同的識別碼,方便事後把同一次請求的所有日誌串起來看:

// app/Http/Middleware 或 Controller 裡
Log::shareContext([
    'request_id' => (string) Str::uuid(),
]);

// 之後這次請求內所有 Log:: 呼叫都會自動帶上 request_id
Log::info('開始處理文章建立請求');
// ...
Log::info('文章建立完成', ['post_id' => $post->id]);

shareContext() 影響全域(所有通道),如果只想讓某一段邏輯的日誌帶有額外上下文、不影響其他地方,用 withContext()

Log::withContext(['post_id' => $post->id])->info('開始處理文章發布流程');

結構化日誌:陣列參數不是裝飾用的

Log::info('文章已建立', ['post_id' => $post->id]) 第二個陣列參數容易被新手忽略、只把訊息塞進第一個字串參數裡(例如寫成 Log::info("文章 #{$post->id} 已建立"))。這是個值得改掉的習慣:把可查詢的資料放進結構化的陣列參數,而不是內嵌進字串,理由是正式環境的日誌通常會被送進 Elasticsearch、Datadog 這類日誌分析平台,這些平台能夠針對陣列裡的欄位(例如 post_id)做索引跟篩選,但沒辦法有效解析內嵌在自由格式字串裡的資料。養成「訊息本身講清楚發生了什麼事,可查詢的細節放進陣列」的習慣,日誌在正式環境的分析價值會差很多。

使用特定通道,不影響全域預設

Log::channel('slack')->critical('資料庫連線失敗!');

// 動態組一個一次性通道,不需要事先寫進 config/logging.php
Log::build([
    'driver' => 'single',
    'path' => storage_path('logs/payment.log'),
])->info('收到金流 Webhook', $payload);

Deprecation Warnings:追蹤即將棄用的用法

Laravel 或第三方套件升級時,有些寫法會先進入「已棄用但仍可用」的過渡期,PHP 本身也會對已棄用的語法發出警告。這些警告預設會跟一般日誌混在一起,Laravel 提供獨立的通道分開處理:

LOG_DEPRECATIONS_CHANNEL=deprecations
// config/logging.php
'deprecations' => [
    'driver' => 'single',
    'path' => storage_path('logs/deprecations.log'),
],

設定好之後,棄用警告會單獨寫進 deprecations.log,不會淹沒在一般的應用程式日誌裡,這對於評估「升級到下一個 Laravel 大版本前,我的專案還有哪些地方用了已棄用的寫法」特別有幫助——升級前先跑一輪測試、看看這個檔案累積了哪些警告,是很實用的升級前檢查手段。

建立自訂 Monolog 通道

如果內建的通道類型不夠用(例如想串接公司內部的日誌收集系統),可以自訂:

// config/logging.php
'custom' => [
    'driver' => 'custom',
    'via' => App\Logging\CreateCustomLogger::class,
],
// app/Logging/CreateCustomLogger.php
class CreateCustomLogger
{
    public function __invoke(array $config): Logger
    {
        return new Logger('custom', [
            new CustomHandler($config['level'] ?? 'debug'),
        ]);
    }
}

多數專案不會走到需要自訂 Handler 這一步——內建的 daily/slack/papertrail 加上第三方服務(如 Sentry、Datadog)提供的官方 Laravel 整合套件,已經能覆蓋絕大多數需求,自訂 Monolog 通道適合真的有特殊整合需求時再考慮。

串接第三方錯誤追蹤服務:以 Sentry 為例

實務上多數團隊不會只靠自己寫的 Log::error() 排查正式環境問題,而是搭配 Sentry、Bugsnag 這類專門的錯誤追蹤服務——它們除了記錄錯誤,還能自動蒐集完整的堆疊追蹤、發生當下的環境資訊,並在同類型錯誤重複發生時自動群組化,避免同一個問題的上千次錯誤淹沒你的收件匣。以 Sentry 為例,整合方式大致是:

composer require sentry/sentry-laravel
php artisan sentry:publish --dsn=your-dsn-here

安裝完成後,Sentry 會自動掛進 Laravel 的例外處理機制(Day 18 會深入 withExceptions()),未被攔截的例外會自動上報,不需要每個地方手動呼叫。這類服務通常也提供效能監控(追蹤慢查詢、慢請求)的附加功能,blog-app 這種規模的專案在正式上線前,導入其中一套錯誤追蹤服務通常是投資報酬率很高的一步——比起只靠翻閱 log 檔案,你能更快知道「使用者剛剛遇到了什麼」。

開發階段即時查看日誌:Laravel Pail

正式環境用 tail -f storage/logs/laravel.log 追蹤日誌是老方法,本機開發階段,Laravel 官方提供一個更好用的工具:

composer require laravel/pail --dev
php artisan pail

pail 會即時把日誌串流輸出到終端機,並且提供顏色高亮、篩選功能(可以只看特定等級或特定關鍵字),比原始的 tail -f 好讀得多,也不需要自己記日誌檔案的完整路徑。開發 blog-app 時,開一個終端機視窗跑 php artisan pail,另一個視窗跑 php artisan serve,能即時看到每個請求觸發了哪些日誌,對抓 bug 很有幫助。

測試中驗證日誌行為

Day 27 會深入測試,這裡先提一個跟日誌相關的實用工具:測試某段邏輯「有沒有正確記錄日誌」時,不需要真的去讀日誌檔案內容:

use Illuminate\Support\Facades\Log;

test('文章刪除時會記錄日誌', function () {
    Log::spy();

    $post = Post::factory()->create();
    $post->delete();

    Log::shouldHaveReceived('info')
        ->once()
        ->with(\Mockery::pattern('/已被刪除/'));
});

這裡的 Log::spy() 是 Laravel 通用的「Facade 間諜」機制——所有 Facade 都支援 spy(),呼叫之後框架會把容器裡對應的綁定換成一個會記錄所有互動的替身(Day 23 講 Facade 與容器解析時會解釋為什麼做得到),之後就能用 Mockery 的 shouldHaveReceived() 斷言「這段程式碼確實呼叫過 Log::info()」,不需要真的產生日誌檔案再去解析內容。

注意 Log 沒有 fake() 方法——Queue::fake()Mail::fake()Event::fake() 這些是各自 Facade 專門提供的假實作(連帶提供 assertPushed()assertSent() 這類專屬斷言),Log 不在這個行列裡,測試日誌一律走 spy()expects() 這條通用路線:

// 另一種寫法:用 expects() 事先宣告預期,沒被呼叫到測試就會失敗
Log::expects('info')->with('文章已建立', Mockery::type('array'));

這正是 Day 9 建立的 PostObserver::deleted() 那段日誌記錄邏輯,之後寫測試時可以直接套用的驗證方式。

常見錯誤與踩雷點

  • 把敏感資料直接寫進日誌Log::info('使用者登入', ['password' => $request->password]) 這種寫法會讓密碼明文留在日誌檔案裡,記錄使用者相關資訊時要有意識地排除密碼、信用卡號、Token 等敏感欄位。
  • 正式環境忘記設定 days 導致日誌檔案無限累積daily 通道如果沒設 days,日誌檔案會一直累積下去,長期下來可能把磁碟空間吃光,記得依專案需求設定合理的保留天數。
  • 在高頻迴圈裡寫 debug 等級日誌卻忘記關閉:正式環境如果 LOG_LEVEL 設得太低(例如 debug),高頻執行的程式碼路徑會產生大量日誌,除了效能影響,也會讓真正重要的錯誤訊息淹沒在雜訊裡。
  • 把可查詢的資料塞進訊息字串,而不是結構化陣列參數:前面提過的結構化日誌習慣,如果所有細節都內嵌進字串,正式環境用日誌分析平台做篩選/統計時會非常困難。
  • 正式環境沒有導入任何錯誤追蹤服務,只靠翻 log 檔案排查問題:小型專案初期這樣做沒問題,但隨著使用者變多,翻 log 檔案的效率會明顯跟不上問題出現的速度,及早導入 Sentry 這類服務能省下大量排查時間。

小結

日誌系統這塊在 Laravel 10 到 13 間相當穩定,今天重點在建立正確的使用習慣:善用通道分流不同嚴重程度的訊息、用 shareContext 串起同一次請求的日誌、寫結構化日誌而不是把資料塞進字串、留意敏感資料不要外洩進日誌檔案。也認識了 Pail 這個開發階段即時查看日誌的工具,以及 Sentry 這類第三方錯誤追蹤服務在正式環境的價值。

好記性不如爛筆頭 — 中國諺語

明日預告

Day 16 進入這系列另一個重大版本更新:Middleware 的註冊方式全面改寫,加上 Laravel 13 把 CSRF 中介層改名並強化了防護機制。


上一篇
我推的Laravel S2|Day 14:RESTful API 設計:REST 原則、實例、Resource/Collection、JSON:API
下一篇
我推的Laravel S2|Day 16:Middleware 與請求偽造防護新篇章:bootstrap/app.php、CSRF 改名
系列文
我推的Laravel S2!18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言