Log::build()->pushContext(‘tenant_id‘, $tenantId)->info(‘email.sent‘, $context);的庖丁解牛
·
Log::build()->pushContext('tenant_id', $tenantId)->info('email.sent', $context); 是 Laravel 10+ 引入的日志上下文链式构建器(Logging Context Builder),用于在单次日志调用中注入临时上下文,而不污染全局日志配置。
一、核心目的:隔离上下文,避免全局污染
1. 传统方法的问题
// ❌ 全局注入上下文(影响后续所有日志)
Log::withContext(['tenant_id' => $tenantId]);
Log::info('email.sent', $context);
// 后续 Log::info() 也会携带 tenant_id → 数据污染
2. Log::build() 的解决方案
// ✅ 仅当前日志携带 tenant_id
Log::build()
->pushContext('tenant_id', $tenantId)
->info('email.sent', $context);
// 后续日志不受影响
✅ 本质:创建一次性、隔离的日志实例
二、底层执行流程
1. Log::build()
- 返回
Illuminate\Log\LogManager的新实例(非单例) - 不共享 全局日志通道的上下文
2. pushContext()
- 将
['tenant_id' => $tenantId]合并到当前构建器的上下文 - 支持链式调用:
Log::build() ->pushContext('tenant_id', $tenantId) ->pushContext('trace_id', $traceId) ->info('...');
3. info()
- 触发日志写入:
- 合并
$context(方法参数)与构建器上下文 - 调用底层 Monolog 记录日志
- 构建器实例销毁(上下文不保留)
- 合并
📌 关键:
上下文仅存在于本次build()链式调用中
三、Monolog 底层实现
1. 上下文合并逻辑
- 最终日志的
context=构建器上下文+info($message, $context)的$context - 示例:
Log::build() ->pushContext('tenant_id', 123) ->info('email.sent', ['to' => 'a@example.com']);- 最终 context:
[ 'tenant_id' => 123, 'to' => 'a@example.com' ]
- 最终 context:
2. 无全局状态变更
- Laravel 全局日志实例(
app('log')) 的上下文保持不变 - 线程安全:多请求并发时上下文不交叉污染
四、典型使用场景
1. 多租户应用
// 处理租户请求时
$tenant = Tenant::current();
Log::build()
->pushContext('tenant_id', $tenant->id)
->info('user.login', ['user_id' => $user->id]);
- 日志系统可按
tenant_id过滤
2. 链路追踪(Distributed Tracing)
$traceId = request()->header('X-Trace-Id');
Log::build()
->pushContext('trace_id', $traceId)
->info('api.request', ['path' => $request->path()]);
- 关联同一请求的多个服务日志
3. 队列任务上下文
// app/Jobs/SendEmail.php
public function handle()
{
Log::build()
->pushContext('job_id', $this->job->getJobId())
->info('email.processing', ['to' => $this->email]);
}
- 避免队列任务日志混杂
五、与全局上下文的对比
| 方法 | 作用域 | 适用场景 |
|---|---|---|
Log::withContext() |
全局(当前请求生命周期) | 请求级上下文(如 user_id) |
Log::build()->pushContext() |
单次日志 | 临时/任务级上下文(如 job_id) |
✅ 组合使用:
// 全局:当前用户 Log::withContext(['user_id' => auth()->id()]); // 临时:当前任务 Log::build() ->pushContext('job_id', $jobId) ->info('task.start'); // 日志包含 user_id + job_id
六、生产环境最佳实践
1. 字段命名规范
- 使用 snake_case:
tenant_id而非tenantId - 避免敏感数据:
// ❌ 危险 Log::build()->pushContext('password', $password); // ✅ 安全 Log::build()->pushContext('user_id', $user->id);
2. 性能考量
build()有轻微开销(创建新实例)- 仅在需要隔离上下文时使用,避免滥用
3. 与结构化日志集成
- 输出 JSON 格式(生产环境):
// config/logging.php 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['daily'], 'formatter' => Monolog\Formatter\JsonFormatter::class, ], ], - 日志示例:
{ "level": "info", "message": "email.sent", "context": { "tenant_id": 123, "to": "a@example.com" } }
七、总结
| 问题 | 答案 |
|---|---|
Log::build() 作用? |
✅ 创建隔离的日志实例,避免上下文污染 |
与 withContext() 区别? |
✅ build() 是单次,withContext() 是全局 |
| 典型场景? | ✅ 多租户、链路追踪、队列任务 |
| 生产建议? | ✅ 仅必要时使用 + 字段规范 + JSON 格式 |
日志上下文 = 数据的维度。
Log::build()让你精确控制每个日志事件的维度,
而非用全局上下文“污染”整个请求。
这是构建高可观测性系统的关键细节。
更多推荐

所有评论(0)