Uso, manejo y visor personalizado de logs en Laravel Livewire

- Andrés Cruz - EN In english

Video thumbnail

Los archivos de registro o logs constituyen el mecanismo principal en cualquier aplicación web para auditar el comportamiento del sistema, permitiendo rastrear eventos informáticos, advertencias y errores de ejecución. En Laravel, los registros se centralizan por defecto en el directorio storage/logs/laravel.log.

El framework ofrece un nivel alto de personalización a través de su archivo de configuración config/logging.php, permitiendo definir el tipo de almacenamiento (archivos diarios, canales independientes o integración con servicios externos) y ajustar los niveles de severidad según el estándar PSR-3 (desde mensajes informativos hasta fallos críticos del sistema).

Los archivos de log en Laravel se encuentran en storage/logs. A veces, VS Code no los muestra en la búsqueda rápida (Ctrl + P), pero eso no significa que no estén ahí. Si tu aplicación lanza un Error 500 en producción, el usuario solo verá una pantalla genérica, pero el "porqué" del fallo estará escrito en estos archivos.

¿Por qué son vitales?

No puedes confiar en que un usuario te avise de un error, y mucho menos que te dé detalles técnicos. Revisar los logs periódicamente (vía FTP o SSH) te permite:

  • Identificar URLs rotas que afectan al SEO.
  • Detectar fallos en actualizaciones de base de datos.
  • Corregir errores de lógica que solo ocurren con ciertos datos de clientes reales.

El log desempeñan un papel primordial para guardar registros de posibles problemas que están ocurriendo en la aplicación para su posterior identificación y resolución de errores, usualmente en local, usamos la herramienta de debug, junto con la función dd() para poder ver claramente los errores por pantalla, pero, para producción se emplea algo más elegante como es el uso de los logs que es el tema que cubriremos en este apartado.

Los registros de los logs se pueden generar a partir de errores que ocurren en la aplicación o por métodos empleados por nosotros mismos como veremos en este apartado, además, podemos personalizar los procesos de log ya sea para registrar errores en el framework.

En Laravel, los logs se configuran mediante canales como archivos o base de datos.

Lo primero que vamos a hacer es ocasionar algún error en el proyecto, por ejemplo:

app\Http\Controllers\Dashboard\PostController.php

$posts = Post::paginat(10);

Automáticamente si vamos a la ruta anterior, veremos el error por pantalla:

http://larafirststeps.test/dashboard/post

Call to undefined method App\Models\Post::paginat()

En producción, no deberíamos dejar habilitado este tipo de mensajes para evitar exponer partes críticas del sistema a usuarios no autorizados, lo que se recomienda es almacenar estos errores en un log del sistema; para habilitarlo, primero desactivamos el modo debug del proyecto:

.env

APP_DEBUG=false

Y ahora veríamos un error 500 en la ruta de:

http://larafirststeps.test/dashboard/post

Configurar canales para el log

Ahora si vamos a ver el archivo de configuración para el log:

config\logging.php

'default' => env('LOG_CHANNEL', 'stack'),

Por defecto emplea la configuración de stack.

Si analizamos el archivo de configuración de los logs, vemos los posibles canales que podemos usar:

config\logging.php

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => explode(',',env('LOG_STACK', 'single')),
        'ignore_exceptions' => false,
    ],
    'single' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'debug'),
        'replace_placeholders' => true,
    ],
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'debug'),
        'days' => env('LOG_DAILY_DAYS', 14),
        'replace_placeholders' => true,
    ],
];

Canales por defecto

Como puedes apreciar en el archivo anterior, existen varios canales para el log que puedes usar para diferentes propósitos:

  • Canal single, registra todos los mensajes en un único archivo de log especificado en la configuración. Es útil para el desarrollo local o cuando necesita un archivo de registro simple sin rotación de registros.
  • Canal daily, registra mensajes en un nuevo archivo de registro cada día, con esto se evita que el archivo de log crezca demasiado como en el caso anterior, por lo tanto, este es el preferido para cuando tenemos la aplicación en producción.
  • Canal slack, es un controlador Monolog basado en SlackWebhookHandler.
  • Canal syslog, envía mensajes de registro al servicio syslog del sistema.
  • Canal errorLog, registra mensajes en el registro de errores de PHP, que es específico del sistema.
  • Custom channel, logs personalizados que permitirá registrar mensajes en cualquier ubicación.

