skip to content

In Laravel 13, how do you control the log level and the extra context an exception's default log entry carries?

level: middleimportance: should knowfreq 28%

answer

  1. default level is error
  2. level(PDOException::class, LogLevel::CRITICAL)
  3. first matching registration wins
  4. context() method on the exception class
  5. userId added when someone is logged in

basics

~10 s

$exceptions->level(Type::class, LogLevel::WARNING) sets the PSR-3 level for a type; unmapped exceptions log at error. Context comes from the exception's context() method and $exceptions->context() closures, plus the logged-in user's id and the exception itself.

solid answer

~40 s

When an exception reaches Laravel's default log path, the handler calls the logger with `$e->getMessage()` at a level and with a context array. `$exceptions->level(PDOException::class, LogLevel::CRITICAL)` maps a type to a PSR-3 level; unmapped exceptions log at `error`. The lookup takes the **first** registration the exception is an `instanceof`, in registration order, so register specific classes before broad ones. The context array starts with the exception's own public `context()` method, if any, merges each closure registered with `$exceptions->context()`, adds `userId` when `Auth::id()` returns one, and finally the `exception` object. Level matters beyond labelling, because channels have minimum levels: raising a type to `critical` can send it to a channel that only takes critical entries. None of this applies when the exception's `report()` method or a stopped report callback handles it first.

code

php · 22 lines
php
<?php

namespace App\Exceptions;

use RuntimeException;

class SmsGatewayException extends RuntimeException
{
    public function __construct(
        string $message,
        public readonly string $provider,
        public readonly string $smsId,
    ) {
        parent::__construct($message);
    }

    /** @return array<string, mixed> */
    public function context(): array
    {
        return ['provider' => $this->provider, 'sms_id' => $this->smsId];
    }
}

go deeper

for a junior

Remember reported exceptions are logged at error unless level() in bootstrap/app.php says otherwise.

for a middle

Explain first-match ordering for level(), and how the exception's context() and $exceptions->context() build the entry's context.

for a senior

Use levels to route alerts, keep noisy external failures below paging level, and keep secrets out of exception context.

for a principal

Agree a severity policy across services so critical means the same thing everywhere alerts are routed.

## What the default log entry is made of When nothing earlier handles a reported exception (it is not filtered or throttled, its own `report()` method does not claim it, and no report callback stops the chain), Laravel's handler writes it to the default logger. The call is effectively: ```php $logger->{$level}($e->getMessage(), $context); ``` Two things are under your control: the **level**, and the **context array** attached to the entry. ## Setting the level with level() Every PSR-3 logger has eight severities, from `debug` to `emergency`. Laravel logs a reported exception at **`error`** unless you map its type in `bootstrap/app.php`: ```php $exceptions->level(PDOException::class, LogLevel::CRITICAL); $exceptions->level(SmsGatewayException::class, LogLevel::WARNING); ``` `level(string $type, string $level)` stores the pair in a map. At report time the handler walks the map **in registration order** and takes the first entry where the exception is an `instanceof` the registered type. There is no "most specific class wins" rule: | Registered order | Exception thrown | Level used | |---|---|---| | `RuntimeException` → warning, then `SmsGatewayException` → critical | `SmsGatewayException` (extends `RuntimeException`) | warning | | `SmsGatewayException` → critical, then `RuntimeException` → warning | `SmsGatewayException` | critical | | nothing registered | any | error | So register narrow classes before broad ones. ## Why the level matters The level is not only a label. Laravel log channels can declare a minimum `level`, so the same entry may reach some channels and not others. Typical uses: - Raise database connection failures to `critical` so they reach an alerting channel configured for critical entries only. - Lower a noisy, well-understood external failure, such as a flaky SMS gateway, to `warning` so it stays in the file log without paging anyone. - Keep the application's default `error` for everything unexpected. ## Adding context The context array is built in this order, with later string keys overriding earlier ones: 1. **The exception's own `context()` method.** If the class defines a public `context(): array`, its result is the starting array. Use it for data that belongs to the failure, such as the provider and the SMS message id. 2. **Closures registered with `$exceptions->context()`.** Each receives the exception and the array so far, and its result is merged in. Use it for data you want on every exception entry, such as the environment name. 3. **The user id.** If an authenticated user is present, the handler adds `userId` from `Auth::id()`; with no user, the key is absent. 4. **The exception itself** under the `exception` key, which formatters use to print the class, file, line and stack trace. ## A worked example Take the `SmsGatewayException` from the code examples below, with `level(SmsGatewayException::class, LogLevel::WARNING)` and a global `context()` closure that adds the environment name. A logged-in user with id 42 triggers it while the provider is timing out. The default log path then produces one entry: - **level:** `warning`, from the first matching `level()` registration; - **message:** the exception's message, for example `Gateway timed out`; - **context:** `provider` and `sms_id` from the class's `context()` method, `app_env` from the closure, `userId` 42 from the authenticated user, and `exception` holding the object. A channel whose minimum level is `error` skips this entry entirely, while a file channel at `debug` records it. If the same exception were thrown by a queued job with no authenticated user, the entry would be identical except that `userId` would be missing, which is a useful hint when reading logs: its absence does not always mean an anonymous web visitor. ## When none of this applies Level and context belong to the **default log path**. They are skipped when: - the exception's own `report()` method returns anything but `false`; - a report callback chained with `stop()`, or returning `false`, handles it; - deduplication, the ignore rules or `throttle()` filter it out. A report callback that forwards the exception elsewhere must build its own payload; it can call `$e->context()` itself if the method exists. ## Common mistakes - Registering a broad class first and expecting a later, narrower `level()` to win. - Putting secrets or full request bodies into `context()`, where they end up in every log destination. - Expecting `level()` to change what a stopped report callback sends. - Assuming an unmapped exception is logged at the channel's configured minimum level; it is logged at `error`.

  • Why can level(Exception::class, 'warning') registered first make database errors log as warnings?
    The handler takes the first `level()` registration the exception is an `instanceof`, in registration order. `PDOException` extends `Exception`, so the broad entry matches before a later `PDOException` entry is considered. Register the narrow class first.
  • Does level() change anything for an exception a report callback handled with stop()?
    No. The level and the context array are only used on the default log path. Once the exception's own `report()` or a stopped report callback handles it, the handler returns before choosing a level or building context.

saying these in an interview costs you the question

  • Unmapped exceptions are logged at the channel's minimum level
  • The most specific level() registration always wins
  • level() also changes the HTTP status of the response
  • The context() method is only used when APP_DEBUG is true
  • A stopped report callback still gets the mapped level and context