skip to content

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%

answer

  1. formatter key on a channel config
  2. formatter_with passes constructor arguments
  3. LOG_STDERR_FORMATTER in the skeleton
  4. context key versus extra key
  5. stack channels borrow member handlers

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.

solid answer

~40 s

In `config/logging.php`, give the writing channel a `formatter` key, for example `'formatter' => Monolog\Formatter\JsonFormatter::class`, plus `formatter_with` for constructor arguments such as `includeStacktraces`. When the key is absent Laravel uses its own `LineFormatter`; `'default'` keeps the handler's built-in formatter. The skeleton's `stderr` channel already reads `LOG_STDERR_FORMATTER`. Since 13.6.0 the framework also ships `Illuminate\Log\Formatters\JsonFormatter`, which adds an exception's context to its serialised form. Each JSON record has `message`, `context`, `level`, `level_name`, `channel`, `datetime` and `extra`: the call's array and `withContext()` data go in `context`, Context facade data in `extra`. Put the formatter on the member channel, not the `stack` entry, which builds no handler of its own.

code

php · 17 lines
php
<?php

// config/logging.php (excerpt)
use Monolog\Handler\StreamHandler;

return [
    'channels' => [
        'stderr' => [
            'driver' => 'monolog',
            'level' => env('LOG_LEVEL', 'debug'),
            'handler' => StreamHandler::class,
            'handler_with' => ['stream' => 'php://stderr'],
            'formatter' => Illuminate\Log\Formatters\JsonFormatter::class,
            'formatter_with' => ['includeStacktraces' => true],
        ],
    ],
];

go deeper

for a junior

Know that config/logging.php controls how each channel formats records and that JSON output is a formatter setting.

for a middle

Explain the formatter and formatter_with keys, the 'default' value, and why Context data shows under extra while call data shows under context.

for a senior

Configure JSON stderr for containers, restore stack traces, avoid the stack-entry trap, and make sure the log pipeline indexes both context and extra.

for a principal

Choose a log format policy per environment, trading human readability against machine indexing and the cost of field cardinality in the log store.

## How Laravel picks a formatter Laravel builds each channel's Monolog handler in `LogManager`, and for every formattable handler it runs `prepareHandler()` with the channel's config. That method looks at one key, `formatter`: | `formatter` value | Result | |---|---| | not set | Laravel's default: a Monolog `LineFormatter` with date format `Y-m-d H:i:s`, inline line breaks allowed, empty context and extra hidden, and stack traces included | | `'default'` | the handler's own built-in formatter is kept | | a class name | the container builds that class, passing `formatter_with` as named constructor arguments | This applies to the built-in file drivers (`single`, `daily`, `monthly`) and to `monolog` driver channels alike. The skeleton's `stderr` channel sets `'formatter' => env('LOG_STDERR_FORMATTER')`, so a container deployment can switch to JSON from the environment; when the variable is unset the value is `null`, which counts as not set. ## Configuring JSON output ```php 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'formatter' => Monolog\Formatter\JsonFormatter::class, 'formatter_with' => ['includeStacktraces' => true], ], ``` Monolog's `JsonFormatter` constructor defaults are worth knowing because they differ from Laravel's line default: - `batchMode` is `BATCH_MODE_JSON` and `appendNewline` is `true`, giving one JSON object per line; - `ignoreEmptyContextAndExtra` is `false`, so empty parts are written as `{}`; - `includeStacktraces` is `false`, so exceptions lose their traces unless you turn it on. **Laravel 13.6.0** added `Illuminate\Log\Formatters\JsonFormatter`, a subclass of Monolog's. When it serialises an exception that the handler is not currently reporting, it merges in the context the exception handler would build for it: the exception's own `context()` output plus any context callbacks registered on the handler. It is not yet described in the logging docs, so name the class explicitly if you want it. ## The shape of a record Every Monolog record has the same parts, and a JSON formatter writes them as keys: ```json {"message":"Charging order.","context":{"order_id":1042},"level":200,"level_name":"INFO","channel":"production","datetime":"2026-09-29T10:15:02.123456+00:00","extra":{"trace_id":"7f3c…"}} ``` - **`context`** holds the per-call array merged over `Log::withContext()` and `Log::shareContext()` data. - **`extra`** holds what processors add; Laravel's `ContextLogProcessor` puts `Context::all()` here, so a `trace_id` from the Context facade appears under `extra`, not `context`. - **Hidden context** appears in neither. Log pipelines that index fields therefore need both `context.*` and `extra.*`; querying only `context.trace_id` misses Context facade data. ## Traps 1. **Formatter on the stack entry.** A `stack` channel's Monolog instance borrows its members' handlers and never runs `prepareHandler()` itself, so `'formatter'` there does nothing. Put it on `single`, `daily` or `stderr`. 2. **Handlers that are not formattable.** `prepareHandler()` skips handlers that do not implement Monolog's formattable interface, so the key has no effect on them. 3. **Placeholders.** Skeleton channels set `replace_placeholders`, which copies `{key}` values into the message; the JSON `context` still holds the original array. 4. **Stack traces.** Switching from Laravel's line default to a bare Monolog `JsonFormatter` silently drops traces until `includeStacktraces` is set. ## Three formatters side by side | Formatter | Where it comes from | Stack traces | Empty context and extra | |---|---|---|---| | Laravel's default `LineFormatter` | used when `formatter` is not set | included | omitted | | `Monolog\Formatter\JsonFormatter` | Monolog, any Laravel version | off unless enabled | written as `{}` | | `Illuminate\Log\Formatters\JsonFormatter` | framework 13.6.0 and later | off unless enabled (inherits Monolog's default) | written as `{}` | ## When to choose JSON Line format is easy to read in a terminal and with `php artisan pail`. JSON is for machines: a log shipper or a container platform reading stderr can index `context` and `extra` fields without parsing text. A common split is line format locally and a JSON `stderr` channel in production, driven by `LOG_CHANNEL` and `LOG_STDERR_FORMATTER`.

  • You add 'formatter' => JsonFormatter::class to the stack channel and nothing changes. Why?
    The `stack` driver builds a Monolog instance from its member channels' existing handlers and processors; it never runs `prepareHandler()`, which is where the `formatter` key is read. The members were already built with their own formatters. Put the key on each member channel, such as `single` or `stderr`.
  • After switching to Monolog's JsonFormatter, exception entries lost their stack traces. What happened?
    Laravel's default line formatter is created with stack traces enabled, but Monolog's `JsonFormatter` constructor defaults `includeStacktraces` to `false`. Pass `'formatter_with' => ['includeStacktraces' => true]` in the channel config, which the container hands to the constructor by parameter name.

saying these in an interview costs you the question

  • Setting formatter on the stack channel changes every member's output.
  • Context facade data is written under the context key in JSON records.
  • Monolog's JsonFormatter includes stack traces by default.
  • Laravel needs a custom driver to change a file channel's formatter.
  • Hidden context appears in extra when using a JSON formatter.