hyperf 多服务日志统一归集需满足三要素对齐:json 格式日志、trace_id 显式顶层字段、filebeat 正确解析配置;须替换 lineformatter 为 jsonformatter,添加 traceidprocessor,显式传 context,启用 json.keys_under_root,并按 service 区分标签。

Hyperf 多服务日志分散,靠手动翻文件或 grep 查问题根本不可行;统一归集不是“加个 Filebeat 就完事”,核心卡点在日志结构、trace_id 透传、采集配置三者必须对齐,缺一不可。
日志必须是 JSON 格式且 trace_id 显式在顶层字段
Hyperf 默认用 LineFormatter 输出纯文本,trace_id 被塞进整行字符串里——ELK/Loki 拿不到字段,Kibana 里搜 trace_id: "xxx" 必然为空。
- 改
config/autoload/logger.php:把 formatter 换成\Monolog\Formatter\JsonFormatter::class,并确保'batchMode' => \Monolog\Formatter\JsonFormatter::BATCH_MODE_JSON和'appendNewline' => true - 加自定义 Processor,比如
App\Logger\TraceIdProcessor,在$record['extra']['trace_id']中写入值,而不是只依赖context数组 - 确认
Hyperf\Context\Context::get('trace_id')确实有值:需已安装hyperf/tracer,且中间件TraceMiddleware已启用(检查config/autoload/middlewares.php) - 业务代码中写日志时,必须显式传 context:
$logger->info('order paid', ['order_id' => $id, 'trace_id' => $tid]),别拼字符串
Filebeat 配置必须开启 json.keys_under_root: true
就算 Hyperf 输出了合法 JSON,Filebeat 默认会把整条日志当字符串塞进 message 字段;不打开这个开关,trace_id 还是藏在嵌套 JSON 里,无法被 ES 或 Loki 提取为可查字段。
- 在
filebeat.yml的 input 配置块中,必须显式设json.keys_under_root: true和json.add_error_key: true - 路径要指向容器内日志文件实际位置,例如
/app/runtime/logs/*.log,不是宿主机路径 - 如果用 Docker 部署,Filebeat 容器必须以
bind mount方式挂载宿主机日志目录(如-v /var/log/hyperf:/app/runtime/logs:ro),不能只靠容器内路径 - 避免在 Filebeat 里加
dissect或grok解析——Hyperf 已输出结构化 JSON,再解析是冗余且易错的
多服务共用一套采集链路但需区分 service 标签
多个 Hyperf 服务(如 user-service、order-service)不能共用一个 Filebeat 实例却输出相同 service 字段,否则日志混在一起,查起来还是分不清来源。
- 每个服务部署时,在 Filebeat 配置里用
processors.add_fields注入静态标签:service: "user-service"、env: "prod" - 不要靠日志内容动态提取 service 名——JSON 里没这个字段,提取逻辑容易失效
- 若用 Loki,label 设计更关键:
{service="user-service", level="error"}是查询基础,message内容只能用 LogQL 正则匹配,不能全文索引 - ELK 场景下,建议在
output.elasticsearch.indices中按 service 分 index:"hyperf-user-logs-%{+yyyy.MM.dd}",避免单 index 过大
别用 Monolog SocketHandler 直连 Logstash
有人图省事在 logger.php 里配 SocketHandler 发日志到 Logstash,生产环境这是高危操作:连接无重试、缓冲无背压、进程崩溃时日志直接丢失。
- Monolog 的
SocketHandler是玩具级设计,不适用于高并发协程场景 - Filebeat 是专为日志传输打造的,自带断点续传、磁盘缓冲、失败重发、JSON 自动解析
- Hyperf 容器里只需专注写文件(
RotatingFileHandler+BufferHandler),采集交给 Filebeat,职责分离才稳 - 如果非要用 TCP,至少加一层
WhatFailureGroupHandler包裹StreamHandler(写文件)和SocketHandler(备选),但依然不推荐
最常被跳过的一步是确认 Context::get('trace_id') 在协程生命周期内始终有效——中间件顺序错、异步任务未继承上下文、协程池复用旧 context,都会导致 trace_id 为空。日志格式和采集配置再完美,源头字段丢了,整个链路就断了。











