日志需贯穿请求全链路:早期生成请求id并注入log上下文,用户id在auth后获取;跨service需显式传递上下文,异步任务须序列化上下文;高频日志用字符串拼接,模块日志通过独立channel隔离。

日志里怎么带上请求 ID 和用户 ID
没有上下文的日志等于盲人摸象。ThinkPHP 默认不自动注入请求 ID,Log::info() 写进去的只是一堆孤立时间戳和字符串。
关键是在请求生命周期早期就生成唯一标识,并挂到全局可访问的位置。推荐在 app/middleware/LogContext.php 中间件里做:
- 用
uniqid('req_')或Str::uuid()->toString()生成请求 ID,存入think\facade\Request::instance()->header('x-request-id', $id)不可靠,应存到think\facade\App::getContainer()->bind('request_id', $id)或更直接地写进think\facade\Log::setExtra(['req_id' => $id]) - 用户 ID 要等 Auth 中间件执行完才有,所以 LogContext 最好放在 Auth 之后;否则
Auth::id()返回 null,硬塞会污染日志字段 - 别在每个
Log::info()前手动拼数组,那样漏写概率极高。改用Log::withContext(['uid' => Auth::id(), 'req_id' => $reqId])(TP6.1+)或封装一个AppLog::info()静态方法统一注入
业务流程跨多个 Service 怎么保持日志链路不中断
一个订单创建可能经过 OrderService → PaymentService → InventoryService,每个 service 都调 Log::info(),但默认彼此无关,看不出调用顺序和归属。
核心是把上下文“透传”下去,而不是靠日志内容里写“调用了 PaymentService::charge”。有两条路:
- 用 PHP 的
debug_backtrace()自动提取当前调用栈,但性能差、信息杂,线上慎用;更稳妥的是在入口处(比如控制器)把上下文对象传给第一个 service,后续逐层->withContext($ctx)传递(需各 service 方法签名支持) - ThinkPHP 本身不提供跨 service 上下文传递机制,别指望
Container::get()拿到的实例能自动共享 log context;必须显式传递或用静态变量(如static $logContext = []),但后者在协程或并发场景下会串数据 - 如果用了消息队列异步处理(比如下单后发 Kafka 更新库存),原始请求上下文必须序列化进消息体,消费者端再反序列化并重置
Log::setExtra(),否则日志完全断链
Log::info() 里传数组还是字符串?性能差多少
直接传数组给 Log::info(['order_id' => 123, 'status' => 'paid']) 看着干净,但 ThinkPHP 日志驱动(尤其 FileDriver)默认会用 var_export() 格式化,嵌套深了容易触发超时或内存溢出。
- 简单 key-value(≤3 层)用数组没问题;含对象、资源、闭包的必须先
json_encode()或Arr::except($data, ['user_obj'])过滤掉不可序列化项 - 高频日志(如每秒百次的支付回调验签)务必用字符串拼接:
Log::info("verify_ok|order_id={$order->id}|ip={$request->ip()}"),比数组快 3–5 倍,且避免 JSON 编码失败导致整条日志丢弃 - TP6 默认日志格式是
[date] [level] [msg],数组会被转成单行,可读性差;想结构化,得配合 ELK,这时应统一输出 JSON 字符串,而非原生 PHP 数组
日志文件按业务模块分目录,配置怎么写才不踩坑
默认所有日志都写进 runtime/log/202405/,查订单问题要翻一堆无关日志。按模块拆是刚需,但 ThinkPHP 的 log.file_format 配置容易误解。
-
'file_format' => '{Y-m-d}/order/{name}.log'看似合理,但{name}是日志级别(info/debug),不是模块名;真想按模块分,得自定义FileDriver子类,在write()方法里根据$content或上下文动态改$filename - 更轻量的做法:用
Log::channel('order')定义独立 channel,在config/log.php里配'order' => ['type' => 'File', 'path' => runtime_path('log/order/')],然后在 order 相关代码里统一用Log::channel('order')->info() - 注意 channel 名不能含路径分隔符(如
'order/create'),否则 TP 会报Invalid channel name;多级目录用下划线代替,比如'order_create'
上下文不是加几个字段就完事,它得贯穿整个调用树,且在异步、重试、跨进程时依然可追溯。少一个环节,排查时就要多花三倍时间翻日志
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











