skip to content

Context Data & Formatters

Laravel adds context to log records per call, per channel with withContext, across channels with shareContext, and via the Context facade into queued jobs. Interviewers probe request correlation.

on this pageshow

explore

questions

5

In Laravel, how do you attach data such as an order id to one Log::info() call, and what must never go into that array?

level: juniorimportance: must knowfreq 55%

answer

  1. second argument of every log method
  2. appended after the message on the line
  3. no automatic redaction of keys
  4. log ids, not whole models or requests
  5. Context::addHidden keeps values out of logs

basics

~20 s

Pass an associative array as the second argument, for example Log::info('Order paid.', ['order_id' => $order->id]); Laravel writes it as JSON after the message. Never put passwords, tokens, card data or a whole $request->all() in it: nothing is redacted.

solid answer

~40 s

Every `Log` method takes a context array as its second argument: `Log::info('Order paid.', ['order_id' => $order->id])`. The default channels format records with Monolog's `LineFormatter`, which prints that array as JSON after the message, and a channel-wide `Log::withContext()` array is merged in first, so a per-call key with the same name wins. Laravel does **not** redact anything in that array: whatever you pass is written to disk or shipped to your log store. So log identifiers and outcomes, not secrets: never passwords, API or remember tokens, session ids, card numbers, or `$request->all()`. Passing a whole Eloquent model also dumps every visible attribute, so log `$user->id`, not `$user`. Data a later step needs but logs must not show belongs in `Context::addHidden()`.

code

php · 13 lines
php
<?php

use Illuminate\Support\Facades\Log;

// Good: explicit ids and outcomes
Log::info('Checkout completed.', [
    'order_id' => $order->id,
    'user_id' => $request->user()->id,
    'items' => $order->items()->count(),
]);

// Bad: dumps every field, including the password on a login form
Log::info('Login attempt.', $request->all());

go deeper

for a junior

Recall that every Log method takes a second array argument and that it is printed after the message. Name at least three kinds of data that never go in it.

for a middle

Explain how the call's array merges with the channel's withContext data, which one wins on a clash, and how Monolog serialises models and exceptions in it.

for a senior

Show you treat logs as a data-exposure surface: explicit keys, ids over objects, hidden context for values later code needs, and a review rule against logging raw input.

for a principal

Frame logging hygiene as policy: what the team may log, how long logs are kept, and who can read them, balanced against the debugging value of rich context.

