1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Monolog 3 で message のない JSON ログが出したいんだよッ

Posted at

はじめに

Laravel で DB のクエリログを JSON 形式で出力したくて、Monolog 3 と格闘したときのおはなしです。

やりたかったこと

  • JSON 形式で DB クエリログを出力したい
  • message は使っていないので、ログに載せたくない
  • SQL 文や バインド値などを詰めた context を CloudWatch Logs で扱いやすくするため flatten したい

最終的に出したいログの例です。

{
  "level": "INFO",
  "datetime": "2025-12-05T12:34:56+09:00",
  "sql": "select * from users where id = ?",
  "bindings": [123],
  "time_ms": 12.5,
  "request.method": "GET",
  "request.url": "/users/123"
}

message が存在せず、context の中身だけトップレベルに展開させる感じ。

わかったこと

  • Monolog 3 の LogRecord は immutable なので tap で message を直接消せない
  • JsonFormatter は必ず message を出力する
  • そのため、custom driver + 独自フォーマッターで出力形式を制御するのが最適解
  • context flatten も自由にできるので、CloudWatch Logs での分析が捗る

:x: NG例:直接値を書き換えようとして怒られるやつ

最初はこんな感じで処理しようとしました。

public function __invoke($logger)
{
    foreach ($logger->getHandlers() as $handler) {
        $handler->pushProcessor(function ($record) {
            // ここで message を削除 -> できない
            unset($record['message']); // LogicException

            // context のなかみを flatten して代入 -> できない
            $record['context'] = Arr::dot($record->context); // LogicException
            return $record;
        });
    }
}

すると怒られてしまった…。

Unsupported operation: offsetUnset('message')

え、なんで?

原因は Monolog 3 から LogRecord が immutable(不変) になっていたためでした。

  • 直接値を書き換えたり、削除したりできない
  • 標準の JsonFormatter必ず message を出力する

つまり、「message を消す」は仕様上不可能です。
空文字にはできるのですが、存在そのものを消すことはできません。
→ ログに "message": "" が出るのは避けられないということ…。

いうても、ログにいらんもん出したくないし。

:wink: 解決方法:custom driver + 独自フォーマッター

Laravel では custom driver を作れば、自由にログ形式をコントロールできるそうです。
Monolog 3 の仕様には逆らえないので、この custom driver を使って対処することにしました。
すると、

  • message を消して 1 必要なフィールドだけ JSON に出力
  • context を flatten

が可能になりました。

実装例

logging.php
'channels' => [
    'db_query' => [
        'driver' => 'custom',
        'via' => App\Logging\DbQueryLoggerFactory::class,
        'level' => 'info',
    ],
],
DbQueryLoggerFactory.php
namespace App\Logging;

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

class DbQueryLoggerFactory
{
    public function __invoke(array $config)
    {
        $logger = new Logger('db_query');

        $handler = new StreamHandler(storage_path('logs/db-query.log'));
        $handler->setFormatter(new DbQueryLogJsonFormatter());

        $logger->pushHandler($handler);

        return $logger;
    }
}
DbQueryLogJsonFormatter.php
namespace App\Logging;

use Monolog\Formatter\FormatterInterface;
use Illuminate\Support\Arr;

class DbQueryLogJsonFormatter implements FormatterInterface
{
    public function format(array $record): string
    {
        $context = $this->flatten($record['context'] ?? []);

        $output = [
            'level' => $record['level_name'],
            'datetime' => $record['datetime']->format('c'),
            ...$context,
        ];

        return json_encode($output, JSON_UNESCAPED_UNICODE) . "\n";
    }

    public function formatBatch(array $records): string
    {
        return implode('', array_map([$this, 'format'], $records));
    }

    private function flatten(array $context): array
    {
        return Arr::dot($context);
    }
}

さいごに

戦いの末たどりついたのは、custom driver と独自フォーマッターだったのです。
DB クエリログに限らず、Laravel で自分好みの形式のログを出力したいときに使えるテクニックでした。

  1. message は内部的には存在していますが、フォーマッターでそもそも出力しない形にします。そうすることで「message がないログ」を実現しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?