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

# Log-Channels in Paketen erweitern und neu auflösen

> Setzt custom/via und LogManager::extend() in Laravel 13 gezielt ein und testet die Logging-Funktion unter Berücksichtigung zwischengespeicherter Channels, forgetChannel() und Fallbacks.

Wenn ein Paket eine eigene Log-Ausgabe bereitstellen soll, trennen Sie **den Mechanismus, der den Logger erzeugt**, von **der Channel-Konfiguration, die die nutzende Anwendung wählt**. Das bloße Registrieren eines Treibers baut bestehende Channels nicht neu auf, und auch nach einer Konfigurationsänderung kann weiterhin der alte Handler verwendet werden.

Diese Seite basiert auf der Implementierung von Laravel Framework `v13.39.0`. Die Grundlagen finden Sie unter [Logging](https://kawax.biz/de/logging.md), die Vorbereitung des Providers unter [Paketentwicklung](https://kawax.biz/de/advanced/package-development.md).

## Den passenden Erweiterungspunkt wählen

Wenn sich das Ziel mit einem vorhandenen Treiber erreichen lässt, müssen Sie keinen eigenen Treiber hinzufügen.

| Ziel | Option | Erweiterungsort |
| - | - | - |
| Formatter usw. eines bestehenden Channels anpassen | `tap` | Der erzeugte `Illuminate\Log\Logger` |
| Einen Monolog-Handler per Konfiguration wählen | `driver: monolog` | Einstellungen wie `handler` und `handler_with` |
| Den Logger in einer Factory des Pakets zusammensetzen | `driver: custom` und `via` | Eine Klasse mit `__invoke(array $config)` |
| Einen paketspezifischen Treibernamen veröffentlichen | `LogManager::extend()` | Eine Closure, die die Anwendung und das Konfigurations-Array erhält |

Die Klassen für `via` und `tap` werden über den Container aufgelöst, sodass auch Konstruktorinjektion möglich ist. `tap` ist eine Anpassung bei der Erzeugung des Channels und kein Hook, der bei jedem einzelnen Log-Eintrag ausgeführt wird.

> [!NOTE]
>
> `audit` in `logging.channels.audit` ist der **Channel-Name**. Das darin enthaltene `driver` ist der **Name der Erzeugungsmethode**. Mit `Log::channel('audit')` wählen Sie den Channel-Namen, sodass der Name auf Aufruferseite erhalten bleibt, auch wenn Sie später den Treiber ändern.

## Zuerst eine Factory mit custom und via veröffentlichen

Im fiktiven Paket `Acme\Audit` stellen wir eine Factory für die Ausgabe in eine Datei bereit. Sie wird in `src/Logging/CreateAuditLogger.php` abgelegt, das per PSR-4 geladen werden kann.

```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 der nutzenden Anwendung fügen Sie dem bestehenden `channels`-Array in `config/logging.php` das folgende Element hinzu. Die vorhandenen Einstellungen für `default` und `stack` bleiben erhalten.

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

`LogManager::createCustomDriver()` bezieht die Klasse aus `via` über den Container und ruft sie mit dem Konfigurations-Array des Channels als einzigem Argument auf. Bei diesem Ansatz allein ist kein Provider für die Treiberregistrierung nötig.

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

Log::channel('audit')->info('Bestellung wurde bestätigt.', ['order_id' => 42]);
```

Diese Factory übergibt `level` explizit an den StreamHandler. **Der LogManager wendet das `level` aus der Konfiguration nicht pauschal auf den Logger an, den eine `custom`-Factory zurückgibt.** Auch wenn Sie eigene Konfigurationsoptionen hinzufügen, müssen diese in der Factory berücksichtigt werden.

> [!TIP]
>
> Wenn Sie nur das Ausgabeziel der Datei ändern möchten, genügt der Treiber `single`. Dieses Beispiel ist die minimale Konfiguration, um paketeigene Handler oder Initialisierungen einzubinden. Geben Sie für `via` einen Klassennamen an, der im Konfigurations-Cache gespeichert werden kann, und legen Sie im Konfigurations-Array keine Closures oder Logger-Instanzen ab.

## Für eigene Treibernamen extend verwenden

Wenn Sie der nutzenden Seite eine Konfiguration wie `driver: acme-audit` anbieten möchten, registrieren Sie einen Treiber, der dieselbe Factory verwendet. In `boot()` von `src/AuditServiceProvider.php` erweitern Sie den `log`-Service von Laravel.

```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);
            });
        });
    }
}
```

Der Provider wird etwa über die [Paket-Erkennung](https://kawax.biz/de/advanced/package-discovery.md) geladen. Auf der nutzenden Seite ersetzen Sie `channels.audit` durch die folgende Konfiguration anstelle der vorherigen. `via` wird nicht mehr benötigt.

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

* Der `LogServiceProvider` von Laravel registriert `log` als Singleton. `callAfterResolving()` wartet, solange der Service noch nicht aufgelöst ist, und wendet die Registrierung auch auf die Instanz an, wenn er bereits aufgelöst wurde. Details finden Sie unter [Hooks nach der Service-Auflösung](https://kawax.biz/de/advanced/package-service-resolution.md).
* `extend()` registriert lediglich die Erzeugungs-Closure. Beim Registrieren dieses Callbacks werden weder Channels erzeugt noch Logs geschrieben.
* `callCustomCreator()` übergibt der Closure `$app` und das Konfigurations-Array des Channels. Das unterscheidet sich von den Argumenten, die an eine `via`-Factory übergeben werden.
* Eine erneute Registrierung desselben Treibernamens überschreibt die Erzeugungs-Closure. Verwenden Sie keine eingebauten Namen wie `single` und keine Namen, die mit anderen Paketen kollidieren.

> [!WARNING]
>
> In der geprüften Implementierung wird eine an `extend()` übergebene anonyme Closure an den Scope des LogManagers neu gebunden. `$this` in einer nicht-statischen Closure ist nicht der Service Provider. Verwenden Sie wie im Beispiel eine statische Closure mit dem Argument `$app` und machen Sie die Implementierung nicht von `$this` des Providers abhängig.

Das Paket stellt den Treiber bereit, und die nutzende Seite entscheidet, in welchem Channel oder Stack er verwendet wird. Gestalten Sie den Provider so, dass er weder `logging.default` noch die gesamten bestehenden `logging.channels` überschreibt.

## Channels werden pro Name zwischengespeichert

`LogManager::get()` speichert aufgelöste Channels in `$channels`. Wird ein Channel mit demselben Namen erneut abgerufen, liest der Manager das Konfigurations-Array nicht neu ein, sondern gibt das zwischengespeicherte Objekt zurück.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"audit bereits aufgelöst?"}
    B -->|Yes| C["Zwischengespeicherten Logger zurückgeben"]
    B -->|No| D["driver aus channels.audit wählen"]
    D --> E["Factory über via oder extend"]
    E --> F["Mit Illuminate Logger umhüllen<br>tap und gemeinsamen Kontext anwenden"]
    F --> G["Unter dem Namen audit speichern"]
```

Dies ist nicht der in Dateien gespeicherte Konfigurations-Cache, sondern **das Vorhalten von Objekten im aktuellen LogManager**. Eine Änderung über `config()` oder eine erneute Registrierung mit `extend()` allein tauscht bereits erzeugte Handler nicht aus.

```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 diesem Beispiel ist `$same` dasselbe Objekt wie `$before`. Sobald `forgetChannel('audit')` den Eintrag im Manager entfernt, wird beim nächsten Abruf `$after` aus der neuen Konfiguration erzeugt.

### forgetChannel aktualisiert keine bestehenden Referenzen

`forgetChannel()` entfernt lediglich den Eintrag im Manager.

* Services, die `$before` halten, können den alten Logger weiterhin verwenden. Die Operation ersetzt bestehende Referenzen nicht durch das neue Objekt.
* Ein bereits erzeugter Stack, etwa über `Log::channel('stack')`, besitzt ein eigenständiges Monolog, das die Handler und Processors der untergeordneten Channels sammelt. Wenn Sie nur einen untergeordneten Channel mit `forgetChannel()` entfernen, ändert sich die Zusammensetzung des bestehenden Stacks nicht. Entfernen Sie bei Bedarf auch den Eintrag für den Channel-Namen des Stacks und rufen Sie ihn neu ab.
* `forgetChannel()` selbst ruft `close()` der Handler nicht auf. Die API garantiert nicht das Beenden von Verbindungen oder Puffern.

Nehmen Sie bei gewöhnlichen Deployments die [Aktualisierung des Konfigurations-Caches](https://kawax.biz/de/advanced/package-config-merging.md) und den Neustart verwendeter langlebiger Worker in den Betriebsablauf auf. Verlassen Sie sich nicht auf einen Mechanismus, der die Konfiguration pro Request umschreibt und neu auflöst, und berücksichtigen Sie auch den [Lebenszyklus von Services](https://kawax.biz/de/advanced/package-service-lifetimes.md).

### Für die Einbindung in einen Stack sind auch Monolog-kompatible Operationen nötig

Ein eigener Treiber kann eine Implementierung von `Psr\Log\LoggerInterface` zurückgeben, doch `createStackDriver()` von Laravel ruft bei untergeordneten Channels `getHandlers()` und `getProcessors()` auf. Diese sind nicht Teil des PSR-Interfaces.

Da ein beliebiger PSR-Logger nicht zwangsläufig auch in einem Stack funktioniert, sollten Pakete, die auch die Einbindung in einen Stack unterstützen, wie im Beispiel ein Monolog zurückgeben oder den Einsatzbereich explizit angeben und testen.

## Nicht nur Mocks, sondern die tatsächliche Channel-Auflösung prüfen

Mit `Log::shouldReceive('channel')` allein lassen sich die Registrierung des Providers, die Auswahl der Factory und die Übernahme der Konfiguration nicht prüfen. Das folgende Beispiel nutzt die [Testumgebung für Pakete](https://kawax.biz/de/advanced/package-testing.md) und durchläuft den tatsächlichen Manager und die Factory.

Als Ausgabeziel für Tests wird `php://memory` angegeben. Hier werden der Aufbau des Channels und die Einstellungen des Handlers geprüft, ohne tatsächlich Logs zu schreiben.

```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()` fängt Exceptions ab, die während der Erzeugung eines Channels auftreten, und kann stattdessen den Emergency-Logger zurückgeben. Die Feststellungen „Es trat keine Exception auf“ oder „Es war ein LoggerInterface“ allein beweisen nicht, dass der gewünschte Channel aufgebaut wurde. Der obige Test prüft daher auch den Namen, den Handler und das Log-Level des Monolog-Loggers, damit ein Fallback nicht fälschlich als Erfolg gewertet wird. Schreibfehler nach der Erzeugung werden nicht zwangsläufig über denselben Mechanismus abgefangen.

## Zusätzliche Prüfungen für die langfristige Wartung

| Änderung oder Nutzungsweise | Zu prüfen |
| - | - |
| Hinzufügen von `tap` | Die erwartete Anpassung wird auf den Logger angewendet, den die tatsächliche Factory zurückgibt |
| Einbindung in einen Stack | Handler und Processors der untergeordneten Channels werden eingebunden, und auch die Zusammensetzung nach erneuter Auflösung ist korrekt |
| Änderung von Ausgabeziel oder Format | Tatsächlich geschriebene Inhalte, Berechtigungen, Rotation usw. in einer separaten Prüfumgebung kontrollieren |
| Änderung des Registrierungszeitpunkts | Der Treiber wird auch dann registriert, wenn `log` bereits aufgelöst ist, und der Channel kann erzeugt werden |
| Laravel-Update | Argumente und Scope der Callbacks, das Entfernen zwischengespeicherter Einträge und den Fallback auf den Emergency-Logger fortlaufend prüfen |

## Verwendete Primärquellen

* [Offizielle Laravel-Dokumentation: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: Erzeugung, Erweiterung und Freigabe von Channels im LogManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: Umhüllung und Methodenweiterleitung im Logger](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: Singleton-Registrierung im LogServiceProvider](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)
* [Offizielle Laravel-Tests: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [Weiterführende Themen](https://kawax.biz/de/advanced/index.md)
- [Storage-Treiber in Paketen registrieren und neu auflösen](https://kawax.biz/de/advanced/package-filesystem-drivers.md)
- [Logging](https://kawax.biz/de/logging.md)
- [Eigene Benachrichtigungskanäle in Paketen und Prüfung der Zustellung](https://kawax.biz/de/advanced/package-notification-channels.md)
- [Artisan-Befehle in Paketen registrieren und Abhängigkeiten auflösen](https://kawax.biz/de/advanced/package-commands.md)


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