skip to content

In Laravel, how do you send critical errors to Slack and everything else to daily files with a stack channel, and what decides which channel writes each entry?

level: middleimportance: must knowfreq 55%

answer

  1. stack merges its channels' handlers
  2. each channel's 'level' is a minimum
  3. skeleton slack level reads LOG_LEVEL first
  4. LOG_LEVEL=debug floods the chat
  5. ignore_exceptions and handler order

basics

~20 s

List daily and slack in the stack channel, give slack a level of critical and daily a lower one. The stack offers every entry to each channel, and a channel writes it only at or above its own minimum level.

solid answer

~40 s

A `stack` channel collects the Monolog handlers of the channels it lists into one logger, so every entry is offered to each of them in list order. Each handler has its own minimum `level` and writes only entries at or above it; the stack itself has no level. For a ride-hailing app: `'channels' => ['daily', 'slack']`, `daily` at `info`, `slack` at `critical`. Then `Log::error()` reaches only the files and `Log::critical()` reaches both. The trap is the skeleton's `slack` entry: `'level' => env('LOG_LEVEL', 'critical')`. `.env.example` sets `LOG_LEVEL=debug`, so Slack gets everything; hard-code `'critical'`. Also set `'ignore_exceptions' => true`, or a failing webhook throws out of the log call and skips the handlers after it. Remember reported exceptions log at `error` unless their level is raised.

code

php · 28 lines
php
<?php

// config/logging.php (excerpt)
return [
    'default' => env('LOG_CHANNEL', 'stack'),

    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['daily', 'slack'],
            'ignore_exceptions' => true,
        ],

        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => env('LOG_LEVEL', 'info'),
            'max_files' => 14,
        ],

        'slack' => [
            'driver' => 'slack',
            'url' => env('LOG_SLACK_WEBHOOK_URL'),
            'username' => 'Dispatch Alerts',
            'level' => 'critical', // not env('LOG_LEVEL', ...)
        ],
    ],
];

go deeper

for a junior

Know that a stack sends each entry to several channels and each channel has a minimum level.

for a middle

Explain handler merging, per-channel thresholds, and why the skeleton's slack level follows LOG_LEVEL.

for a senior

Harden alerting: explicit Slack level, ignore_exceptions for network handlers, and exception levels that actually reach the alert channel.

for a principal

Define which severities page people and which only persist, and keep that contract the same across services.

## What a stack really is In `config/logging.php`, a channel with `'driver' => 'stack'` is not a destination. When Laravel builds it, the log manager resolves each listed channel, takes its Monolog **handlers** (and processors) and puts them all into one new Monolog logger. Writing to the stack means offering the entry to each of those handlers in the order the channels are listed. Each handler carries the `level` from its own channel config and writes an entry only when the entry's level is **at or above** that minimum. The stack has no `level` option of its own. So routing by severity is simply a matter of giving each member channel the right threshold. ## Routing for a ride-hailing app The goal: everything from `info` upward in rotating files for investigation, and only `critical` and above in the dispatch team's Slack channel. ```php 'stack' => [ 'driver' => 'stack', 'channels' => ['daily', 'slack'], 'ignore_exceptions' => true, ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Dispatch Alerts', 'level' => 'critical', ], ``` With `daily` at `info`: | Call | daily | slack | |---|---|---| | `Log::debug('Heartbeat')` | dropped | dropped | | `Log::info('Ride completed')` | written | dropped | | `Log::error('Payout API timeout')` | written | dropped | | `Log::critical('Dispatch queue stalled')` | written | posted | | `Log::emergency('Database unreachable')` | written | posted | ## The skeleton's Slack trap The skeleton defines Slack with `'level' => env('LOG_LEVEL', 'critical')`. The `critical` is only a **fallback** for when `LOG_LEVEL` is unset, and `.env.example` sets `LOG_LEVEL=debug`. Add `slack` to the stack without editing that line and the chat receives every debug and info entry, often thousands an hour. Fixes: - hard-code `'level' => 'critical'` in the Slack channel; - or read a separate environment variable that you add for Slack alone; - and never rely on the documented "critical by default" without checking your `.env`. ## Failures inside the stack A handler that throws, such as a Slack webhook timing out, is not isolated by default: 1. Monolog rethrows the exception, so the `Log::...` call itself throws. 2. Handlers listed after the failing one never see the entry. 3. If the log call was the exception handler reporting an error, the report now fails too. `'ignore_exceptions' => true` wraps the stack's handlers in Monolog's `WhatFailureGroupHandler`, which swallows handler failures and carries on. The skeleton ships `false`, so either set it for stacks that include network handlers or list file handlers first so they always write. The file channels' `bubble` option (default `true`) can also stop later handlers if set to `false`. ## Getting exceptions to the alert channel Reported exceptions are logged at `error` by default, which is below `critical`, so an unhandled exception does **not** reach a critical-only Slack channel. Either lower Slack's threshold to `error`, which is noisy for flaky dependencies, or raise specific exception types to `critical` with the exception handler's per-type level mapping in `bootstrap/app.php`. ## Keeping debug detail only when something goes wrong Laravel's log manager also understands an `action_level` option on the `single`, `daily`, `monthly`, `slack`, `syslog` and `monolog` drivers. When it is present, the channel's handler is wrapped in Monolog's `FingersCrossedHandler`: 1. Entries below `action_level` are held in a buffer instead of written. 2. When an entry at or above `action_level` arrives, the buffered entries are flushed first, then the triggering entry. 3. With the default `stop_buffering`, entries after that pass straight through instead of being buffered again. For a ride-hailing app this means a failed ride match writes the debug trail that led to it, while successful matches write nothing. Two cautions apply: the wrapped handler's own `level` must be low enough to accept the flushed entries (`debug`), and the buffer is unbounded, so in a long-running queue worker it can grow until an error flushes it. ## Checklist - Each member channel has an explicit, intended `level`. - `LOG_SLACK_WEBHOOK_URL` is set in every environment whose stack includes `slack`; without a URL the Slack channel cannot be built, and Laravel substitutes its emergency logger, a debug-level file handler on `laravel.log`, for that member. - `ignore_exceptions` is `true` for stacks with network handlers. - `Log::critical()` is reserved for events a person must act on now.

  • Why does an unhandled exception not appear in the critical-only Slack channel?
    Laravel's exception handler logs reported exceptions at `error` unless a per-type level is configured in `bootstrap/app.php`. An `error` entry is below Slack's `critical` minimum, so only the file channels write it.
  • What does 'ignore_exceptions' => true change in a stack?
    It wraps the stack's handlers in Monolog's `WhatFailureGroupHandler`, so a handler that throws, such as a timed-out webhook, is skipped instead of making the log call throw and stopping the handlers after it.

saying these in an interview costs you the question

  • The skeleton's Slack channel only ever posts critical entries
  • A stack channel filters entries by its own level first
  • A failing Slack webhook is always ignored by the stack
  • Unhandled exceptions are logged at critical by default
  • Each entry goes to only the first channel that accepts it