## The context array on a log call Laravel's `Log` facade forwards to a **channel**, an `Illuminate\Log\Logger` that wraps a Monolog logger. Every level method (`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`) takes the message plus an optional **context array**; `log()` takes the level first, then the same two. ```php use Illuminate\Support\Facades\Log; Log::info('Order paid.', [ 'order_id' => $order->id, 'amount_cents' => $order->total_cents, ]); ``` Inside `Logger::writeLog()` Laravel merges the channel's own context (set with `Log::withContext()`) with the array you passed, using `array_merge($this->context, $context)`. Because the call's array comes second, a per-call key overrides a channel key of the same name. ## Where it shows up The skeleton's file channels (`single`, `daily`, `monthly`) get Laravel's default formatter, a Monolog `LineFormatter` whose format is roughly: ```console [%datetime%] %channel%.%level_name%: %message% %context% %extra% ``` So the line looks like `[2026-09-29 10:15:02] local.INFO: Order paid. {"order_id":1042,"amount_cents":4999}`. Two details worth knowing: - Laravel builds that formatter with **ignore-empty context and extra** switched on, so a call without an array leaves no trailing `[]`. - The `%extra%` slot is where data from the `Context` facade lands; the call's array always sits in `%context%`. Monolog normalises non-scalar values before writing them: - an object implementing `JsonSerializable` (an **Eloquent model** or a collection) is written through its serialised form, so a model contributes every visible attribute and loaded relation, leaving out only what `$hidden` lists; - a `Throwable` is expanded into class, message, file and trace; - other objects are JSON-encoded or cast to string. ## What never belongs in the array Laravel has **no automatic redaction** for log context. A key called `password` is written exactly like `order_id`. Logs are copied to more places, kept longer and read by more people than the database, so treat every context array as published. | Never log | Log instead | |---|---| | Passwords, even failed attempts | the user id and the outcome | | API keys, bearer tokens, remember tokens, reset tokens | the token's name or last four characters | | Session ids and CSRF tokens | nothing; they identify a live session | | Card numbers, CVV, full bank details | the payment provider's reference | | `$request->all()` or the raw request body | the specific, validated fields you need | | A whole `$user` or `$order` model | `$user->id`, `$order->id` | A few habits keep this true in practice: 1. **Build the array by hand.** Pick keys explicitly; never spread an input array into it. 2. **Prefer ids over objects.** An id lets you look the record up with proper access control; a dumped model leaks email addresses and whatever columns are added next year. 3. **Keep secrets you still need in hidden context.** `Context::addHidden('payment_intent', $id)` makes the value available to later code and queued jobs through `Context::getHidden()` without appending it to log records. Its value parameter is also marked `#[\SensitiveParameter]`, so PHP masks it in stack traces. ## Levels still matter Context does not bypass the channel's minimum level. With `LOG_LEVEL=info`, a `Log::debug()` call with a large array is dropped before formatting, which is why noisy diagnostic arrays belong at `debug` and should not be relied on in production. ## Seeing what you actually wrote Before trusting a context array in production, look at the real output: - the skeleton's default `stack` channel writes through `single` to `storage/logs/laravel.log`; - `php artisan pail` (Laravel Pail, a dev dependency of the skeleton) streams new entries live in the terminal, which is the quickest way to check that a checkout line carries the keys you expect; - every write also fires the `Illuminate\Log\Events\MessageLogged` event with the level, message and merged context array, which `Log::listen()` can observe, for example in a test that asserts a secret never appears. ## Why interviewers ask it The question checks two things at once: that you know the context array exists (so you do not glue ids into the message string, which makes logs impossible to filter), and that you treat log output as a data-exposure surface. A candidate who answers only the first half is the one who later ships a `Log::info('Login', $request->all())` line with a password in it.

  • If Log::withContext() already set 'order_id' => 1 and a call passes ['order_id' => 2], which value is written?
    The call's value, 2. `Logger::writeLog()` merges the channel context first and the per-call array second with `array_merge`, so a duplicate key from the call overwrites the channel's value for that one entry. The channel context itself is not changed, so later entries still show 1.
  • How would you keep a payment reference available to a queued job without it appearing in any log line?
    Store it with `Context::addHidden('payment_ref', $ref)` and read it back with `Context::getHidden('payment_ref')`. Hidden context travels with dispatched jobs like normal context but is never appended to log records. It is still serialised into the queue payload in plain form, so a real secret such as a card number should not go there either.

saying these in an interview costs you the question

  • Laravel masks keys like password in log context automatically.
  • Concatenating ids into the message string is just as searchable as a context array.
  • Logging $request->all() is fine because the log file is private.
  • Passing a whole Eloquent model only logs its id.
  • The context array is written even when the message is below LOG_LEVEL.
open as a page

In a Laravel checkout, a trace id set with Log::withContext() appears in request logs but not in logs of the queued jobs it dispatches. Why, and how do you fix it?

level: seniorimportance: must knowfreq 40%

basics

~20 s

Log::withContext() only changes the web process's logger; the job runs later in a worker with a fresh one. Put the id in the Context facade instead: Laravel serialises Context into each job payload and restores it before the job runs.

open as a page

How do you make a Laravel 13 app write JSON log records, and where do per-call context and Context facade data land in each record?

level: middleimportance: should knowfreq 30%

basics

~20 s

Set a channel's 'formatter' option in config/logging.php to a JSON formatter class, such as Monolog's JsonFormatter or Laravel's own since 13.6. Per-call and withContext data land under the record's context key; Context facade data lands under extra.

open as a page

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%

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.

open as a page

In Laravel, how do Context::addHidden() and the Context::dehydrating() and hydrated() callbacks let a queued job restore request state such as the locale without logging it?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Context::addHidden() stores values that travel into queued jobs but are never appended to log records. A Context::dehydrating() callback adds request state such as app.locale to the copy sent with the job; a Context::hydrated() callback restores it in the worker.

open as a page