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

# Extensión y nueva resolución de canales de log en paquetes

> Distingue entre custom/via y LogManager::extend() en Laravel 13, y prueba el logging de tus paquetes teniendo en cuenta la retención de canales, forgetChannel() y el fallback.

Cuando un paquete quiere ofrecer su propia salida de logs, conviene separar **el mecanismo que crea el logger** de **la configuración de canales que elige la aplicación que usa el paquete**. Registrar un driver no vuelve a crear los canales existentes, y puede que se siga usando el Handler antiguo incluso después de cambiar la configuración.

Esta página se basa en la revisión de la implementación de Laravel Framework `v13.39.0`. Para el uso básico, consulta [Logging](https://kawax.biz/es/logging.md), y para preparar el provider, [Desarrollo de paquetes](https://kawax.biz/es/advanced/package-development.md).

## Elige el punto de extensión que necesitas

Si puedes conseguirlo con un driver existente, no hace falta añadir un driver propio.

| Lo que quieres hacer | Opción | Dónde se extiende |
| - | - | - |
| Ajustar el formatter u otros aspectos de un canal existente | `tap` | El `Illuminate\Log\Logger` generado |
| Elegir un Handler de Monolog desde la configuración | `driver: monolog` | Opciones como `handler` y `handler_with` |
| Construir el logger con una factory del paquete | `driver: custom` y `via` | Una clase con `__invoke(array $config)` |
| Publicar un nombre de driver propio del paquete | `LogManager::extend()` | Un closure que recibe la aplicación y el array de configuración |

Las clases de `via` y de `tap` se resuelven desde el contenedor, así que también puedes usar inyección por constructor. `tap` es un ajuste que se realiza al generar el canal, no un hook que se ejecute cada vez que se escribe una entrada de log.

> [!NOTE]
>
> En `logging.channels.audit`, `audit` es el **nombre del canal**. El `driver` que contiene es el **nombre del método de creación**. Lo que se elige con `Log::channel('audit')` es el nombre del canal, por lo que aunque cambies el driver más adelante, el código que lo llama puede mantener el mismo nombre.

## Publica primero una factory con custom y via

En el paquete ficticio `Acme\Audit`, ofreceremos una factory para la salida a archivo. Colócala en `src/Logging/CreateAuditLogger.php`, que se carga mediante 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')],
        );
    }
}
```

En la aplicación que usa el paquete, añade el siguiente elemento al array `channels` existente de `config/logging.php`. Conserva la configuración existente de `default` y `stack`.

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

`LogManager::createCustomDriver()` obtiene la clase de `via` desde el contenedor y la llama con el array de configuración del canal como único argumento. Si solo usas este método, no necesitas un provider para registrar el driver.

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

Log::channel('audit')->info('Se ha confirmado el pedido.', ['order_id' => 42]);
```

Esta factory pasa `level` explícitamente al StreamHandler. **LogManager no aplica de forma general el `level` de la configuración al logger que devuelve una factory `custom`**. Si añades opciones de configuración propias, también debes aplicarlas en la factory.

> [!TIP]
>
> Si solo quieres cambiar el archivo de destino, basta con el driver `single`. Este ejemplo es la configuración mínima para incorporar un Handler o una inicialización propios del paquete. Usa en `via` un nombre de clase que pueda almacenarse en la caché de configuración, y no guardes closures ni instancias de logger en el array de configuración.

## Usa extend cuando ofrezcas un nombre de driver propio

Si quieres publicar para la aplicación una configuración como `driver: acme-audit`, registra un driver que use la misma factory. Extiende el servicio `log` de Laravel en el `boot()` de `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);
            });
        });
    }
}
```

