> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Extending and re-resolving log channels in packages

> Choose between Laravel 13's custom/via and LogManager::extend(), and test logging features while accounting for channel retention, forgetChannel(), and fallbacks.

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](https://kawax.biz/en/logging.md). For preparing a provider, see [Package development](https://kawax.biz/en/advanced/package-development.md).

## Choose the extension point you need

If an existing driver can do the job, there is no need to add a custom driver.

| Goal | Option | Where you extend |
| - | - | - |
| Adjust the formatter or other settings of an existing channel | `tap` | The generated `Illuminate\Log\Logger` |
| Select a Monolog Handler through configuration | `driver: monolog` | Settings such as `handler` and `handler_with` |
| Build the logger in a package factory | `driver: custom` and `via` | A class with `__invoke(array $config)` |
| Expose a package-specific driver name | `LogManager::extend()` | A closure that receives the application and the configuration array |

`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.

> [!NOTE]
>
> 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.

```php theme={null}
<?php

namespace Acme\Audit\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;

final class CreateAuditLogger
{
    public function __invoke(array $config): Logger
    {
        return new Logger(
            $config['name'] ?? 'acme-audit',
            [new StreamHandler($config['path'], $config['level'] ?? 'info')],
        );
    }
}
```

In the consuming application, add the following element to the existing `channels` array in `config/logging.php`. Keep the existing `default` and `stack` settings.

```php theme={null}
'audit' => [
    'driver' => 'custom',
    'via' => \Acme\Audit\Logging\CreateAuditLogger::class,
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

`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.

```php theme={null}
use Illuminate\Support\Facades\Log;

Log::channel('audit')->info('Order confirmed.', ['order_id' => 42]);
```

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.

> [!TIP]
>
> 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`.

```php theme={null}
<?php

namespace Acme\Audit;

use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Log\LogManager;
use Illuminate\Support\ServiceProvider;
use Monolog\Logger as Monolog;

class AuditServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('log', static function (LogManager $manager): void {
            $manager->extend('acme-audit', static function (Application $app, array $config): Monolog {
                return $app->make(CreateAuditLogger::class)($config);
            });
        });
    }
}
```

Load the provider through a mechanism such as [package auto-discovery](https://kawax.biz/en/advanced/package-discovery.md). On the consumer side, replace the previous `channels.audit` configuration with the following. `via` is no longer needed.

```php theme={null}
'audit' => [
    'driver' => 'acme-audit',
    'path' => storage_path('logs/audit.log'),
    'level' => 'info',
],
```

* 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](https://kawax.biz/en/advanced/package-service-resolution.md).
* `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.

> [!WARNING]
>
> 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.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"Is audit already resolved?"}
    B -->|Yes| C["Return the retained Logger"]
    B -->|No| D["Select the driver of channels.audit"]
    D --> E["via or extend factory"]
    E --> F["Wrap in Illuminate Logger<br>Apply tap and shared context"]
    F --> G["Retain under the name audit"]
```

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.

```php theme={null}
use Illuminate\Support\Facades\Log;

$before = Log::channel('audit');

config(['logging.channels.audit.level' => 'error']);

$same = Log::channel('audit');

Log::forgetChannel('audit');

$after = Log::channel('audit');
```

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](https://kawax.biz/en/advanced/package-config-merging.md) 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](https://kawax.biz/en/advanced/package-service-lifetimes.md).

### 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](https://kawax.biz/en/advanced/package-testing.md).

Specify `php://memory` as the output destination for testing. Here, we verify channel construction and Handler configuration without actually writing logs.

```php theme={null}
<?php

namespace Acme\Audit\Tests;

use Acme\Audit\AuditServiceProvider;
use Acme\Audit\Logging\CreateAuditLogger;
use Illuminate\Log\Logger;
use Illuminate\Support\Facades\Log;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger as Monolog;
use Orchestra\Testbench\TestCase;

class AuditLoggingTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [AuditServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('logging.channels.audit', [
            'driver' => 'acme-audit',
            'path' => 'php://memory',
            'level' => 'warning',
        ]);

        $app['config']->set('logging.channels.emergency.path', 'php://memory');
    }

    public function test_the_package_driver_creates_the_configured_logger(): void
    {
        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    public function test_configuration_changes_require_a_new_channel(): void
    {
        $before = Log::channel('audit');

        config(['logging.channels.audit.level' => 'error']);

        $this->assertSame($before, Log::channel('audit'));
        $this->assertAuditChannel($before, Level::Warning);

        Log::forgetChannel('audit');

        $after = Log::channel('audit');

        $this->assertNotSame($before, $after);
        $this->assertAuditChannel($after, Level::Error);
        $this->assertAuditChannel($before, Level::Warning);
    }

    public function test_the_same_factory_can_be_used_with_via(): void
    {
        config([
            'logging.channels.audit.driver' => 'custom',
            'logging.channels.audit.via' => CreateAuditLogger::class,
        ]);

        Log::forgetChannel('audit');

        $this->assertAuditChannel(Log::channel('audit'), Level::Warning);
    }

    private function assertAuditChannel(Logger $channel, Level $level): void
    {
        $logger = $channel->getLogger();

        $this->assertInstanceOf(Monolog::class, $logger);
        $this->assertSame('acme-audit', $logger->getName());
        $this->assertCount(1, $logger->getHandlers());

        $handler = $logger->getHandlers()[0];

        $this->assertInstanceOf(StreamHandler::class, $handler);
        $this->assertSame($level, $handler->getLevel());
    }
}
```

> [!WARNING]
>
> `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

| Change or usage | What to verify |
| - | - |
| Adding `tap` | The expected adjustments are applied to the logger returned by the real factory |
| Stack integration | The child channels' Handlers and Processors are included, and the composition is correct after re-resolution |
| Changing the output destination or format | Verify the actual written content, permissions, rotation, and so on in a separate verification environment |
| Changing when registration happens | The driver is registered and channels can be created even if `log` has already been resolved |
| Laravel upgrades | Keep verifying callback arguments and scope, releasing retained entries, and fallback to the emergency logger |

## Primary sources

* [Laravel documentation: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: LogManager creation, extension, and releasing retained channels](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: Logger wrapping and method forwarding](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: LogServiceProvider singleton registration](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogServiceProvider.php)
* [Framework v13.39.0: ServiceProvider::callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Framework v13.39.0: RebindsCallbacksToSelf](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/RebindsCallbacksToSelf.php)
* [Laravel framework tests: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [Advanced Topics](https://kawax.biz/en/advanced/index.md)
- [Logging](https://kawax.biz/en/logging.md)
- [Registering and re-resolving package Storage drivers](https://kawax.biz/en/advanced/package-filesystem-drivers.md)
- [Custom notification channels and delivery testing for packages](https://kawax.biz/en/advanced/package-notification-channels.md)
- [Package service resolution hooks](https://kawax.biz/en/advanced/package-service-resolution.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.