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()

  • 触发日志写入
    1. 合并 $context(方法参数)与构建器上下文
    2. 调用底层 Monolog 记录日志
    3. 构建器实例销毁(上下文不保留)

📌 关键
上下文仅存在于本次 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'
      ]
      

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_casetenant_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() 让你精确控制每个日志事件的维度
而非用全局上下文“污染”整个请求。
这是构建高可观测性系统的关键细节。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