はじめに
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 での分析が捗る
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": "" が出るのは避けられないということ…。
いうても、ログにいらんもん出したくないし。
解決方法:custom driver + 独自フォーマッター
Laravel では custom driver を作れば、自由にログ形式をコントロールできるそうです。
Monolog 3 の仕様には逆らえないので、この custom driver を使って対処することにしました。
すると、
- message を消して 1 必要なフィールドだけ JSON に出力
- context を flatten
が可能になりました。
実装例
'channels' => [
'db_query' => [
'driver' => 'custom',
'via' => App\Logging\DbQueryLoggerFactory::class,
'level' => 'info',
],
],
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;
}
}
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 で自分好みの形式のログを出力したいときに使えるテクニックでした。
-
message は内部的には存在していますが、フォーマッターでそもそも出力しない形にします。そうすることで「message がないログ」を実現しています。 ↩