In a Laravel 13 app, what do LOG_CHANNEL, LOG_STACK and LOG_LEVEL control, and where do log entries go by default?
answer
- config/logging.php 'default' key
- a stack wrapping single
- storage/logs/laravel.log
- LOG_LEVEL=debug in .env.example
- LOG_STACK=daily for rotating files
basics
~10 sLOG_CHANNEL picks the default channel (stack), LOG_STACK lists the channels that stack writes to (single), and LOG_LEVEL is the minimum level for channels that read it (debug). So Log::info() lands in storage/logs/laravel.log.
solid answer
~40 sAll logging is configured in `config/logging.php`. `'default' => env('LOG_CHANNEL', 'stack')` names the channel that `Log::info()` and the exception handler use. The skeleton's `stack` channel reads `LOG_STACK` (default `single`) as a comma-separated list, so by default everything goes through `stack` to `single`, which writes `storage/logs/laravel.log`. `LOG_LEVEL`, `debug` in `.env.example`, is not a global switch: it is the `level` value most skeleton channels read, and each channel drops entries below its own level. To rotate files, set `LOG_STACK=daily` (one file per day, `LOG_DAILY_DAYS` kept, 14 by default) or, since Laravel 13.23, `monthly`. Several channels are a list such as `LOG_STACK=daily,slack`. Level names must be the lowercase PSR-3 names; an invalid value makes the channel fail and Laravel falls back to its emergency logger.
code
ini · 4 linesLOG_CHANNEL=stack
LOG_STACK=daily
LOG_LEVEL=info
LOG_DAILY_DAYS=30go deeper
Know that logs land in storage/logs/laravel.log by default and that LOG_CHANNEL and LOG_STACK choose where they go.
Explain how the stack reads LOG_STACK, how each channel's level filters entries, and how to switch to daily files.
Pick levels and rotation for production, and catch silent emergency-logger fallbacks caused by bad level names or channel typos.
Standardise logging configuration across services so the same variables mean the same thing everywhere they are deployed.
## Where logging is configured Laravel logs through **channels**. A channel is a named entry in `config/logging.php` with a **driver** (how and where to write) and options such as a file path and a minimum level. Under the hood each channel becomes a Monolog logger. Three environment variables in the skeleton's `.env.example` steer the defaults: ```ini LOG_CHANNEL=stack LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug ``` ## What each variable does - **`LOG_CHANNEL`** feeds `'default' => env('LOG_CHANNEL', 'stack')`. The default channel receives everything written through the `Log` facade without an explicit channel, and every exception the handler reports. - **`LOG_STACK`** feeds the skeleton's `stack` channel: `'channels' => explode(',', (string) env('LOG_STACK', 'single'))`. A stack writes each entry to every channel in its list, so `LOG_STACK=daily,slack` means both. - **`LOG_LEVEL`** is read as `'level' => env('LOG_LEVEL', ...)` by most channels in the skeleton: `single`, `daily`, `monthly`, `syslog`, `errorlog`, `stderr`, a remote-syslog example channel and, with a `critical` fallback, `slack`. A channel ignores entries below its level. It is a shared default, not a global filter: a channel that hard-codes its own `level` ignores it. ## The default path, step by step 1. Code calls `Log::info('Driver matched to ride')`. 2. The log manager resolves the default channel, `stack`. 3. The stack lists `single`, from `LOG_STACK`. 4. `single` uses a stream handler on `storage/logs/laravel.log` with level `debug`, so the `info` entry is appended. The same file therefore holds every level from `debug` upward, and it grows until something rotates or truncates it. ## The built-in drivers | Driver | Writes to | Notes | |---|---|---| | `single` | One file | `storage/logs/laravel.log` in the skeleton | | `daily` | One file per day | `max_files` from `LOG_DAILY_DAYS`, 14 in the skeleton | | `monthly` | One file per month | Added in Laravel 13.23; keeps 3 files | | `stack` | Other channels | Fans out; has no level of its own | | `slack` | A Slack incoming webhook | Needs `LOG_SLACK_WEBHOOK_URL` | | `syslog` | The system log | Ident taken from `app.name` | | `errorlog` | PHP's `error_log()` | Wherever the PHP runtime sends it | | `monolog` | Any Monolog handler | The skeleton's `stderr` and `null` channels use it | | `custom` | A factory you write | Full control over the logger | Note that `null` in the skeleton is a *channel* built with the `monolog` driver and Monolog's `NullHandler`, not a driver of its own; it is where deprecations go by default. ## Reading a default log line Laravel formats file and stream entries with Monolog's line formatter, so a line in `laravel.log` looks like: ``` [2026-09-29 10:15:02] local.INFO: Driver accepted ride ``` - The timestamp uses Laravel's `Y-m-d H:i:s` date format. - `local` is the Monolog **channel name**. Unless a channel sets a `name` option, Laravel uses the current environment, so production entries read `production.ERROR`; it is not the name of the `config/logging.php` channel. - `INFO` is the level, followed by the message and, when present, the context array; empty context is omitted. - Exceptions logged by the handler include their stack trace on the following lines. Knowing this helps when searching: `grep 'production.ERROR' storage/logs/laravel.log` finds errors whatever channel wrote them. ## Typical changes - **Rotation for a long-running server:** `LOG_STACK=daily` and, if needed, `LOG_DAILY_DAYS=30`. - **Quieter production logs:** `LOG_LEVEL=info` or `warning`, so `Log::debug()` calls stop being written. - **Adding an alert channel:** `LOG_STACK=daily,slack`, with the Slack channel's level kept at `critical`. - **Containers:** switch to the skeleton's `stderr` channel instead of files. ## Pitfalls - Level names are the lowercase PSR-3 names. `LOG_LEVEL=WARNING` or `LOG_LEVEL=warn` is rejected as an invalid level; the channel cannot be built, and Laravel writes through an **emergency logger** to `storage/logs/laravel.log` instead, adding an `Unable to create configured logger` entry. - A misspelled channel name in `LOG_CHANNEL` or `LOG_STACK` fails the same way. - Setting `LOG_LEVEL=error` silences `Log::info()` everywhere that reads it, which surprises people who wanted only Slack to be quiet. - Leaving `single` in production: one file that grows without limit until the disk fills. Use `daily` or `monthly` with `max_files`, or ship logs off the machine.
- What happens if LOG_STACK names a channel that is not defined?The log manager cannot build that channel, so it falls back to an emergency logger that writes to `storage/logs/laravel.log` (or the `emergency` channel's `path`) and records an 'Unable to create configured logger' entry. Nothing throws, so the typo can go unnoticed.
- Does LOG_LEVEL filter entries for every channel?No. It is only the value that channels read for their own `level` option. A channel with a hard-coded `level`, or one that reads a different variable, is unaffected, and the `slack` channel falls back to `critical` only when `LOG_LEVEL` is unset.
saying these in an interview costs you the question
- LOG_LEVEL is a global filter applied before any channel
- Laravel writes one file per day by default
- LOG_LEVEL=WARNING works because level names are case-insensitive
- A stack channel has its own minimum level
- A typo in LOG_CHANNEL throws an exception on the next log call