El provider se carga, por ejemplo, mediante el [descubrimiento automático de paquetes](https://kawax.biz/es/advanced/package-discovery.md). En la aplicación, sustituye la configuración anterior de `channels.audit` por la siguiente. `via` ya no es necesario.

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

* El `LogServiceProvider` de Laravel registra `log` como singleton. `callAfterResolving()` espera si todavía no se ha resuelto y, si ya está resuelto, aplica el registro también a esa instancia. Para más detalles, consulta [Hooks posteriores a la resolución de servicios](https://kawax.biz/es/advanced/package-service-resolution.md).
* `extend()` solo registra el closure de creación. Al registrar este callback no se genera ningún canal ni se escribe ningún log.
* `callCustomCreator()` pasa al closure `$app` y el array de configuración del canal. Son argumentos distintos de los que recibe la factory de `via`.
* Registrar de nuevo el mismo nombre de driver sobrescribe el closure de creación. No uses nombres integrados como `single` ni nombres que puedan entrar en conflicto con otros paquetes.

> [!WARNING]
>
> En la implementación revisada, el closure anónimo pasado a `extend()` se vuelve a vincular al ámbito de LogManager. En un closure no estático, `$this` no es el service provider. Como en el ejemplo, usa un closure estático con el argumento `$app` y no dependas del `$this` del provider.

El paquete proporciona el driver, y es la aplicación la que elige en qué canal o stack usarlo. Diseña el provider de forma que no reescriba `logging.default` ni todo el `logging.channels` existente.

## Los canales se retienen por nombre

`LogManager::get()` guarda los canales resueltos en `$channels`. Al obtener un canal con el mismo nombre, devuelve el objeto retenido sin volver a leer el array de configuración.

```mermaid theme={null}
flowchart TD
    A["Log::channel('audit')"] --> B{"¿audit ya está resuelto?"}
    B -->|Sí| C["Devuelve el Logger retenido"]
    B -->|No| D["Selecciona el driver de channels.audit"]
    D --> E["Factory de via o extend"]
    E --> F["Envuelve en Illuminate Logger<br>Aplica tap y el contexto compartido"]
    F --> G["Lo retiene con el nombre audit"]
```

Esto es **la retención de objetos dentro del LogManager actual**, distinta de la caché de configuración que se guarda en un archivo. Cambiar `config()` o volver a registrar `extend()` no sustituye por sí solo los Handlers ya creados.

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

En este ejemplo, `$same` es el mismo objeto que `$before`. Cuando `forgetChannel('audit')` libera la retención en el Manager, la siguiente obtención genera `$after` a partir de la nueva configuración.

### forgetChannel no actualiza las referencias existentes

Lo que hace `forgetChannel()` es eliminar la entrada dentro del Manager.

* Los servicios que conservan `$before` pueden seguir usando el logger antiguo. No es una operación que sustituya las referencias existentes por el nuevo objeto.
* Un stack ya generado, por ejemplo con `Log::channel('stack')`, tiene su propio Monolog independiente que reúne los Handlers y Processors de los canales hijos. Aplicar `forgetChannel()` solo al hijo no cambia la composición del stack existente. Si es necesario, libera también la retención del nombre de canal de ese stack y vuelve a obtenerlo.
* `forgetChannel()` por sí mismo no llama a `close()` de los Handlers. No es una API que garantice el cierre de conexiones ni el vaciado de buffers.

En los despliegues habituales, incluye en la operación la [actualización de la caché de configuración](https://kawax.biz/es/advanced/package-config-merging.md) y el reinicio de los workers de larga duración que utilices. No dependas de un mecanismo que reescriba la configuración y vuelva a resolver los canales en cada solicitud, y ten en cuenta también el [ciclo de vida de los servicios](https://kawax.biz/es/advanced/package-service-lifetimes.md).

### Para incorporarlo a un stack también se necesitan operaciones compatibles con Monolog

Un driver propio puede devolver una implementación de `Psr\Log\LoggerInterface`, pero `createStackDriver()` de Laravel llama a `getHandlers()` y `getProcessors()` en los canales hijos. Estos métodos no forman parte de la interfaz PSR.

Devolver cualquier logger PSR no garantiza que funcione dentro de un stack, así que en los paquetes que también admitan su incorporación a un stack, devuelve un Monolog como en el ejemplo, o indica claramente el alcance de uso y pruébalo.

## Verifica la resolución real de canales, no solo con mocks

Con `Log::shouldReceive('channel')` por sí solo no puedes comprobar el registro del provider, la selección de la factory ni la aplicación de la configuración. A continuación se muestra un ejemplo que pasa por el Manager y la factory reales en el [entorno de pruebas de paquetes](https://kawax.biz/es/advanced/package-testing.md).

Como destino de salida para las pruebas se indica `php://memory`. Aquí se verifican la construcción del canal y la configuración del Handler, sin escribir realmente ningún log.

```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()` puede capturar las excepciones producidas durante la generación del canal y devolver el emergency logger. Comprobar solo que «no se lanzó ninguna excepción» o que «era un LoggerInterface» no demuestra que se haya construido el canal deseado. La prueba anterior verifica también el nombre del Monolog, el Handler y el nivel de log para no confundir el fallback con un éxito. Los errores de escritura posteriores a la generación no siempre se capturan con el mismo mecanismo.

## Verificaciones adicionales para el mantenimiento a largo plazo

| Cambio o forma de uso | Qué comprobar |
| - | - |
| Añadir `tap` | El ajuste esperado se aplica al logger que devuelve la factory real |
| Incorporación a un stack | Los Handlers y Processors de los canales hijos se incorporan, y la composición también es correcta al volver a resolver |
| Cambio del destino de salida o del formato | Comprueba en un entorno de verificación aparte el contenido escrito real, los permisos, la rotación, etc. |
| Cambio del momento de registro | El driver se registra y puede generar canales aunque `log` ya esté resuelto |
| Actualización de Laravel | Sigue comprobando los argumentos y el ámbito de los callbacks, la liberación de la retención y el fallback al emergency logger |

## Fuentes primarias consultadas

* [Documentación oficial de Laravel: Logging](https://github.com/laravel/docs/blob/13.x/logging.md)
* [Framework v13.39.0: generación, extensión y liberación de la retención en LogManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/LogManager.php)
* [Framework v13.39.0: envoltura y reenvío de métodos en Logger](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Log/Logger.php)
* [Framework v13.39.0: registro singleton en 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)
* [Tests oficiales de Laravel: LogManagerTest](https://github.com/laravel/framework/blob/v13.39.0/tests/Integration/Log/LogManagerTest.php)


## Related topics

- [Temas avanzados](https://kawax.biz/es/advanced/index.md)
- [Registro y nueva resolución de drivers de Storage en paquetes](https://kawax.biz/es/advanced/package-filesystem-drivers.md)
- [Canales de notificación personalizados en paquetes y verificación de la entrega](https://kawax.biz/es/advanced/package-notification-channels.md)
- [Hooks de resolución de servicios en paquetes](https://kawax.biz/es/advanced/package-service-resolution.md)
- [Logging](https://kawax.biz/es/logging.md)


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