必须用thinkfacadeLog配合自定义日志处理器注入TraceID,否则跨请求、协程、中间件时链路ID会丢失;需在应用初始化阶段注册上下文透传处理器,并通过Log::setConfig注入格式化模板,Swoole下须用Co::getContext()或Container协程隔离容器。
直接结论:不用 Log::write() 原始写法,必须用 thinkfacadeLog + 自定义日志处理器注入 TraceID,否则跨请求、跨协程、跨中间件时链路 ID 会断。
常见错误是每个接口里手动拼 trace_id 字段塞进日志,结果发现异步任务、队列消费、Redis 回调里全没这个 ID —— 因为 ThinkPHP 默认的 Log facade 是 request 生命周期绑定的,中间件一结束、worker 一重启,上下文就丢了。
app/common.php 或服务提供者中)注册一个带上下文透传的日志处理器,比如继承 thinklogdriverFile 并重写 write() 方法,从 thinkRequest 或 thinkfacadeSession(如果用了 session)或更可靠的 thinkhelperStr::uuid() 生成/复用 trace_idthinkContainer::getInstance()->get('request')->header('x-trace-id', Str::uuid()),再通过 Log::setConfig(['format' => '[{date}] [{level}] {trace_id} {message}']) 注入格式化模板$_SERVER 或全局变量存 trace_id,必须走 Co::getContext() 或 Container 的协程隔离容器默认日志只记时间、级别、消息,查问题时根本不知道这行日志来自哪个控制器哪个方法。靠人工 grep 路由配置和控制器太慢,也容易漏。
关键不是改日志内容,而是改日志上下文注入点。ThinkPHP 6.1+ 支持在 Log::record() 时传第二个参数作为上下文数组,但多数人没意识到这个参数能被日志处理器捕获并格式化输出。
立即学习“PHP免费学习笔记(深入)”;
initialize() 方法里统一写:Log::record('enter ' . $this->request->action(), 'info', ['route' => $this->request->url(), 'controller' => get_class($this), 'action' => $this->request->action()]);
write() 方法里检查 $data['context'] 是否存在 route 或 controller 键,有则拼进日志行;否则 fallback 到 debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2) 抽 controller/action(仅开发环境启用,避免性能损耗)__METHOD__ 或 __FUNCTION__ 直接打,它们在中间件或事件回调里指向的是框架内部方法,不是业务入口因为默认日志是本地文件,而你调用的下游服务(如 Python 微服务、Node.js 网关)压根不走你的 runtime/log/ 目录。所谓“聚合”,不是把所有日志 cp 到一台机器,而是统一打到支持 trace_id 关联的中心存储(如 ELK、Loki、Datadog)。
ThinkPHP 本身不内置日志上报能力,得自己补一层。重点不是“怎么发”,而是“发什么”——必须确保每条日志都含可关联字段:trace_id、span_id、parent_span_id、service_name、host、timestamp。
thinklogdriverSocket 驱动(非官方,需自己实现)或替换为 monolog/monolog + elasticsearch/elasticsearch 客户端,但注意 Monolog 的 Processors 要挂载 trace_id 注入逻辑service_name,应从配置读:config('app.service_name', 'tp-api'),方便不同部署环境区分不是日志本身慢,是日志写入阻塞了主流程。尤其用了同步 HTTP 上报、文件锁冲突、或在循环里高频调用 Log::info()。
最典型的坑:在 foreach 处理 1000 条数据时,每条都 Log::info('process item', ['id' => $id]) —— 这等于触发 1000 次磁盘 I/O 或网络请求,而不是一次批量写入。
thinkqueueJob 包一层日志写入任务,或借助 Swoole 的 Co::create() 启协程非阻塞发送(注意协程安全)realtime_write => false(TP6.2+ 支持),让日志先缓存在内存,定时 flush,但要注意进程重启时缓存丢失风险app_debug,否则框架会自动记录 SQL、路由匹配等冗余日志,量级翻几倍链路日志真正的难点不在怎么打,而在怎么保证 trace_id 在所有可能出口(HTTP、RPC、MQ、定时任务)里都不丢、不错、不重复。一旦某个环节漏传,整条链就断成两截,排查时反而更费劲。