Laravelのログは、設定を一度も触らないまま本番に出しても動いてしまいます。ファイルは勝手に増え、個人情報はそのまま書き込まれ、障害が起きたときに追いたい1リクエストだけが他の数万行に埋もれる状態です。この記事では2026年8月30日時点の公式ドキュメントと配布版スケルトン、それにフレームワーク本体の実装まで突き合わせて、config/logging.phpが何を決めているのか、保持期間の設定キーが12系と13系でどう変わったのか、個人情報をどこで伏せるのかを順に見ていきます。メトリクスの収集と可視化はLaravelでPrometheus+Grafana監視を実装、リクエスト単位の閲覧はLaravel Telescopeの使い方に譲り、ここでは標準の書き出し側だけを扱います。
まとめ:既定値・保持期間・マスキングの判断
- 既定の書き出し先は
stackチャンネルだが、その実体はLOG_STACKの既定値であるsingle1本だけで、日次分割は最初から効いていない。 - チャンネルに書いた
levelは下限のしきい値で、stackにぶら下げた各チャンネルはそれぞれ独立に採否を判定する。 - 日次ローテーションの世代数は12系が
days、13系がmax_files。13系はdaysも後方互換で読むが、どちらも未設定なら7世代に落ちる。 - 個人情報を伏せる場所はMonologのprocessorが原則で、出力の形そのものを変えたいときだけ
tapを併用する。 - ファイル出力を続けるか標準出力に寄せるかは、サーバ台数とログの保存要件で決まる。1台構成なら
dailyのままで足りる。
logging.phpは既定チャンネルとドライバの2階層で決まる
設定ファイルを開くと最初に出てくるのがdefaultキーで、ここに書かれた名前がLogファサードの既定の書き出し先になります。その下のchannels配列に、名前ごとの実体が並ぶという構造です。上の階層で「どれを使うか」、下の階層で「どう書くか」を決めていると考えると迷いません。
既定のLOG_CHANNELはstackでその実体はsingle1本
13.x系の配布版スケルトンは、defaultにenv('LOG_CHANNEL', 'stack')を置いています。stackは自分では何も書かず、channelsに並べた名前へ処理を回すだけの取りまとめ役です。ここで見落とされやすいのが、そのchannelsの中身が固定の配列ではなく環境変数から組み立てられている点になります。
'default' => env('LOG_CHANNEL', 'stack'),
'stack' => [
'driver' => 'stack',
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
配布版の.env.exampleにはLOG_STACK=singleが書かれています。つまり何も変えないまま本番に出すと、storage/logs/laravel.logという1ファイルに全部が追記され続ける状態が既定です。日次で分けたいならLOG_STACK=dailyに変えるだけで切り替わり、複数指定したいときはLOG_STACK=daily,slackのようにカンマ区切りで並べます。日本語の解説記事には「stackの既定はdaily」と書いてあるものが少なくありませんが、少なくとも12系と13系の配布版はどちらもsingleです。
10種のドライバは書き込み先と用途で3つの系統に分けて考える
13.xのドキュメントが挙げるドライバはcustom・daily・monthly・errorlog・monolog・papertrail・single・slack・stack・syslogの10種です。数だけ見ると多く感じますが、書き込み先で束ねると3系統に収まります。ディスクのファイルに書くsingle・daily・monthly、OSやプロセスの標準機構に渡すsyslog・errorlog、そして外部サービスへ送るslack・papertrailの3つです。残るmonologとcustomは、Monolog側の任意のハンドラを持ち込むための入口にあたります。
配布版のスケルトンには、この10種のほかにstderr・null・emergencyという3つのチャンネル定義も同梱されています。stderrはmonologドライバでStreamHandlerを標準エラー出力に向けたもので、コンテナ運用のときにそのまま使えます。nullは捨て先で、LOG_DEPRECATIONS_CHANNELの既定値がこれになっているため、非推奨警告は初期状態では記録されません。
単発の書き込み先はLog::channelとLog::buildで変える
既定チャンネル以外に書きたい場面では、その場で行き先を指定できます。設定ファイルに定義済みの名前を使うならLog::channel()、その場限りの組み合わせならLog::stack()、設定ファイルに存在しない構成を実行時に組み立てるならLog::build()という使い分けです。
Log::channel('slack')->info('Something happened!');
Log::stack(['single', 'slack'])->info('Something happened!');
$channel = Log::build([
'driver' => 'single',
'path' => storage_path('logs/custom.log'),
]);
Log::stack(['slack', $channel])->info('Something happened!');
バッチ処理の結果だけを別ファイルに分けたい、といった要件はこのLog::build()で足ります。専用チャンネルを増やすほどでもない一時的な切り分けに向きます。
ログレベルの割り当てとstackの積み方で本番環境の通知量が決まる
どのチャンネルにも書けるlevelキーは、そのチャンネルが受け付ける下限を指します。ここを雑に決めると、通知チャンネルが鳴りやまないか、逆に肝心の障害が届かないかのどちらかに振れます。
RFC 5424の8段階は本番運用では3層に丸めて割り当てる
Monologが持つレベルはRFC 5424に沿った8段階で、深刻な順にemergency・alert・critical・error・warning・notice・info・debugと並びます。8段階すべてに別々の意味を持たせて運用しきるのは難しく、実務では3層に丸めるほうが機能します。人を起こす層をerror以上、あとで追える形で残す層をinfo以上、開発中しか見ない層をdebugとする割り当てです。
この3層をチャンネルに写すと、通知先はlevelをerrorに、保存先はinfoに、ローカル用はdebugに設定することになります。配布版がslackチャンネルのlevelをcriticalにしているのは通知を絞る意図ですが、criticalまで上げると多くのアプリで何も鳴りません。未捕捉の例外はハンドラ経由でerrorとして記録されるため、通知の入口はerrorに置くほうが実態に合います。
levelは下限値でstack配下の各チャンネルが独立に採否を判定する
stackにぶら下げたチャンネルは、渡ってきた1件のレコードを各自で判定します。親が通したから子も書く、という連鎖ではありません。syslogをdebug、slackをerrorにしておけば、Log::debug()はsyslogにだけ残り、Log::emergency()は両方に届きます。1本の呼び出しで宛先を出し分けられるのがstackの利点です。
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'max_files' => 30,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'level' => 'error',
],
ignore_exceptionsとbubbleの既定値が事故を生む場面
配布版のstackはignore_exceptionsをfalseにしています。ログの書き込み中に例外が出たとき、それを握りつぶさない設定です。Slackのwebhookが落ちたりログディレクトリの権限が足りなかったりすると、その例外がアプリ側へ跳ね返り、ログを出そうとして本体が500を返します。外部サービスをstackに入れるなら、ここをtrueにして書き込み失敗を切り離すか、外部への送信そのものを非同期に逃がすかを先に決めておきます。
single・daily・monthlyにはbubble・locking・permissionの3つの任意オプションがあり、既定はそれぞれtrue・false・0644です。bubbleがtrueのままだと、そのチャンネルが処理したレコードは後続のハンドラにも回ります。同じ内容を2箇所に書いてしまっているときは、まずここを疑ってください。
日次ローテーションの設定キーはLaravel 12系と13系で名前が違う
ログファイルの世代管理は、この記事で扱う中でもっとも版差が出た部分です。アップグレード時に古いconfig/logging.phpをそのまま持ち込むと、意図と違う世代数で回り続けます。
12系はdaysで13系はmax_filesになりmonthlyが増えた
12.x系の配布版はdailyチャンネルにdaysキーを置き、LOG_DAILY_DAYSの既定値14を与えていました。13.x系ではこれがmax_filesに変わり、あわせて月次で分割するmonthlyチャンネルが追加されています。環境変数名はLOG_DAILY_DAYSのまま据え置かれているので、.envだけ見ていると差分に気づけません。
フレームワーク本体の実装まで下りると、13.xのLogManagerにあるcreateDailyDriver()は世代数をmax_files、days、直値の7という順で解決しています。daysも後方互換で読まれるため、12系の設定を持ち込んでも保持期間が消えるわけではありません。ただし両方のキーが無い場合のフォールバックは7で、配布版の14はあくまで設定ファイル側が与えている値です。チャンネル定義を自作してpathとlevelだけ書いたときに、意図せず7世代で切られるのはこの分岐が理由になります。
Laravelの版ごとに保持期間の設定キーと既定値を突き合わせる
| 項目 | Laravel 12.x | Laravel 13.x |
|---|---|---|
| dailyの世代数キー | days | max_files(daysも可) |
| 配布版configの既定 | 14 | 14 |
| 環境変数 | LOG_DAILY_DAYS | LOG_DAILY_DAYS |
| キー未設定時の値 | 7 | 7 |
| monthlyチャンネル | なし | あり(既定3世代) |
| ドライバ総数 | 9種 | 10種 |
実機で確かめるなら、php artisan tinkerからconfig('logging.channels.daily')を叩いて解決後の配列を見るのが早いです。設定をキャッシュ済みの本番でも、その値がそのまま返ります。
permissionとlockingはコンテナ運用で先に決めておく
ファイル権限の既定値0644は、PHP-FPMとキューワーカーを別ユーザーで動かしている構成でつまずきます。先に書き込んだ側の所有でファイルが作られ、もう片方が同じファイルを開けません。permissionを0664に上げてグループを揃えるか、そもそも両者のログを分けるかのどちらかで解決します。キューワーカー側のログ運用はLaravel Queueの使い方の常駐の前提とあわせて考えてください。
lockingは書き込み前にファイルロックを取る設定で、既定はfalseです。複数プロセスが同一ファイルに追記する構成では行が混ざる可能性があるためtrueが候補になりますが、ロック待ちの分だけ書き込みが遅くなります。プロセスごとにファイルを分けられるなら、そちらのほうが素直な解決です。
外部への転送とコンテキスト付与で障害時に追跡可能なログを作る
ログを1行ずつ眺めて障害を追える規模は、案件が育つと早々に超えます。どこへ送るかと、1行に何を持たせるかの2点を設計しておくと、あとから収集基盤を足すときの手戻りが減ります。
slackとpapertrailとsyslogは前提条件がそれぞれ違う
slackドライバはIncoming Webhookのurlが必須で、LOG_SLACK_WEBHOOK_URLから与えます。papertrailはhostとportが必要で、配布版ではSyslogUdpHandlerをTLS付きの接続文字列で叩く形です。syslogはOS側のsyslogに投げるだけなので追加の資格情報は要りませんが、コンテナ内で動かすとホスト側にsyslogデーモンが居ないことがあります。
DatadogやNew Relicのように専用ドライバが無い送り先は、monologドライバで該当のMonologハンドラを直接指定します。handlerにクラス名、handler_withにコンストラクタ引数を渡す形式です。ハンドラが自前のフォーマッタを持つ場合はformatterにdefaultと書いて、Laravel側のLineFormatterの差し込みを止めます。
1リクエストが出した数十行を1本の糸で束ねるには、共通のIDを全行に載せます。ミドルウェアでLog::withContext()を呼べば、それ以降そのチャンネルに書かれる全レコードにキーが付きます。チャンネルをまたいで共有したいならLog::shareContext()のほうです。
public function handle(Request $request, Closure $next): Response
{
$requestId = (string) Str::uuid();
Log::shareContext(['request-id' => $requestId]);
$response = $next($request);
$response->headers->set('Request-Id', $requestId);
return $response;
}
レスポンスヘッダにも同じIDを返しておくと、利用者から「この画面でエラーが出た」と連絡を受けたときに、ヘッダの値だけでログを引ける状態です。キュー経由の処理へIDを引き継ぐ場合は、ジョブミドルウェアで同じことをします。
標準エラー出力へJSONで吐いてログ収集と保存は基盤側に任せる
コンテナで動かすなら、ファイルに書かず標準エラー出力に流して収集はプラットフォームに任せる形が扱いやすくなります。配布版のstderrチャンネルにはLOG_STDERR_FORMATTERを読むformatterが用意されているので、環境変数にMonologのJsonFormatterを指定するだけで1行1JSONになります。設定ファイルを触らずに出力形式を切り替えられる作りです。
この構成にすると、ローテーションもディスク容量も考えずに済む代わりに、収集基盤が落ちている間のログは失われます。監査目的で保存義務があるログをここに混ぜないことだけ気をつけてください。
個人情報のマスキングはtapとprocessorの2箇所で作る
フォームの入力値をそのままLog::info()に渡す実装は、レビューで見つけ次第止めるべきものです。とはいえ全箇所を人の目で防ぎ切るのは現実的ではないので、書き出しの直前で機械的に伏せる層を1枚入れておきます。
tapでフォーマッタを差し替えてチャンネルごとの出力形式を握る
チャンネル設定にtap配列を書くと、Monologのインスタンス生成直後に指定クラスの__invoke()が呼ばれます。ここでハンドラのフォーマッタを差し替えれば、そのチャンネルの出力形式を丸ごと握れます。
class CustomizeFormatter
{
public function __invoke(Logger $logger): void
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter(new LineFormatter(
'[%datetime%] %channel%.%level_name%: %message% %context%'
));
}
}
}
ただしtapでやるのは形式の変更までに留めます。値そのものを書き換える処理をフォーマッタに埋め込むと、フォーマッタを差し替えた瞬間にマスキングも消えるという壊れ方をします。
processorでcontextを走査して該当キーを伏せる
値を伏せる場所はprocessorです。Monolog 3系のprocessorはLogRecordを受け取ってLogRecordを返す形になっており、with()でcontextだけを差し替えられます。伏せるキーの一覧を定数で持っておき、再帰的に走査するのが定石です。
class MaskSensitive
{
private const KEYS = ['password', 'token', 'tel', 'email'];
public function __invoke(LogRecord $record): LogRecord
{
return $record->with(context: $this->mask($record->context));
}
private function mask(array $rows): array
{
foreach ($rows as $key => $value) {
if (is_array($value)) {
$rows[$key] = $this->mask($value);
} elseif (in_array($key, self::KEYS, true)) {
$rows[$key] = '[FILTERED]';
}
}
return $rows;
}
}
設定側ではmonologドライバのチャンネルならprocessorsキーにクラス名を並べます。dailyのようにドライバ側がprocessorsを受けないチャンネルでは、tapの中からpushProcessor()を呼んで差し込みます。伏せ字にした値は復元できないので、調査に要る識別子まで潰していないかは実機の出力で確かめてください。
マスキングしても個人情報が漏れる3つの経路を処理前に確実に塞ぐ
processorが見るのはmessageとcontextだけです。そこを通らない経路が3つあります。1つ目は例外のスタックトレースで、メソッドの引数に渡ったパスワードが文字列として乗る経路です。2つ目はDB::listen()やクエリログで、バインド値が配列のまま出ます。3つ目はHTTPクライアントのリクエストボディを丸ごと記録している箇所です。
いずれも「ログに出す前に落とす」しか手がありません。例外側は例外ハンドラのcontext()で出す情報を明示的に絞り、クエリログは本番で止め、HTTPクライアントはボディではなくステータスコードと所要時間だけを残す方針にします。APP_DEBUGがtrueのまま本番に出ていると、例外画面に環境変数がまるごと並ぶ点もあわせて確認してください。
受託案件でログ設定を決める採用条件と見送る構成の判断基準を整理する
ここまでの設定項目のうち、案件の最初に決めておくべきものは多くありません。台数と保存要件の2つが決まれば、残りはほぼ機械的に埋まります。
採用条件はサーバ台数とログ保存要件の2軸で構成を切り分けて判断する
1台構成で保存要件が「障害調査に使える程度」なら、LOG_STACKをdailyにしてmax_filesを30前後に置き、通知だけSlackへ回す構成で足ります。追加のミドルウェアは要りません。2台以上に増えた時点でファイルは各サーバに散るため、標準エラー出力へJSONで出して収集基盤に寄せるか、syslogで1箇所に集約するかの判断が必要になります。ここはセッションの保存先を決めるときと同じ分岐で、Laravelのセッション管理と同じタイミングで決めると構成が揃います。
保存要件に監査や法定保存が絡む場合は、別扱いにします。アプリのログと同じ経路に乗せると保持期間の変更に巻き込まれるので、専用チャンネルを切って保存先も分けるほうが後々もめません。
見送るのはsingleのまま外部転送だけを先に足してしまう構成です
逆に採らないほうがよい構成もあります。singleのまま外部転送だけ足す形は、ファイルが無制限に膨らんでディスクを埋めます。数百MBを超えたログファイルは末尾を読むだけでも重く、障害の最中に開けません。日次分割を入れずに外部転送を先に足すのは順序が逆です。
もう1つ見送るのは、全チャンネルのlevelをdebugのまま本番に出す構成です。SQLもキャッシュのヒットも全部出るため、量が増えるだけでなく個人情報の混入経路も比例して増えます。debugはローカルと検証環境までに留めてください。
運用に組み込むのは保持期間とログレベルと書き込み監視という3点
設定を書いて終わりにせず、運用側に3つ組み込みます。保持期間はディスク使用量の推移を見て四半期ごとに見直すこと、レベルは通知が鳴りすぎたときに閾値を上げるのではなく発生元を潰すこと、そして書き込み失敗そのものを監視することの3点です。3つ目は見落とされがちで、権限やディスク満杯でログが書けなくなっても、アプリは静かに動き続けます。ログの最終更新時刻を監視対象に入れておくと気づけます。
これらの見直しは公開直後より、運用が半年ほど回ってからのほうが判断材料が揃います。自社では保守運用/内製化支援として、ログの保持期間や通知の閾値をアプリの実際の挙動に合わせて調整するところまで継続的に引き受けています。
よくある質問
ログの出力先を変えたのに反映されないのはなぜですか?
本番で設定をキャッシュしている場合、config/logging.phpや.envを書き換えてもキャッシュ済みの値が使われ続けます。php artisan config:clearを実行するか、デプロイ手順にキャッシュの再生成を組み込んでください。キャッシュ後はenv()がnullを返す点にも注意します。
ログファイルが増え続けるのは放置してよいですか?
放置は勧めません。singleは1ファイルに追記し続け、dailyもmax_filesを明示しなければフレームワーク側のフォールバックである7世代で切られます。想定と違う世代数で回っていることがあるので、実機でstorage/logsの中身を数えて確認してください。
本番でログをリアルタイムに見る方法はありますか?
laravel/pailを開発依存として入れるとphp artisan pailで追尾できます。PCNTL拡張が前提で、2026年8月時点の最新はv1.2.7です。レベルや利用者IDで絞り込むオプションがあるため、末尾を読む従来のコマンドと検索を組み合わせるより速く目的の行に届きます。
非推奨警告のログはどこで有効にしますか?
設定ファイルのdeprecationsキーで指定します。既定はLOG_DEPRECATIONS_CHANNELがnullチャンネルを向いているため記録されません。メジャーバージョンの移行前にdailyなどへ向け、LOG_DEPRECATIONS_TRACEをtrueにすると発生箇所まで追えます。
スタックにSlackを足したら本体が落ちたのですが?
stackのignore_exceptionsがfalseだと、webhookへの送信失敗がアプリ側へ例外として伝播します。trueに変えて書き込み失敗を切り離すか、通知そのものをキュー経由の非同期に逃がしてください。前者は失敗に気づけなくなるため、送信の成否は別途監視します。
関連記事
- LaravelでPrometheus+Grafana監視を実装|メトリクス収集・可視化・アラート設定:数値の収集と可視化、発報の設計はこちらです。
- Laravel Telescopeの使い方|設定と本番運用の判断基準【2026年版】:リクエスト単位で中身を眺めるための道具です。
- Laravel Queueの使い方|ジョブ定義からworker常駐と失敗ジョブ運用まで:ワーカー側のログ運用の前提を扱っています。
- Laravelのセッション管理|ドライバ選定とCSRF・複数台構成の共有設計:複数台構成で決めることが共通します。