In Laravel, how do Log::withContext() and Log::shareContext() differ, and which log channels receive the data each one adds?
answer
- default channel versus every channel
- LogManager forwards unknown calls to default driver
- sharedContext applied to channels built later
- withoutContext() and flushSharedContext()
- worker clears both between jobs
basics
~10 sLog::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
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
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.
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.
Discuss lifetimes: resets in the queue worker and Octane, why neither crosses into queued jobs, and when to reach for the Context facade instead.
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.