In Laravel's config/logging.php, how do tap classes and the monolog and custom drivers let you customise a log channel beyond the built-in drivers?
answer
- 'tap' => [Class::class] on a channel
- __invoke(Illuminate\Log\Logger $logger)
- monolog driver: handler plus handler_with
- custom driver: via factory returns a logger
- Log::extend() registers a new driver
basics
~20 sA tap class on a channel receives the built Illuminate\Log\Logger once, to add processors or adjust handlers. The monolog driver builds any Monolog handler from handler and handler_with; a custom driver's via factory returns a whole Monolog logger.
solid answer
~40 sLaravel offers three extension points below the built-in drivers. A **tap**: add `'tap' => [App\Logging\AddRequestUid::class]` to a channel, and when the channel is first resolved Laravel builds the class through the container and calls `__invoke(Illuminate\Log\Logger $logger)`; the logger proxies to Monolog, so you can call `pushProcessor()` or loop over `getHandlers()`. A string such as `Class:arg1,arg2` passes extra arguments. The **monolog driver** instantiates any class implementing Monolog's `HandlerInterface` from `handler`, with constructor arguments from `handler_with`, plus optional `processors`; the skeleton's `stderr` and `null` channels use it. The **custom driver** calls the `via` factory's `__invoke(array $config)`, which must return a Monolog logger, for full control. For a reusable driver name, `Log::extend('name', fn ($app, array $config) => ...)` registers a creator.
code
php · 15 lines<?php
namespace App\Logging;
use Illuminate\Log\Logger;
use Monolog\Processor\UidProcessor;
class AddRequestUid
{
public function __invoke(Logger $logger, string $length = ''): void
{
// Laravel passes '' when the tap entry has no ':arguments' part.
$logger->pushProcessor(new UidProcessor($length === '' ? 8 : (int) $length));
}
}go deeper
Know that config/logging.php can customise channels beyond the built-in drivers.
Explain what a tap receives, how handler and handler_with build a monolog-driver channel, and what a custom via factory returns.
Pick the lightest extension point, keep customisation in config where possible, and account for taps running once per long-lived process.
Package shared logging behaviour as one driver or tap used by every service instead of per-app copies.
## When the built-in drivers are not enough The built-in drivers (`single`, `daily`, `monthly`, `slack`, `syslog`, `errorlog`, `stack`) cover files, the system log and Slack. Real systems also need things such as an extra field on every entry, a handler Laravel has no driver for, or a logger assembled in code. Laravel exposes Monolog at three levels of control. | Tool | You provide | Laravel builds | Typical use | |---|---|---|---| | `tap` | A class with `__invoke(Logger $logger)` | The channel as usual, then calls your class | Add processors, adjust existing handlers | | `monolog` driver | A handler class and its arguments | The handler and the logger around it | Any Monolog handler without a Laravel driver | | `custom` driver | A factory class | Nothing; your factory returns the logger | Complete control | ## Tap classes A tap customises a channel after Laravel has built it: ```php 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'tap' => [App\Logging\AddRequestUid::class], ], ``` - The class is resolved from the **service container**, so its constructor can type-hint dependencies. - Its `__invoke()` receives an `Illuminate\Log\Logger`, which forwards unknown method calls to the underlying Monolog logger, so `pushProcessor()`, `pushHandler()` and `getHandlers()` all work. - A tap entry may carry arguments: `'App\Logging\AddRequestUid:12'` calls `__invoke($logger, '12')`, splitting further arguments on commas. - It runs once, when the channel is first resolved; the channel is then cached for the rest of the process. Formatting choices such as switching to JSON are also often done in a tap, but log formatters and context are their own subject; the mechanism is the same. ## The monolog driver The `monolog` driver builds a channel around **any** Monolog handler: 1. `handler` names a class that must implement `Monolog\Handler\HandlerInterface`; anything else throws when the channel is built. 2. `handler_with` supplies constructor arguments by name, merged with the channel's `level`. 3. `processors` lists processor classes, or `['processor' => Class::class, 'with' => [...]]` entries for ones that need arguments. 4. `formatter` and `formatter_with` choose the formatter; `'formatter' => 'default'` keeps the handler's own. The skeleton itself uses this driver for `stderr` (`StreamHandler` on `php://stderr`), for `null` (`NullHandler`) and for a remote-syslog example channel (`SyslogUdpHandler`). ## The custom driver With `'driver' => 'custom'` and `'via' => App\Logging\CreateDispatchLogger::class`, Laravel resolves the factory and calls `__invoke(array $config)` with the channel's config array. The factory must return a Monolog `Logger`, and it decides every handler, processor and formatter. Laravel still wraps the result in its own `Logger` and applies any `tap` classes listed on the channel. ## Registering a driver name with Log::extend() When several channels should share a new driver name, register a creator, typically in a service provider's `boot()`: ```php Log::extend('dispatch', function ($app, array $config) { return new Monolog\Logger('dispatch', [/* handlers */]); }); ``` Channels can then use `'driver' => 'dispatch'`. The closure is bound to the log manager and receives the application and the channel config. ## Handlers, processors and formatters The three tools act on Monolog's three building blocks, which are worth defining precisely: - A **handler** decides where a record goes and whether it is accepted, using its minimum level: a file, a stream, syslog, a webhook, or nowhere. - A **processor** is a callable that receives each record before handlers write it and can add data to its `extra` array, such as a request id, the hostname or memory usage. - A **formatter** turns a record into the final text or JSON that a handler writes. A channel is a Monolog logger holding a list of handlers and processors. Taps usually add processors or change formatters on existing handlers; the `monolog` driver chooses the handler; a custom factory chooses everything. ## Choosing - Adding data to every entry of an existing channel: a **tap** with a processor. - A Monolog handler Laravel has no driver for: the **monolog driver**, purely in config. - A logger that needs code to assemble, or that several apps share: a **custom** factory or `Log::extend()`. ## Pitfalls - Expecting a tap to run per request; under a long-running worker it runs once per process. - Putting a non-handler class in `handler`, which fails channel creation and sends entries to the emergency logger. - Returning a handler instead of a logger from a `via` factory.
- What does the via factory of a custom driver receive, and what must it return?Laravel resolves the `via` class and calls `__invoke(array $config)` with the channel's config array. It must return a Monolog logger; Laravel then wraps it and applies any taps listed on the channel.
- How do you pass arguments to a tap class?Append them after a colon in the tap entry, separated by commas: `'App\\Logging\\AddRequestUid:12'`. Laravel calls `__invoke($logger, '12')`; the arguments arrive as strings.
saying these in an interview costs you the question
- A tap class receives each log record before it is written
- The monolog driver accepts any class, even one that is not a handler
- A custom driver's via factory returns a Monolog handler
- Tap classes are instantiated with new, so they cannot use dependencies
- Taps run again for every log entry