Skip to main content
When a package needs to provide its own log output, separate the mechanism that creates the logger from the channel configuration chosen by the consuming application. Registering a driver does not rebuild existing channels, and an old Handler may still be used after the configuration changes. This page is based on the implementation of Laravel Framework v13.39.0. For basic usage, see Logging. For preparing a provider, see Package development.

Choose the extension point you need

If an existing driver can do the job, there is no need to add a custom driver. via classes and tap classes are resolved from the container, so you can use constructor injection. tap adjusts the channel when it is created; it is not a hook that runs every time a log entry is written.
The audit in logging.channels.audit is the channel name. The driver inside it is the name of the creation method. Log::channel('audit') selects by channel name, so you can keep the name used by callers even if you change the driver later.

Start by exposing a factory with custom and via

The fictional Acme\Audit package provides a factory for file output. Place it in src/Logging/CreateAuditLogger.php, where it can be loaded via PSR-4.
In the consuming application, add the following element to the existing channels array in config/logging.php. Keep the existing default and stack settings.
LogManager::createCustomDriver() retrieves the via class from the container and calls it with the channel’s configuration array as its only argument. With this approach alone, you do not need a provider to register a driver.
This factory explicitly passes level to the StreamHandler. LogManager does not uniformly apply the configured level to the logger returned by a custom factory. If you add your own configuration options, apply them in the factory as well.
If you only want to change the output file, the single driver is enough. This example is a minimal setup for incorporating package-specific Handlers or initialization. Make via a class name that can be stored in the configuration cache, and do not store closures or logger instances in the configuration array.

Use extend when providing a custom driver name

If you want to expose a driver: acme-audit setting to consumers, register a driver that uses the same factory. Extend Laravel’s log service in boot() of src/AuditServiceProvider.php.
Load the provider through a mechanism such as package auto-discovery. On the consumer side, replace the previous channels.audit configuration with the following. via is no longer needed.
  • Laravel’s LogServiceProvider registers log as a singleton. callAfterResolving() waits if it has not been resolved yet, and applies the registration to the instance if it already has been. For details, see Service resolution hooks.
  • extend() only registers a creation closure. No channel is created and nothing is logged when this callback is registered.
  • callCustomCreator() passes $app and the channel configuration array to the closure. These arguments differ from those passed to a via factory.
  • Registering the same driver name again overwrites the creation closure. Do not use built-in names such as single or names that collide with other packages.
In the implementation we checked, an anonymous closure passed to extend() is rebound to the LogManager’s scope. In a non-static closure, $this is not the service provider. As in the example, use a static closure with the $app argument so the implementation does not depend on the provider’s $this.
The package provides the driver, and the consumer decides which channels or stacks use it. Design your provider so that it does not overwrite logging.default or the entire existing logging.channels.

Channels are retained by name

LogManager::get() stores resolved channels in $channels. When you retrieve a channel with the same name, it returns the retained object without rereading the configuration array. This is object retention within the current LogManager, separate from the configuration cache stored in a file. Changing config() or re-registering with extend() alone does not replace Handlers that have already been created.
In this example, $same is the same object as $before. Once forgetChannel('audit') releases the Manager’s retained entry, the next retrieval creates $after from the new configuration.

forgetChannel does not update existing references

All forgetChannel() does is remove the entry inside the Manager.
  • A service that holds $before can continue to use the old logger. This is not an operation that swaps existing references for the new object.
  • A stack already created with Log::channel('stack') or similar has its own independent Monolog that collects the Handlers and Processors of its child channels. Calling forgetChannel() on a child alone does not change the composition of the existing stack. If needed, also release the retained entry for that stack’s channel name and retrieve it again.
  • forgetChannel() itself does not call close() on Handlers. It is not an API that guarantees connections or buffers are shut down.
For normal deployments, include refreshing the configuration cache and restarting any long-running workers you use in your operations. Do not rely on a mechanism that rewrites configuration and re-resolves channels per request, and also consider service lifetimes.

Stack integration also requires Monolog-compatible operations

A custom driver can return an implementation of Psr\Log\LoggerInterface, but Laravel’s createStackDriver() calls getHandlers() and getProcessors() on child channels. These are not part of the PSR interface. Returning an arbitrary PSR logger does not guarantee that it works in a stack, so packages that also support stack integration should return Monolog as in the example, or clearly document and test the supported scope.

Verify actual channel resolution, not just mocks

Log::shouldReceive('channel') alone cannot confirm provider registration, factory selection, or how configuration is applied. The following example runs through the real Manager and factory in a package test environment. Specify php://memory as the output destination for testing. Here, we verify channel construction and Handler configuration without actually writing logs.
LogManager::get() may catch exceptions thrown while creating a channel and return the emergency logger instead. Checking only that “no exception was thrown” or that “it was a LoggerInterface” does not prove that the intended channel was built. The test above also checks the Monolog name, Handler, and log level so that a fallback is not mistaken for success. Write errors that occur after creation are not necessarily caught by the same mechanism.

Additional checks for long-term maintenance

Primary sources

Last modified on October 28, 2026