Cada uno de los canales, tienen diversas formas de configuración como el path, para almacenar el archivo:

'path' => storage_path('logs/laravel.log'),

O el nivel que va a escuchar:

'level' => env('LOG_LEVEL', 'debug'),

Del cual hablaremos un poco más adelante.

El driver en donde el más sencillo es el single, para indicar un archivo:

'single' => [
    'driver' => 'single',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'replace_placeholders' => true,
],

Aunque también podemos crear nuestros propios logs de la siguiente manera:

config\logging.php

'custom' => [
            'driver' => 'single',
            'path' => storage_path('logs/custom.log'),
            'level' => 'debug'
        ]

El tipo de log que nos serviría en la mayoría de los casos, sería el de single (o daily en producción), y cada cierto tiempo vamos revisando el log para corregir posibles problemas en el sistema.

Para ver el error anterior por el log, configura el driver con:

config\logging.php

'default' => env('LOG_CHANNEL', 'single'),

Al recargar la página con el error, deberías de ver un archivo log generado en:

config\logging.php

Niveles de Log

El nivel de log ofrece un esquema de todos los niveles de log definidos en la especificación RFC 5424. En orden descendente de gravedad, estos niveles de registro son: emergency, alert, critical, error, warning, notice, info, y debug.

  • "emergency": El sistema no se puede utilizar.
  • "alert": Se deben tomar medidas de inmediato.
  • "critical": Condiciones críticas.
  • "error": Errores de tiempo de ejecución que no requieren acción inmediata.
  • "warning": Sucesos excepcionales que no son errores.
  • "notice": Eventos normales pero significativos.
  • "info": Eventos o información interesante.
  • "debug": Información de depuración.

Es decir, un nivel de tipo error tiene más peso o importancia que el de warning según la escala presentada antes, y esto es importante ya que con la opción de level de los logs, podemos especificar hasta qué nivel queremos registrar.

Desde la aplicación, podemos registrar nuestros propios logs de la siguiente manera en la cual tenemos un método por cada nivel del log:

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

Formateador para el log

Podemos crear un formato para los logs como el siguiente:

app/Logging/CustomFormatter.php

<?php
namespace App\Logging;
use Monolog\Formatter\LineFormatter;
class CustomFormatter{
    public function __invoke($logger)
    {
        foreach ($logger->getHandlers() as $handle) {
            $handle->setFormatter(new LineFormatter("(%datetime%) - %message%\n"),null,true,true);
        }
    }
}

Los parámetros del método setFormatter() corresponde:

  • $format: Define el formato de la línea de registro. Puedes utilizar marcadores de posición como %datetime%, %channel%, %level_name%, %message%, %context%, %extra%, etc.
  • $dateFormat: Define el formato de la fecha y hora en el registro.
  • $allowInlineLineBreaks: Si se permite o no saltos de línea en línea.
  • $ignoreEmptyContextAndExtra: Si se deben ignorar los contextos y extras vacíos (valor booleano).
  • $includeStacktraces: Si se deben incluir trazas de pila en el registro (valor booleano).

Es posible personalizar aspectos como los mostrados en este ejemplo:

<?php
 
namespace App\Logging;
 
use Illuminate\Log\Logger;
use Monolog\Formatter\LineFormatter;
 
class CustomizeFormatter
{
    public function __invoke(Logger $logger): void
    {
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(new LineFormatter(
                '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'
            ));
        }
    }
}

Puedes obtener más información en:

https://laravel.com/docs/master/logging

Y registramos el formateador en el canal que estemos empleando, en nuestro ejemplo, el de single:

config\logging.php
'single' => [
    ***
    'tap' => [App\Logging\CustomLogging::class],
],

Desde ahora, cada vez que se genere un registro en el log, se hará con el formato anterior.

Implementación de un Visor de Logs Personalizado con Livewire

