Logging
Com gestionar logs a Laravel: canals, nivells, Monolog, context, stacks i alertes.
Per què necessites logging?#
Una aplicació sense logs és com conduir de nit sense llums: tot funciona bé fins que alguna cosa falla, i llavors no tens cap informació per entendre què ha passat. Els logs són el registre cronològic de tot el que passa a la teva aplicació: cada error, cada avís, cada acció rellevant. Quan un usuari reporta un problema, els logs són la primera eina que consultes per diagnosticar la causa.
El logging serveix per a quatre propòsits fonamentals. Primer, la depuració: quan alguna cosa no funciona com s'espera, els logs et permeten seguir el flux d'execució i trobar el punt exacte on les coses van malament. Segon, la monitorització: en producció, els logs et permeten detectar problemes abans que els usuaris els notin, com un servei extern que comença a respondre lentament o un disc que s'està omplint. Tercer, l'auditoria: per a aplicacions que gestionen dades sensibles, els logs registren qui ha accedit a què i quan, cosa que pot ser un requisit legal. Quart, la resposta a incidents: quan hi ha un atac o una caiguda del servei, els logs són essencials per entendre què ha passat i com prevenir-ho en el futur.
Laravel utilitza Monolog, una de les biblioteques de logging més robustes de PHP, que implementa l'estàndard PSR-3. Monolog suporta dotzenes de handlers (destinacions on enviar els logs) i processadors (que afegeixen informació contextual). Laravel embolcalla Monolog amb una API elegant que simplifica la configuració i l'ús diari, permetent enviar logs a fitxers, serveis externs, canals de Slack i molt més, tot des d'una configuració centralitzada.
Nivells de log#
L'estàndard PSR-3 defineix vuit nivells de log, ordenats de més greu a menys greu. Cada nivell té un propòsit específic, i triar el nivell correcte és important perquè determina com es gestiona el missatge: un missatge debug s'ignora en producció, però un critical pot enviar una alerta a Slack i despertar l'equip de guàrdia a les tres de la matinada.
El nivell emergency indica que el sistema és completament inutilitzable: la base de dades és inaccessible, el disc és ple o el servidor no pot processar cap petició. El nivell alert requereix acció immediata: per exemple, l'aplicació ha detectat que un certificat SSL expirarà en 24 hores. El nivell critical indica condicions crítiques com un component essencial que ha fallat. El nivell error és per a errors d'execució que no requereixen acció immediata però que s'han d'investigar.
El nivell warning és per a situacions anormals que no són errors: per exemple, l'ús d'una API deprecated o un inventari que s'acosta al mínim. El nivell notice és per a esdeveniments normals però significatius. El nivell info registra informació general sobre el funcionament de l'aplicació, com un usuari que ha iniciat sessió o una comanda que s'ha executat correctament. Finalment, el nivell debug és per a informació detallada de depuració que només és útil durant el desenvolupament:
use Illuminate\Support\Facades\Log;
Log::emergency('El sistema és inutilitzable.');
Log::alert('Cal acció immediata: certificat SSL expira avui.');
Log::critical('Component de pagament no disponible.');
Log::error('Error al processar la comanda #1234.');
Log::warning('API deprecated: v1 es retirarà el 2025-01-01.');
Log::notice('Nou usuari registrat des de 192.168.1.1.');
Log::info('Comanda schedule:run executada correctament.');
Log::debug('Query SQL: SELECT * FROM users WHERE id = 1.');Una regla pràctica per triar el nivell adequat: si algú ha de llevar-se a les 3 de la matinada, és emergency o alert. Si cal investigar-ho l'endemà, és error o critical. Si és una nota per al futur, és warning o notice. Si és informació de rutina, és info. Si només és útil mentre depures, és debug.
Configuració#
Tota la configuració de logging es troba al fitxer config/logging.php. La variable d'entorn LOG_CHANNEL determina quin canal s'utilitza per defecte, i LOG_LEVEL estableix el nivell mínim de log que es registra. Els missatges per sota d'aquest nivell s'ignoren completament.
Per exemple, si configures LOG_LEVEL=warning, només es registraran els missatges de nivell warning, error, critical, alert i emergency. Els missatges notice, info i debug es descartaran. Això és útil en producció per reduir el volum de logs i centrar-se en els problemes reals:
LOG_CHANNEL=stack
LOG_LEVEL=debug
LOG_DEPRECATIONS_CHANNEL=null
LOG_STACK=singleLa variable LOG_DEPRECATIONS_CHANNEL permet enviar els avisos de funcions deprecated a un canal específic. Això és útil quan estàs preparant una actualització de Laravel o PHP: pots enviar els deprecations a un fitxer separat per revisar-los sense que contaminin els logs principals.
Canals de log#
Laravel organitza els logs en canals. Cada canal defineix on i com s'escriuen els missatges. Pots tenir un canal que escriu a un fitxer local, un altre que envia errors crítics a Slack, i un tercer que envia tot a un servei de monitorització extern com Papertrail o Datadog. Aquesta flexibilitat permet adaptar el logging a les necessitats de cada aplicació i cada entorn.
El fitxer config/logging.php defineix els canals disponibles dins de l'array channels. Cada canal té un driver que determina el seu comportament, i opcions addicionals específiques del driver:
// config/logging.php
'channels' => [
'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,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER',
SyslogUdpHandler::class
),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
],El canal single escriu tots els logs a un únic fitxer. Això és senzill però el fitxer creix indefinidament. El canal daily crea un fitxer per dia (laravel-2024-01-15.log) i elimina automàticament els fitxers més antics del nombre de dies configurat. El canal syslog envia els logs al sistema syslog del servidor operatiu, cosa que és útil si ja tens infraestructura de recollida de logs al servidor. El canal errorlog utilitza la funció error_log de PHP. El canal null descarta tots els missatges, cosa útil per a tests on no vols generar fitxers de log.
El canal stack#
El canal stack és un dels més potents de Laravel perquè permet combinar múltiples canals en un de sol. Quan escrius un missatge al canal stack, el missatge s'envia a tots els canals que inclou. Això permet, per exemple, escriure tots els logs a un fitxer local (per a depuració ràpida) i alhora enviar els errors crítics a Slack (per a alertes immediates).
El canal stack és el canal per defecte de Laravel. La variable LOG_STACK defineix quins canals s'inclouen. Per defecte inclou el canal single, però en producció és habitual combinar daily i slack:
// config/logging.php
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
],# Desenvolupament: només fitxer
LOG_CHANNEL=stack
LOG_STACK=single
# Producció: fitxer diari + Slack per a errors crítics
LOG_CHANNEL=stack
LOG_STACK=daily,slackL'opció ignore_exceptions controla què passa si un dels canals del stack falla. Per defecte és false, cosa que significa que si Slack no respon, l'excepció es propaga. En producció, potser vols canviar-ho a true perquè un error al canal de Slack no impedeixi que el log s'escrigui al fitxer.
Escriure logs#
La manera més habitual d'escriure logs és a través de la facade Log. Cada nivell de log té el seu propi mètode, i tots accepten un segon paràmetre opcional amb un array de context. El context és informació addicional que s'afegeix al missatge i que és fonamental per poder depurar el problema posteriorment.
Sense context, un log que diu "Error al processar la comanda" és gairebé inútil: quina comanda? De quin usuari? Quin error exactament? Amb context, el mateix log es converteix en informació accionable que permet identificar i resoldre el problema ràpidament:
use Illuminate\Support\Facades\Log;
// Missatge simple
Log::info('Usuari ha iniciat sessió.');
// Missatge amb context (molt recomanable)
Log::info('Usuari ha iniciat sessió.', [
'user_id' => $user->id,
'email' => $user->email,
'ip' => $request->ip(),
]);
// Error amb context detallat
Log::error('Error al processar el pagament.', [
'order_id' => $order->id,
'user_id' => $order->user_id,
'amount' => $order->total,
'gateway' => 'stripe',
'error' => $exception->getMessage(),
]);
// Warning amb dades de rendiment
Log::warning('Consulta SQL lenta detectada.', [
'query' => $query,
'time_ms' => $timeInMilliseconds,
'threshold_ms' => 1000,
]);Escriure a un canal específic#
Per defecte, els logs s'envien al canal configurat com a default. Però pots enviar un missatge a un canal específic utilitzant el mètode channel. Això és útil quan vols que certs tipus de logs vagin a una destinació diferent sense canviar el canal per defecte:
// Enviar directament al canal de Slack
Log::channel('slack')->critical('El servidor de base de dades no respon!');
// Enviar a un canal diari específic
Log::channel('daily')->info('Exportació completada.', [
'records' => 15000,
'duration' => '45s',
]);
// Enviar a múltiples canals alhora
Log::stack(['daily', 'slack'])->error('Servei de pagament no disponible.');Logging contextual#
Quan vols que totes les entrades de log d'una petició incloguin la mateixa informació (com el user_id o el request_id), pots utilitzar Log::withContext. Aquesta informació s'afegirà automàticament a tots els missatges de log durant la resta de la petició, sense haver de passar-la manualment cada vegada.
Això és especialment útil en middleware. Pots crear un middleware que afegeix informació de la petició al context de log, i després tots els logs generats durant aquella petició inclouran automàticament aquesta informació. Quan revises els logs posteriorment, pots filtrar per request_id per veure exactament tot el que ha passat durant una petició concreta:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class AddLogContext
{
public function handle(Request $request, Closure $next)
{
Log::withContext([
'request_id' => (string) Str::uuid(),
'user_id' => $request->user()?->id,
'ip' => $request->ip(),
'url' => $request->fullUrl(),
'method' => $request->method(),
]);
return $next($request);
}
}Un cop registrat aquest middleware, cada log que s'escrigui durant la petició inclourà automàticament el request_id, el user_id i la resta d'informació. Així pots correlacionar totes les entrades de log d'una mateixa petició.
Si vols compartir context entre tots els canals de log (incloent canals que es creen on-demand), utilitza Log::shareContext:
// El context compartit s'afegeix a TOTS els canals
Log::shareContext([
'hostname' => gethostname(),
'deploy_id' => config('app.deploy_id'),
]);Canals personalitzats amb Monolog#
Darrere de cada canal de Laravel hi ha Monolog. Si necessites un handler de Monolog que Laravel no exposa directament, pots crear un canal personalitzat amb el driver monolog. Monolog suporta dotzenes de handlers: des de fitxers rotatius fins a Elasticsearch, passant per Telegram, bases de dades, serveis de log centralitzats i molt més.
Per exemple, per enviar logs a un fitxer amb rotació basada en la mida (en lloc de per dia), pots configurar un canal amb el RotatingFileHandler de Monolog:
// config/logging.php
'channels' => [
'rotating' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\RotatingFileHandler::class,
'handler_with' => [
'filename' => storage_path('logs/app.log'),
'maxFiles' => 10,
],
'level' => 'info',
],
],Personalitzar Monolog amb tap#
Si vols modificar la configuració de Monolog d'un canal existent (per exemple, afegir un processador que inclou informació extra a cada log), pots utilitzar l'opció tap. Un tap és una classe amb un mètode __invoke que rep la instància del logger de Monolog i pot modificar-la:
// config/logging.php
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'days' => 14,
'tap' => [App\Logging\CustomizeFormatter::class],
],<?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%\n",
'Y-m-d H:i:s',
true,
true
));
}
}
}Processadors de Monolog#
Els processadors de Monolog afegeixen informació addicional a cada entrada de log automàticament. Per exemple, pots afegir informació sobre l'ús de memòria, el PID del procés o dades del request HTTP. Això és útil per a la monitorització i la depuració en producció:
<?php
namespace App\Logging;
use Illuminate\Log\Logger;
use Monolog\Processor\MemoryUsageProcessor;
use Monolog\Processor\WebProcessor;
class AddProcessors
{
public function __invoke(Logger $logger): void
{
foreach ($logger->getHandlers() as $handler) {
$handler->pushProcessor(new MemoryUsageProcessor());
$handler->pushProcessor(new WebProcessor());
}
}
}Integració amb Slack#
Enviar alertes a Slack per a errors crítics és una de les configuracions de logging més útils en producció. Quan alguna cosa falla greument, no vols haver de revisar fitxers de log manualment: vols que l'alerta arribi immediatament al canal de l'equip perquè algú pugui actuar.
Per configurar el canal de Slack, necessites un Incoming Webhook URL. Crea un webhook a la configuració de Slack de la teva organització i afegeix l'URL al fitxer .env. Configura el nivell del canal a critical o error perquè Slack no s'inundi amb missatges informatius:
LOG_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ// config/logging.php
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
],Amb aquesta configuració i el canal stack que inclou daily i slack, els missatges info i warning s'escriuran només al fitxer diari, mentre que els critical i emergency s'enviaran tant al fitxer com a Slack. Així l'equip rep alertes immediates dels problemes greus sense soroll dels missatges rutinaris.
Rotació i neteja de logs#
Si utilitzes el canal single, el fitxer de log creix indefinidament fins que omple el disc. Això és un problema greu en producció perquè pot causar que el servidor deixi de funcionar. El canal daily resol parcialment aquest problema creant un fitxer per dia i eliminant els més antics, però convé configurar correctament el nombre de dies que es mantenen.
L'opció days del canal daily defineix quants fitxers es conserven. Un valor de 14 dies és adequat per a la majoria d'aplicacions: prou temps per investigar problemes recents, però no tant com per ocupar espai innecessari:
// config/logging.php
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],Per a aplicacions amb alt volum de logs, pots complementar la rotació de Laravel amb logrotate del sistema operatiu. Això dóna més control sobre la compressió, la retenció i la gestió del disc:
# /etc/logrotate.d/laravel
/path-to-project/storage/logs/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0664 www-data www-data
}Canals on-demand#
De vegades necessites enviar logs a una destinació temporal que no val la pena configurar permanentment a config/logging.php. Per exemple, durant una migració de dades puntual o un procés de depuració temporal. El mètode Log::build permet crear canals al vol:
use Illuminate\Support\Facades\Log;
// Crear un canal temporal per a una migració
$logger = Log::build([
'driver' => 'single',
'path' => storage_path('logs/migration-2024-01-15.log'),
]);
$logger->info('Migració iniciada.', ['records' => $totalRecords]);
foreach ($records as $record) {
try {
$this->migrate($record);
$logger->info('Registre migrat.', ['id' => $record->id]);
} catch (\Exception $e) {
$logger->error('Error migrant registre.', [
'id' => $record->id,
'error' => $e->getMessage(),
]);
}
}
$logger->info('Migració completada.');Pots combinar un canal on-demand amb un canal existent dins d'un stack temporal:
$logger = Log::stack(['daily', Log::build([
'driver' => 'single',
'path' => storage_path('logs/import.log'),
])]);
$logger->info('Importació iniciada.');Logging d'excepcions#
Laravel registra automàticament totes les excepcions no capturades al log. La configuració de com es gestionen les excepcions es troba al fitxer bootstrap/app.php. Pots personalitzar quines excepcions es registren, quines s'ignoren i com es formaten.
El helper report() permet registrar una excepció al log sense interrompre l'execució. Això és útil quan captures una excepció, vols registrar-la per a posteriors investigacions, però l'aplicació pot continuar funcionant normalment:
try {
$this->processPayment($order);
} catch (PaymentException $e) {
// Registrar l'error però continuar
report($e);
// Notificar l'usuari i oferir alternatives
return back()->with('warning',
'El pagament no s\'ha pogut processar. Si us plau, torna a intentar-ho.'
);
}Pots configurar quines excepcions no s'han de registrar per evitar soroll als logs. Per exemple, les excepcions de validació i les respostes 404 són esperades i no aporten informació útil als logs:
// bootstrap/app.php
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions) {
$exceptions->dontReport([
\Illuminate\Validation\ValidationException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
]);
})Consideracions de rendiment#
El logging pot tenir un impacte significatiu en el rendiment de l'aplicació si no es gestiona correctament. Cada crida a Log::info() o similar implica formatar el missatge, serialitzar el context i escriure al disc o enviar per xarxa. En una petició que genera desenes de logs, això pot sumar mil·lisegons significatius.
La primera consideració és el nivell de log. En producció, configura LOG_LEVEL a warning o error per eliminar els missatges debug i info que no aporten valor en un entorn estable. Només baixa el nivell a debug quan estàs investigant un problema concret.
La segona consideració és el driver. Escriure a un fitxer local és ràpid (microsegons), però enviar a Slack o a un servei extern pot trigar centenars de mil·lisegons. Per això, els canals externs s'haurien de configurar amb un nivell alt (critical o error) per minimitzar el nombre de peticions de xarxa. Si necessites enviar molts logs a un servei extern, considera utilitzar un buffer que acumuli missatges i els enviï per lots:
// config/logging.php
'buffered_slack' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\BufferHandler::class,
'handler_with' => [
'handler' => new Monolog\Handler\SlackWebhookHandler(
env('LOG_SLACK_WEBHOOK_URL'),
null,
null,
true,
null,
false,
false,
Monolog\Level::Critical
),
'bufferLimit' => 10,
],
],La tercera consideració és evitar el logging dins de bucles amb moltes iteracions. Si processos 100.000 registres i escrius un log per cadascun, el fitxer de log creixerà ràpidament i el rendiment es degradarà. En aquests casos, és millor registrar només els errors i un resum al final:
$errors = [];
$processed = 0;
foreach ($records as $record) {
try {
$this->process($record);
$processed++;
} catch (\Exception $e) {
$errors[] = ['id' => $record->id, 'error' => $e->getMessage()];
}
}
Log::info('Processament completat.', [
'total' => count($records),
'processed' => $processed,
'errors' => count($errors),
]);
if (count($errors) > 0) {
Log::warning('Registres amb errors durant el processament.', [
'errors' => array_slice($errors, 0, 50),
]);
}Fixa't que l'array d'errors es limita als primers 50 elements amb array_slice. Si tens 10.000 errors, serialitzar-los tots al log no seria pràctic ni útil. Registra un nombre representatiu i guarda el detall complet en un altre lloc si cal.