laravel默认日志为彩色文本,对接elk等需json格式;改config/logging.php的'formatter'无效,因laravel不透传该配置,必须通过tap闭包替换monolog handler并注入jsonformatter。

默认的 Laravel 日志是带颜色、换行、可读性强的文本格式,但对接 ELK、Loki、Datadog 等平台时,必须输出结构化 JSON —— 文本日志会被解析失败、字段丢失、时间戳无法对齐。改 formatter 配置项没用,Laravel 会忽略它;真正生效的是替换 Monolog 的 Handler。
为什么 config/logging.php 里改 'formatter' 不生效
Laravel 在初始化 channel 时,并不会把 formatter 配置透传给 Monolog 的 StreamHandler 或 DailyRotatingFileHandler。它只在极少数内置 handler(如 syslog)中读取该键,其余情况直接丢弃。你改了 'formatter' => \Monolog\Formatter\JsonFormatter::class,日志照样是文本。
必须通过 tap 闭包显式构造带 JsonFormatter 的 handler 实例。
-
tap是唯一被 Laravel 日志系统尊重的“注入点”,用于接管 handler 构建过程 - 所有自定义 formatter(包括时区、JSON 选项、上下文扁平化)都得在这里做
- 别在
stack通道上配tap——它不转发到子通道,得配在具体驱动(如daily)上
怎么用 tap + JsonFormatter 输出标准 JSON 日志
在 config/logging.php 的某个具体通道(比如叫 json_daily)里加 tap 数组,指向一个可调用类:
'json_daily' => [
'driver' => 'daily',
'path' => storage_path('logs/json/app.log'),
'level' => 'debug',
'days' => 14,
'tap' => [App\Logging\CustomJsonLogger::class],
],
然后新建 app/Logging/CustomJsonLogger.php:
namespace App\Logging;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
use Illuminate\Support\Facades\Log;
class CustomJsonLogger
{
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
if (method_exists($handler, 'setFormatter')) {
$formatter = new JsonFormatter(
JsonFormatter::BATCH_MODE_JSON,
new \DateTimeZone('Asia/Shanghai')
);
$formatter->includeStacktraces(false);
$handler->setFormatter($formatter);
}
}
}
}
-
JsonFormatter::BATCH_MODE_JSON确保每条日志一行,避免换行符破坏 JSON 流解析 - 必须传
\DateTimeZone实例,否则datetime字段是 UTC 时间戳(秒级),不是 ISO8601 字符串 - 不要依赖
JsonFormatter::SIMPLE——它不支持时区,且默认含extra和context嵌套,ELK 不友好
Log::info() 传数组自动 JSON 化,但 context 仍被嵌套
你写 Log::channel('json_daily')->info('User logged in', ['user_id' => 123, 'ip' => '1.2.3.4']),JsonFormatter 默认会把第二个参数塞进 context 字段,结果是:
{"message":"User logged in","context":{"user_id":123,"ip":"1.2.3.4"},...}
而多数日志平台希望这些字段平铺在根层级。要扁平化,得重写 format 方法或用 Processor:
- 在
CustomJsonLogger的__invoke中,给 handler 加 processor:$handler->pushProcessor(function ($record) { return array_merge($record, $record['context'] ?? []); }); - 注意:这会覆盖同名字段(如
message),需手动排除context和extra - 更稳妥的做法是继承
JsonFormatter,重写format,把context键值合并进顶层
环境变量控制是否启用 JSON 格式
开发时想看文本,生产才用 JSON?别写两套配置。在 tap 类里判断环境:
if (app()->environment('production')) {
$handler->setFormatter(new JsonFormatter(...));
}
或者更灵活地用 .env 控制:
- 加
LOG_JSON_FORMAT=true到.env - 在
CustomJsonLogger里用env('LOG_JSON_FORMAT') === 'true'做开关 - 注意:别用
config()读取,此时 config 尚未完全加载,应直接读env()
JSON 日志真正的复杂点不在“怎么输出”,而在“字段要不要扁平化”“时区要不要强制覆盖”“context 里敏感字段要不要过滤”——这些没法靠一个配置开关解决,必须在 tap 或自定义 Formatter 里逐层控制。