Aunque la generación de logs es automática, su consulta directa en servidores de producción puede ser incómoda si depende exclusivamente de conexiones SSH o herramientas SFTP (como FileZilla) para descargar y leer los archivos manualmente.

Si bien existen diversos paquetes y visores de pago en el ecosistema, desarrollar un visor interno simplificado dentro del propio panel de administración resulta una alternativa ligera y eficiente, evitando la adición de dependencias externas no esenciales en el archivo composer.json.

A través de un componente de Livewire es posible estructurar un visor interactivo que aplique filtros por nivel de error, permita búsquedas por palabras clave en los mensajes y proporcione acciones para limpiar el archivo de registros.

El núcleo de esta solución se basa en el procesamiento del archivo laravel.log mediante expresiones regulares para parsear la información plana y convertirla en una estructura de datos ordenada:

<?php

use Livewire\Attributes\Title;
use Livewire\Component;
use Illuminate\Support\Facades\File;

new #[Title('Visor de Logs')]
    class extends Component {
    public string $search = '';
    public string $level = 'ALL';
    public int $maxLines = 200;

    /**
     * Obtiene y parsea las entradas del log.
     */
    public function getLogsProperty(): array
    {
        $logPath = storage_path('logs/laravel.log');

        if (!File::exists($logPath)) {
            return [];
        }

        $content = File::get($logPath);
        if (empty(trim($content))) {
            return [];
        }

        $pattern = '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}.*?)\]\s+([a-zA-Z0-9_\-\.]+)\.([A-Z]+):\s+(.*?)(?=\n\[\d{4}-\d{2}-\d{2}|\z)/ms';
        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
        // dd($matches);
        $parsedLogs = [];

        foreach (array_reverse($matches) as $match) {
            $date = $match[1] ?? '';
            $env = $match[2] ?? '';
            $logLevel = strtoupper($match[3] ?? 'INFO');
            $message = trim($match[4] ?? '');

            if ($this->level !== 'ALL' && $logLevel !== $this->level) {
                continue;
            }

            if (!empty($this->search)) {
                $searchLower = strtolower($this->search);
                if (!str_contains(strtolower($message), $searchLower) && !str_contains(strtolower($date), $searchLower)) {
                    continue;
                }
            }

            $parsedLogs[] = [
                'date' => $date,
                'env' => $env,
                'level' => $logLevel,
                'message' => $message,
            ];

            // dd($parsedLogs);

            if (count($parsedLogs) >= $this->maxLines) {
                break;
            }
        }

        return $parsedLogs;
    }

    public function deleteLogFile(): void
    {
        $logPath = storage_path('logs/laravel.log');

        if (File::exists($logPath)) {
            File::delete($logPath);
            Flux::toast('El archivo laravel.log fue eliminado del disco.');
        }
    }

    /**
     * Vacía el archivo laravel.log
     */
    public function clearLog(): void
    {
        $logPath = storage_path('logs/laravel.log');

        if (File::exists($logPath)) {
            File::put($logPath, '');
            Flux::toast('El archivo laravel.log ha sido vaciado.');
        }
    }

    public function with(): array
    {
        return [
            'logs' => $this->logs,
        ];
    }

    public function rendering($view, $data): void
    {
        $view->title('Visor de Logs');
    }
}
?>

Estructura del Componente en Blade

En la vista asociada se renderizan los datos formateados utilizando directivas de Blade, aplicando estilos según la severidad del registro (por ejemplo, alertas rojas para errores críticos y amarillas para advertencias):


