🚨

Laravel Deprecation Logger:LOG_DEPRECATIONS_CHANNEL=nullが効かない理由

に公開

🎭 序章:謎のEMERGENCYログとの遭遇

PHP 8.5へのアップグレード作業中、予期せぬログが出現した。

[2025-12-25 09:34:53] laravel.EMERGENCY: Unable to create configured logger. Using emergency logger.

「設定したのに...なぜ?」

.envファイルにはLOG_DEPRECATIONS_CHANNEL=nullと設定し、本番環境では非推奨警告を無効化したはずだった。しかし、laravel.logにdeprecation警告を吐き出し続ける。

🔍 問題の発見

PHP 8.5へのアップグレード後、Symfonyの非推奨警告が出力されていることに気がついた。

[2025-12-25 09:34:53] laravel.WARNING: Since symfony/http-foundation 7.4: 
Request::get() is deprecated, use properties ->attributes, query or request directly instead.

開発環境では記録したいが、本番環境では不要。そこで、Laravelのドキュメント通りに設定した。

設定したこと

.env

LOG_DEPRECATIONS_CHANNEL=null

期待した動作

  • 非推奨警告がログに記録されない

実際の動作

  • laravel.logに警告が出力される
  • 加えて、謎のEMERGENCYログも出現

「何かがおかしい...」


🤔 最初の仮説:環境変数が読まれていない?

まず疑ったのは、環境変数が正しく読み込まれているかどうか。

sail artisan tinker
> echo env('LOG_DEPRECATIONS_CHANNEL');
// 出力: (何も表示されない = null)

環境変数は正しくnullになっている。では、なぜ動かない?

Laravelの設定キャッシュの可能性

sail artisan config:clear
sail artisan config:cache

しかし、結果は変わらず。コンテナを再起動しても同じ。

「設定は読まれているのに、なぜ効かない?」


📚 設定ファイルの構造を理解する

config/logging.phpを改めて確認する。

return [
    'default' => env('LOG_CHANNEL', 'stack'),
    
    // トップレベルのdeprecations設定
    'deprecations' => [
        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
        'trace' => env('LOG_DEPRECATIONS_TRACE', false),
    ],
    
    'channels' => [
        'stack' => [...],
        'single' => [...],
        'null' => [
            'driver' => 'monolog',
            'handler' => NullHandler::class,
        ],
        // 🤔 deprecationsチャネルが定義されていない!
    ],
];

ここで重要な発見:

  • トップレベルにdeprecations設定がある
  • channels内にはdeprecationsが定義されていない

Laravelのドキュメントでは、トップレベルのdeprecations.channelを設定すれば動くはず...なのに。


🕵️ Laravel内部コードへのダイブ

ここからが本番。Laravelのソースコードを読み解く。

1. エントリーポイント:HandleExceptions.php

非推奨エラーが発生すると、HandleExceptions::handleDeprecationError()が呼ばれる。

// vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php

public function handleDeprecationError($message, $file, $line, $level = E_DEPRECATED)
{
    // ...
    
    $this->ensureDeprecationLoggerIsConfigured();  // 👈 設定を確認
    
    $options = static::$app['config']->get('logging.deprecations') ?? [];
    
    with($logger->channel('deprecations'), function ($log) use (...) {  // 👈 ここで例外発生!
        $log->warning($message);
    });
}

2. 設定を確認する:ensureDeprecationLoggerIsConfigured()

protected function ensureDeprecationLoggerIsConfigured()
{
    $config = static::$app['config'];
    
    // 既に定義されていればスキップ
    if ($config->get('logging.channels.deprecations')) {
        return;
    }
    
    // トップレベルのdeprecations設定から取得
    if (is_array($options = $config->get('logging.deprecations'))) {
        $driver = $options['channel'] ?? 'null';  // 👈 'null'を取得
    } else {
        $driver = $options ?? 'null';
    }
    
    // 動的に設定をコピー
    $config->set('logging.channels.deprecations', 
                 $config->get("logging.channels.{$driver}"));  // 👈 nullチャネルをコピー
}

ここまでは正常に動作しているはず...

3. ログチャネルを取得する:LogManager.php

問題は次の行で発生する。

$logger->channel('deprecations')

内部で何が起きているのか?

// vendor/laravel/framework/src/Illuminate/Log/LogManager.php

protected function get($name, ?array $config = null)
{
    try {
        return $this->channels[$name] ?? with($this->resolve($name, $config), function ($logger) use ($name) {
            // ...
            return $this->channels[$name] = $loggerWithContext;
        });
    } catch (Throwable $e) {
        // 👈 ここで例外をキャッチ!
        return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
            $logger->emergency('Unable to create configured logger. Using emergency logger.', [
                'exception' => $e,
            ]);
        });
    }
}

