Monolog 日志分流关键在于 Handler 组合与级别过滤,而非自定义处理器本身;推荐用 FilterHandler 限定单 Logger 输出路径,或为不同级别创建独立 Logger 实例,并统一使用 JsonFormatter 和 Processor 注入上下文字段。

Monolog 按日志级别拆分到不同存储介质,关键不是靠“自定义处理器”本身,而是靠Handler 的组合策略 + 级别过滤机制。真正起作用的是 FilterHandler 包裹、独立 Logger 实例、或 Processor 配合 Formatter 的结构化输出——自定义 Handler 只是最后一步的“搬运工”,不解决分流逻辑。
用 FilterHandler 限定单 Logger 的输出路径
如果你希望复用一个 Logger 实例(比如全局 $logger),又想让 ERROR 日志进 Elasticsearch、INFO 日志写本地文件、DEBUG 日志只存内存或丢弃,就必须用 FilterHandler 做“闸门”:
- 为每个目标介质创建专属 Handler(如 $esHandler、$fileHandler、$nullHandler)
- 用 FilterHandler 包裹它,并设置 minLevel / maxLevel:例如 new FilterHandler($esHandler, Logger::ERROR, Logger::EMERGENCY)
- 再把 FilterHandler 推入 Logger:$logger->pushHandler($filteredEsHandler)
- 注意:FilterHandler 必须在 Handler 层做级别拦截,不能依赖 Formatter 或 Processor 来“事后筛选”
更推荐:为每类级别建独立 Logger 实例
比 FilterHandler 更清晰、更易维护的方式,是按语义通道划分 Logger,每个只负责一类日志流向:
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
- $errorLogger = new Logger('error') → 绑定 Elasticsearch Handler + RotatingFileHandler(error.log)
- $infoLogger = new Logger('info') → 绑定 StreamHandler(info.log)+ SyslogHandler
- $debugLogger = new Logger('debug') → 绑定 NullHandler(开发环境可切为 StreamHandler)
- 各实例使用统一 JsonFormatter,确保上下文字段(如 trace_id、user_id)结构一致,方便 ES 聚合检索
格式与上下文必须统一,否则分流失效
无论用哪种方式,若日志内容不结构化,Elasticsearch 就无法按 error.code 或 http.status 做聚合分析,文件里也难 grep 出有效线索:
- 所有 Handler 必须用 JsonFormatter(非 LineFormatter),避免 ES 解析失败
- 用 Processor 注入公共字段:request_id、env、app_name,而不是每次 log() 手动传
- 错误日志务必包含 exception 上下文:$logger->error('DB timeout', ['exception' => $e]),Monolog 会自动提取 trace、message、class
不建议从零写 Handler,优先组合现有组件
所谓“自定义 Handler”,多数场景其实是封装已有 Handler 并加一层路由逻辑。比如想把 WARNING 日志同时发 Slack 和写文件,直接 new GroupHandler([$slackHandler, $fileHandler]) 即可;真要对接新介质(如 Kafka、ClickHouse),才需继承 AbstractProcessingHandler,但重点仍是复用 Formatter 和 Processor,而非重写 write() 逻辑。

