<div class="space-y-6">
    <div class="flex justify-between items-center">
        <div>
            <flux:heading level="2">Visor de Logs</flux:heading>
            <flux:subheading>Inspecciona y monitorea storage/logs/laravel.log en tiempo real</flux:subheading>
        </div>

        <div class="flex items-center gap-2">
            <flux:button wire:click="$refresh" icon="arrow-path" size="sm">
                Actualizar
            </flux:button>
            <flux:button wire:click="clearLog" wire:confirm="¿Estás seguro de vaciar el archivo de logs?" icon="trash"
                variant="danger" size="sm">
                Vaciar Log
            </flux:button>
        </div>
    </div>

    <flux:card class="space-y-4">
        <div class="flex flex-row gap-4">
            <div>
                <flux:field>
                    <flux:label>Buscar en el log</flux:label>
                    <flux:input wire:model.live.debounce.300ms="search"
                        placeholder="Buscar errores, consultas SQL o 'ld.'..." icon="magnifying-glass" clearable />
                </flux:field>
            </div>

            <div>
                <flux:field>
                    <flux:label>Nivel de log</flux:label>
                    <flux:select wire:model.live="level">
                        <flux:select.option value="ALL">Todos los Niveles</flux:select.option>
                        <flux:select.option value="ERROR">ERROR</flux:select.option>
                        <flux:select.option value="WARNING">WARNING</flux:select.option>
                        <flux:select.option value="INFO">INFO</flux:select.option>
                        <flux:select.option value="DEBUG">DEBUG</flux:select.option>
                        <flux:select.option value="CRITICAL">CRITICAL</flux:select.option>
                    </flux:select>
                </flux:field>
            </div>
            <div>
                <flux:field>
                    <flux:label>Vaciar Log</flux:label>
                    <flux:button wire:click="clearLog"
                        wire:confirm="¿Estás seguro de que deseas vaciar el archivo de logs?" icon="trash"
                        variant="danger" size="sm">
                        Vaciar Log
                    </flux:button>
                </flux:field>
            </div>
        </div>
    </flux:card>

    <div
        class="overflow-hidden bg-zinc-950 text-zinc-100 rounded-xl border border-zinc-800 shadow-2xl font-mono text-xs">
        <div class="flex items-center justify-between px-4 py-2 border-b border-zinc-800 bg-zinc-900/80 text-zinc-400">
            <span class="flex items-center gap-2">
                <span class="inline-block w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse"></span>
                storage/logs/laravel.log
            </span>
            <span>Mostrando las últimas {{ count($logs) }} entradas</span>
        </div>

        <div class="p-4 overflow-y-auto max-h-[650px] space-y-3 divide-y divide-zinc-900">
            @forelse($logs as $log)
                <div class="pt-3 first:pt-0 space-y-1">
                    <div class="flex flex-wrap items-center gap-2">
                        <span class="text-zinc-500">[{{ $log['date'] }}]</span>
                        <span
                            class="px-1.5 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider
                                                                                                    @if($log['level'] === 'ERROR' || $log['level'] === 'CRITICAL') bg-red-500/20 text-red-400 border border-red-500/30
                                                                                                    @elseif($log['level'] === 'WARNING') bg-amber-500/20 text-amber-400 border border-amber-500/30
                                                                                                    @elseif($log['level'] === 'INFO') bg-blue-500/20 text-blue-400 border border-blue-500/30
                                                                                                    @else bg-zinc-800 text-zinc-400 @endif">
                            {{ $log['level'] }}
                        </span>
                        <span class="text-zinc-400 text-[11px]">env: {{ $log['env'] }}</span>
                    </div>

                    <pre
                        class="p-2 rounded bg-zinc-900/50 text-zinc-300 overflow-x-auto whitespace-pre-wrap break-words leading-relaxed">{{ $log['message'] }}</pre>
                </div>
            @empty
                <div class="py-12 text-center text-zinc-500">
                    No se encontraron registros en el log.
                </div>
            @endforelse
        </div>
    </div>
</div>

Importancia del Monitoreo Continuo

La revisión constante de los archivos de registro es una práctica fundamental en la fase de mantenimiento de cualquier proyecto. Se recomienda auditar los logs con especial atención tras la liberación de nuevos despliegues o actualizaciones en producción.

Disponer de un visor integrado en el panel de administración optimiza este flujo de trabajo, permitiendo detectar de forma temprana excepciones no controladas, cuellos de botella e inconsistencias en la lógica de negocio directamente desde la propia aplicación.

Guía completa sobre el uso de los logs, personalizarlos, imprimir los propios y también veremos como crear un visor de los logs en Laravel Livewire y Laravel.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.