例外が発生すると、Emergency Loggerにフォールバックする!

4. 何の例外が発生している?

protected function resolve($name, ?array $config = null)
{
    $config ??= $this->configurationFor($name);  // 👈 ここで設定を取得
    
    if (is_null($config)) {
        throw new InvalidArgumentException("Log [{$name}] is not defined.");  // 👈 これだ!
    }
    // ...
}

protected function configurationFor($name)
{
    return $this->app['config']["logging.channels.{$name}"];  // 👈 nullが返る
}

5. Emergency Loggerの正体

protected function createEmergencyLogger()
{
    $config = $this->configurationFor('emergency');
    
    $handler = new StreamHandler(
        $config['path'] ?? $this->app->storagePath().'/logs/laravel.log',  // 👈 laravel.log!
        $this->level(['level' => 'debug'])
    );
    
    return new Logger(
        new Monolog('laravel', $this->prepareHandlers([$handler])),
        $this->app['events']
    );
}

真実が明らかになった!


💡 真犯人を発見:タイミングの問題

問題の流れを整理する:

  1. 101行目: ensureDeprecationLoggerIsConfigured() が実行される
  2. 149行目: $config->set('logging.channels.deprecations', ...) で動的に設定
  3. 105行目: $logger->channel('deprecations') を呼び出し
  4. LogManager内部: configurationFor('deprecations') が実行される
  5. しかし: 設定が反映される前にnullが返される
  6. 結果: InvalidArgumentException がスロー
  7. フォールバック: Emergency Logger(laravel.log)が使用される

つまり、設定の動的追加とLogManagerの内部処理の間に競合状態(Race Condition)が存在する!

なぜ競合が起きるのか?

$config->set()は実行時の動的設定だが、LogManagerの内部キャッシュや処理順序により、設定が反映される前に参照されてしまう。


✅ 解決策とその理由

解決策:channels.deprecationsを明示的に定義する

// config/logging.php

'channels' => [
    // ... 他のチャネル
    
    'deprecations' => [
        'driver' => env('LOG_DEPRECATIONS_CHANNEL') === null ? 'monolog' : 'single',
        'handler' => env('LOG_DEPRECATIONS_CHANNEL') === null ? NullHandler::class : null,
        'path' => env('LOG_DIR', storage_path('logs')) . '/deprecations.log',
        'level' => 'warning',
        'replace_placeholders' => true,
        'permission' => 0660,
    ],
],

なぜこれで解決するのか?

ensureDeprecationLoggerIsConfigured()の最初のチェックを見てほしい:

if ($config->get('logging.channels.deprecations')) {
    return;  // 👈 既に定義されていれば何もしない
}

明示的に定義することで:

  • 動的な設定追加をスキップ
  • LogManagerが直接設定を参照できる
  • タイミング問題を回避

環境に応じた制御

開発環境 (.env)

LOG_DEPRECATIONS_CHANNEL=deprecations

→ singleドライバーでdeprecations.logに記録

本番環境 (.env.production)

# LOG_DEPRECATIONS_CHANNELをnullに設定、もしくは削除
LOG_DEPRECATIONS_CHANNEL=null

→ env()がnullを返し、NullHandlerで無効化


🎯 結論:Laravelのバグか、それとも?

これはバグなのか?

技術的には: Yes、タイミング問題による不具合

実用的には: Workaround(回避策)が存在する

Laravelコミュニティへの報告は必要?

  • 報告する価値はある:他の開発者も同じ問題に遭遇している可能性
  • しかし修正は難しい:内部アーキテクチャの大きな変更が必要
  • 現実的な対応:channels.deprecationsを明示的に定義する

マージリクエストのコミットメッセージ例

fix: Add explicit deprecations channel configuration

Laravelのdeprecation logger初期化タイミングの問題を回避するため、
logging.channels.deprecationsを明示的に定義。

これにより、環境変数LOG_DEPRECATIONS_CHANNELで
deprecation警告の出力を制御できるようになる。

- 開発環境: LOG_DEPRECATIONS_CHANNEL=deprecations でログ記録
- 本番環境: 環境変数を未設定にすることでログを無効化

🎓 学んだこと

  1. Laravelの設定は2層構造

    • トップレベル設定(logging.deprecations)
    • チャネル設定(logging.channels.*)
  2. 動的設定には限界がある

    • config->set()は万能ではない
    • タイミング問題を考慮する必要がある
  3. Emergency Loggerの存在

    • ログ設定に失敗するとlaravel.logにフォールバック
    • デバッグの手がかりとしてEMERGENCYログを確認
  4. フレームワークのソースコードを読む価値

    • ドキュメントだけでは分からない問題がある
    • 内部実装を理解することで根本原因を特定できる

📚 参考資料


Discussion