跳转到主要内容

什么是 Context

Laravel 的 Context 功能提供了一种在请求、队列任务、命令之间跨越执行边界记录与共享信息的机制。 通过 Illuminate\Support\Facades\Context 门面添加的信息,会自动附带到应用写出的所有日志条目中。 这样就能明确区分某次日志调用附带的信息与 Context 保存的共享信息。 在使用队列或分布式架构进行追踪时尤其有用。

上下文的传播流程

基本用法

最典型的用法是在中间件中设置 trace_id。之后所有的日志都会自动包含它。
1

创建中间件

php artisan make:middleware AddContext
2

向 Context 添加追踪 ID

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;

class AddContext
{
    public function handle(Request $request, Closure $next): Response
    {
        Context::add('url', $request->url());
        Context::add('trace_id', Str::uuid()->toString());

        return $next($request);
    }
}
3

注册中间件

bootstrap/app.php 中作为全局中间件注册。
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\AddContext::class);
})
配置好后,控制器或服务写入的日志会自动附带 urltrace_id
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
User authenticated. {"auth_id":27} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}

写入上下文

add — 添加值

use Illuminate\Support\Facades\Context;

Context::add('key', 'value');

// 一次添加多个
Context::add([
    'first_key'  => 'value',
    'second_key' => 'value',
]);
add 会覆盖已有的键。若只想在键不存在时才添加,使用 addIf
Context::add('key', 'first');
Context::addIf('key', 'second');

Context::get('key');
// "first" — 未被覆盖

increment / decrement — 管理计数器

用于数值增减的专用方法,可通过第二个参数指定增减量。
Context::increment('records_added');
Context::increment('records_added', 5);

Context::decrement('records_added');
Context::decrement('records_added', 5);

when — 按条件添加

使用 when 可以根据条件为 truefalse 分别添加不同的数据。
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Context;

Context::when(
    Auth::user()->isAdmin(),
    fn ($context) => $context->add('permissions', Auth::user()->permissions),
    fn ($context) => $context->add('permissions', []),
);

push — 追加到栈

Context 支持以「栈」的形式保存列表数据。 push 会按添加顺序累积数据。
Context::push('breadcrumbs', 'first_value');
Context::push('breadcrumbs', 'second_value', 'third_value');

Context::get('breadcrumbs');
// ['first_value', 'second_value', 'third_value']
以下是把查询执行历史存入栈的示例:
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;

// 在 AppServiceProvider.php 的 boot 方法中注册
DB::listen(function ($event) {
    Context::push('queries', [$event->time, $event->sql]);
});

读取上下文

get / all

$value = Context::get('key');

// 获取全部
$data = Context::all();

only / except — 获取子集

$data = Context::only(['first_key', 'second_key']);

$data = Context::except(['first_key']);

pull / pop — 取出并删除

pull 在取值的同时从上下文中删除该键。
$value = Context::pull('key');
从栈中取出最后一个值使用 pop
Context::push('breadcrumbs', 'first_value', 'second_value');

Context::pop('breadcrumbs');
// 'second_value'

Context::get('breadcrumbs');
// ['first_value']

remember — 不存在则设置并返回

$permissions = Context::remember(
    'user-permissions',
    fn () => $user->permissions,
);

has / missing — 判断键是否存在

if (Context::has('key')) {
    // ...
}

if (Context::missing('key')) {
    // ...
}
即使存的是 nullhas 也会返回 true。它只判断键是否已注册。

删除上下文

使用 forget 删除键。
Context::add(['first_key' => 1, 'second_key' => 2]);

Context::forget('first_key');

Context::all();
// ['second_key' => 2]

// 一次删除多个
Context::forget(['first_key', 'second_key']);

作用域上下文

使用 scope 可以在闭包执行期间临时修改上下文,闭包结束后自动恢复原状态。 在测试或局部处理中希望暂时向日志中附加信息时非常方便。
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Log;

Context::add('trace_id', 'abc-999');
Context::addHidden('user_id', 123);

