skip to content

In Laravel, how do Log::withContext() and Log::shareContext() differ, and which log channels receive the data each one adds?

level: middleimportance: should knowfreq 42%

answer

  1. default channel versus every channel
  2. LogManager forwards unknown calls to default driver
  3. sharedContext applied to channels built later
  4. withoutContext() and flushSharedContext()
  5. worker clears both between jobs

basics

~10 s

Log::withContext() merges data into the default channel only, so entries sent through Log::channel('slack') lack it. Log::shareContext() adds it to every channel already resolved and every channel created afterwards, including on-demand stacks.

solid answer

~40 s

`Log::withContext([...])` is not a `LogManager` method: the manager's `__call` forwards it to the **default channel's** `Logger`, which merges the array into its own context for every later entry. A write through `Log::channel('audit')` or `Log::build([...])` does not see it. `Log::shareContext([...])` calls `withContext()` on every channel already resolved and also stores the array in the manager's shared context, which is applied to each channel created later and to `Log::stack([...])`. Both land in the record's `context` part, merged under the per-call array. You clear them with `Log::withoutContext()` (optionally for given keys) and `Log::flushSharedContext()`; the queue worker runs both between jobs, and Octane does the same between requests.

code

php · 25 lines
php
<?php

namespace App\Http\Middleware;

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

class AssignRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        $requestId = (string) Str::uuid();

        // Every channel, including Log::channel('audit'), gets it
        Log::shareContext(['request_id' => $requestId]);

        $response = $next($request);
        $response->headers->set('Request-Id', $requestId);

        return $response;
    }
}

go deeper

for a junior

Remember that withContext adds data to later log lines of one channel and shareContext to all channels. Know that both are usually called in a middleware.

for a middle

Explain the __call forwarding to the default channel, how shared context is applied to channels resolved later, and that the per-call array wins on a key clash.

for a senior

Discuss lifetimes: resets in the queue worker and Octane, why neither crosses into queued jobs, and when to reach for the Context facade instead.

for a principal

Weigh a single request-correlation convention for the whole codebase against ad hoc per-channel context, including what named channels and on-demand stacks will miss.

## Two ways to add channel-level context A **channel** in Laravel is an `Illuminate\Log\Logger` built by the `LogManager` from an entry in `config/logging.php`. Each `Logger` holds a `$context` array that is merged into every record it writes. Two public calls fill it: | Call | Where it is implemented | Who receives the data | |---|---|---| | `Log::withContext($array)` | `Logger::withContext()`, reached through `LogManager::__call()` | the **default channel** only (`LOG_CHANNEL`, `stack` in the skeleton) | | `Log::shareContext($array)` | `LogManager::shareContext()` | every channel already resolved, plus every channel resolved later | | `Log::channel('x')->withContext($array)` | `Logger::withContext()` on channel `x` | channel `x` only | The first row is the one people get wrong. `LogManager` has no `withContext` method of its own; its `__call` forwards the call to `$this->driver()`, the default channel. So in this code: ```php Log::withContext(['request_id' => $id]); Log::info('Cart updated.'); // has request_id Log::channel('audit')->info('Price changed.'); // does not ``` the audit entry lacks the request id, because `audit` is a different `Logger` instance. ## How shareContext reaches later channels `LogManager::shareContext()` does two things: 1. loops over `$this->channels` (the channels resolved so far) and calls `withContext()` on each; 2. merges the array into `$this->sharedContext`. Whenever the manager later resolves a channel, it wraps the Monolog instance in a `Logger` and immediately calls `->withContext($this->sharedContext)` on it. `Log::stack([...])` and `Log::build([...])` do the same, so on-demand channels inherit shared data as well. `Log::sharedContext()` returns the current shared array. A **stack** channel is a single `Logger` whose Monolog instance borrows the handlers of its member channels. Context added to the stack's `Logger` therefore reaches every file or service the stack writes to, even though the member channels' own `Logger` objects never saw it. ## Where the data sits in a record `Logger::writeLog()` calls Monolog with `array_merge($this->context, $callContext)`, so: - channel context and per-call context end up together in the record's **`context`** part; - on a key clash the **per-call** value wins for that one entry; - data from the `Context` facade is different: a processor appends it to the record's **`extra`** part. ## Clearing it, and why lifetimes matter - `Log::withoutContext()` (a `LogManager` method) clears the context on every channel resolved so far; pass an array of keys to remove only those. - `Log::flushSharedContext()` empties the manager's shared array, but does not strip data already copied onto resolved channels, which is why the framework pairs it with `withoutContext()`. Under PHP-FPM each request starts a fresh application, so channel context dies with the request. Long-lived processes are different, and the framework resets it for you: 1. the **queue worker**'s reset callback calls `flushSharedContext()` and `withoutContext()` before it fetches each job; 2. **Octane** registers a `FlushLogContext` listener that does the same as each new request is prepared. A consequence: context set with either call in a web request **never travels into a queued job**. The job runs later, in another process, with a fresh logger. Propagating request data into jobs is the job of the `Context` facade, whose data is serialised into the job payload. ## A trap in long-lived processes Because the worker's reset runs before **every** job, including the first, context added once at start-up does not survive into jobs. A `Log::shareContext(['host' => gethostname()])` placed in `AppServiceProvider::boot()` shows up in web requests, where boot runs per request, but is wiped in a `queue:work` process before its first job is fetched. Static, process-wide fields are better added with a Monolog processor, for example through the `processors` option of a `monolog`-driver channel; processors are not cleared between jobs. ## Choosing between them - Use **`Log::withContext()`** in a middleware when the app logs through the default channel and you want, say, a request id on every line. - Use **`Log::shareContext()`** when code also writes to named channels (an audit log, a Slack alert channel) and all of them must carry the same id. - Use the **`Context` facade** when the value must also reach queued jobs or be read back by application code; it appends to every channel's records through a processor rather than through `withContext()`.

  • Why does the queue worker call both flushSharedContext() and withoutContext() between jobs rather than just one?
    `flushSharedContext()` only empties the manager's stored array, so channels resolved later start clean. Channels already resolved hold their own copies inside each `Logger`, which only `withoutContext()` clears. Calling both prevents one job's context leaking into the next job's log lines in the same long-lived worker process.
  • You call Log::withContext() in a middleware, then a controller logs with Log::stack(['single', 'slack']). Is the context there?
    No. `Log::stack()` builds a new on-demand `Logger` and applies only the manager's shared context to it. `Log::withContext()` changed the default channel's `Logger`, a different object. Using `Log::shareContext()` instead would make the id appear on the on-demand stack as well.

saying these in an interview costs you the question

  • Log::withContext() adds data to every configured channel.
  • shareContext only affects channels that were already resolved.
  • Context added with withContext in a request appears in the logs of jobs it dispatches.
  • Channel context overrides a per-call key with the same name.
  • Log context stays set across requests under PHP-FPM unless you clear it.