Context::scope(
    function () {
        Context::add('action', 'adding_friend');

        $userId = Context::getHidden('user_id');

        Log::debug("Adding user [{$userId}] to friends list.");
        // Adding user [987] to friends list.  {"trace_id":"abc-999","user_name":"taylor_otwell","action":"adding_friend"}
    },
    data: ['user_name' => 'taylor_otwell'],
    hidden: ['user_id' => 987],
);

// scope 结束后已恢复
Context::all();
// ['trace_id' => 'abc-999']

Context::allHidden();
// ['user_id' => 123]
在 scope 内修改了对象的话,该修改在 scope 外也会保留。使用基本类型时不会有此问题。

Hidden Context

对于不希望输出到日志的数据(密码、API 密钥、个人识别信息等),可存放到 Hidden Context 中。 普通的 get 无法读取,需通过 getHidden 等专用方法访问。
use Illuminate\Support\Facades\Context;

Context::addHidden('key', 'value');

Context::getHidden('key');
// 'value'

Context::get('key');
// null — 普通 get 无法读取
Hidden Context 提供与普通 Context 一致的方法集:
Context::addHidden(/* ... */);
Context::addHiddenIf(/* ... */);
Context::pushHidden(/* ... */);
Context::getHidden(/* ... */);
Context::pullHidden(/* ... */);
Context::popHidden(/* ... */);
Context::onlyHidden(/* ... */);
Context::exceptHidden(/* ... */);
Context::allHidden(/* ... */);
Context::hasHidden(/* ... */);
Context::missingHidden(/* ... */);
Context::forgetHidden(/* ... */);

传递到队列任务

当把任务派发到队列时,当前的上下文会自动序列化并包含在任务的 payload 中。 任务执行时会恢复原上下文,因此请求中设置的 trace_id 也会自动出现在队列中的日志里。
// 在中间件中设置
Context::add('trace_id', Str::uuid()->toString());

// 在控制器中派发任务
ProcessPodcast::dispatch($podcast);
class ProcessPodcast implements ShouldQueue
{
    use Queueable;

    public function handle(): void
    {
        Log::info('Processing podcast.', ['podcast_id' => $this->podcast->id]);
    }
}
Processing podcast. {"podcast_id":95} {"url":"https://example.com/login","trace_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697"}
可以看到请求中的 trace_id 也出现在了队列上的日志中。

Dehydrating — 任务发送时的自定义

Context::dehydrating 允许在任务发送前修改上下文。 比如,可用于把由 Accept-Language 头决定的 locale 传给队列。
// AppServiceProvider.php

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;

public function boot(): void
{
    Context::dehydrating(function (Repository $context) {
        $context->addHidden('locale', Config::get('app.locale'));
    });
}
dehydrating 回调中请不要使用 Context 门面,而是操作回调传入的 $context 仓库。 使用门面会修改当前进程的上下文。

Hydrated — 任务执行时的恢复

Context::hydrated 用于在任务执行前恢复上下文的时机添加处理。 比如,把此前保存的 locale 应用回配置:
// AppServiceProvider.php

use Illuminate\Log\Context\Repository;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Context;

public function boot(): void
{
    Context::hydrated(function (Repository $context) {
        if ($context->hasHidden('locale')) {
            Config::set('app.locale', $context->getHidden('locale'));
        }
    });
}
hydrated 回调中同样不要使用 Context 门面,只操作传入的 $context 仓库。

小结

ContextLog::withContext
作用范围所有日志通道仅特定通道
传递到队列任务自动(Dehydrate / Hydrate)不传递
Hidden 数据支持不支持
用途追踪、分布式系统通道自身的元数据
Hidden Context 不会输出到日志,因此适合安全地存放:
  • 会话 ID 或用户 ID(不希望进日志时)
  • API 密钥或认证令牌
  • locale 或配置项(需要传递给队列但不需要出现在日志中)
  • 内部标志或状态
  1. locale 传递dehydrating 时把 app.locale 存入 Hidden Context,hydrated 时通过 Config::set 恢复。
  2. 认证信息传播:使队列任务也能访问请求时已认证的用户信息。
  3. 租户 ID:在多租户应用中跨队列共享租户标识。
最后修改于 2026年7月13日