Gestione strategica dei log Laravel su Hetzner e OVH: come ottantasette gigabyte di laravel.log hanno fermato un magazzino e cosa configurare al suo posto

Gestione strategica dei log Laravel su Hetzner e OVH: come ottantasette gigabyte di laravel.log hanno fermato un magazzino e cosa configurare al suo posto

A novembre 2024 ho ricevuto una telefonata di emergenza di un cliente cuneese del settore logistico che gestisce un WMS (Warehouse Management System) custom su Laravel 9 ospitato su un dedicato OVH SYS-3, con tre operatori del magazzino che dipendono dal sistema per scansionare i pacchi in entrata e in uscita. Verso le 11 del mattino di un mercoledì, gli operatori avevano iniziato a vedere errori 500 in cascata: ogni tentativo di registrare un nuovo movimento di magazzino falliva. Il responsabile mi ha chiamato in panico - il magazzino era fermo, i camion in attesa, i clienti finali in coda telefonica. Quando mi sono collegato in SSH al server, in trenta secondi avevo la diagnosi: df -h mostrava la partizione /var al 100%, con 87 gigabyte di file storage/logs/laravel.log accumulati in otto mesi di scrittura continua, senza alcuna rotazione. Laravel non riusciva più a scrivere nelle sessioni, MySQL aveva smesso di poter scrivere nei propri binlog, e l'applicazione era in stato di stallo completo.

Il fix immediato è stato banale e brutale: truncate -s 0 storage/logs/laravel.log, restart di PHP-FPM, sistema di nuovo in piedi in dodici minuti. Ma il fix immediato non è quello che ho fatturato - quello che ho fatturato sono i quattro giorni successivi spesi a costruire una vera strategia di gestione dei log per quel cliente, perché il fatto che 87 GB si fossero accumulati in otto mesi era il sintomo di un problema strutturale, non un incidente isolato. L'altra cosa rivelatrice di quei 87 GB era il loro contenuto: il 94% erano log di livello DEBUG generati da un servizio di import che nessuno usava più da un anno, e che continuavano ad essere scritti perché il livello di log non era mai stato alzato a INFO in produzione. Tutta quella scrittura aveva nascosto, dentro al rumore, decine di errori reali che il sistema produceva ogni giorno e che nessuno aveva mai notato. Questo articolo è il distillato del piano operativo che ho costruito su quel cliente e che ora applico come standard su tutti i Laravel in produzione: quattro componenti - rotazione di sistema, struttura JSON Monolog, canali separati per intento, centralizzazione fuori host - che insieme trasformano i log da debito tecnico invisibile a sistema nervoso dell'applicazione.

Quando il log diventa l'incidente: ottantasette gigabyte che fermano un magazzino

Il primo punto culturale che cerco di trasmettere ai clienti dopo un incidente come quello cuneese è che il log non è uno scarto - è il primo sistema dell'applicazione che diventa critico quando qualcosa va storto. Un'applicazione che gira bene può sopravvivere senza un sistema di log strutturato (anche se non dovrebbe). Un'applicazione in crisi senza un sistema di log strutturato è impossibile da diagnosticare, e le decisioni di emergency triage vengono prese alla cieca, basate su intuizioni o, peggio, su tail -f che non vedono nulla di utile perché il segnale è sepolto nel rumore.

I tre rischi concreti della gestione passiva dei log che vedo sui clienti sono questi. Primo, l'esaurimento del disco - quello del cliente cuneese è il caso scolastico, ma succede in molte forme: log che non ruotano, sessioni che non si puliscono, cache che crescono senza limite, MySQL binlog non scaduti, dump cron job dimenticati. Quando /var raggiunge il 100%, in cascata smettono di funzionare il database (perché non riesce più a scrivere binlog), il web server (perché non riesce più a scrivere session), il sistema operativo stesso (perché non riesce più a scrivere log di sistema), e l'utente vede una pagina bianca o un errore 500 generico che non spiega niente. Secondo, la cecità operativa: quando arriva un bug intermittente e l'unica fonte di verità è un file di testo monolitico da gigabyte, anche grep diventa inefficiente - ogni invocazione consuma decine di secondi di CPU sul server di produzione e genera I/O che a sua volta rallenta l'applicazione che stai cercando di debuggare. Terzo, la non-conformità: i log applicativi quasi sempre contengono dati personali (IP utente, email, identificativi sessione), e tenerli per sempre senza una policy di retention violenta direttamente il principio di limitazione della conservazione del GDPR articolo 5(1)(e), il cui testo ufficiale è consultabile su EUR-Lex - cosa che diventa documentabile dal Garante in caso di audit successivo a un incident, esattamente lo scenario che descrivo nel mio protocollo di incident response in 72 ore conforme NIS2.

Il principio che ripeto sempre ai clienti dopo un incidente di questo tipo è che il log management non è una "best practice opzionale" - è una linea di difesa di primo livello, e va trattato con la stessa serietà dei backup. Senza log gestiti, il tuo sistema in produzione è una scatola nera, e quando la scatola nera si rompe non c'è modo di capire perché.

Logrotate prima di tutto: la cura che evita il novanta per cento dei disastri

La singola misura più importante, e quella che applico nelle prime ore di qualsiasi intervento sui log di un cliente PMI, è l'attivazione di logrotate di sistema. È uno strumento Linux storico - esiste sostanzialmente da sempre nelle distribuzioni Debian/Ubuntu - documentato nella pagina di manuale ufficiale logrotate(8) su man7.org, che si occupa di ruotare, comprimere e cancellare i vecchi file di log secondo policy configurabili. Lo preferisco alla rotazione interna di Laravel (daily driver) per due motivi: primo, è più affidabile (è un cron di sistema che non dipende dal fatto che PHP riesca a girare correttamente - quindi funziona anche quando l'applicazione è in stato di crisi); secondo, è più potente (gestisce compressione, retention multipla, hook post-rotazione come segnali ai processi).

La configurazione che applico come standard sui clienti Laravel in produzione vive in /etc/logrotate.d/laravel-app e ha questa forma:

/var/www/app/storage/logs/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 deployer www-data
    sharedscripts
    postrotate
        /usr/sbin/service php8.3-fpm reload > /dev/null 2>&1 || true
    endscript
}

I parametri chiave sono cinque. daily esegue la rotazione una volta al giorno. rotate 14 mantiene quattordici archivi storici e cancella tutto quello che è più vecchio (due settimane di retention sono il punto giusto per la maggior parte delle PMI - abbastanza per debuggare incidenti recenti, abbastanza poco da non accumulare gigabyte di storia). compress con delaycompress comprime i log dopo il primo ciclo di rotazione (la compressione gzip riduce tipicamente i log testuali al 5-10% del volume originale). create 0640 deployer www-data crea il nuovo file con permessi sicuri e ownership corretto. postrotate rilancia php-fpm in modo che riapra i descrittori di file verso il nuovo log path. Questa singola configurazione, applicata in cinque minuti, avrebbe completamente prevenuto l'incidente del cliente cuneese: i 87 GB sarebbero stati distribuiti su 14 archivi compressi da circa 600 MB compressi ciascuno, e dopo due settimane sarebbero stati automaticamente cancellati.

C'è un dettaglio importante: dopo aver attivato logrotate su un sistema esistente con file di log enormi, non devi cancellare manualmente il file vecchio con rm. Devi usare truncate -s 0 o aspettare la prima rotazione, perché PHP-FPM ha un descrittore di file aperto verso il file vecchio, e un rm libera lo spazio su disco solo dopo che tutti i descrittori vengono chiusi (cioè dopo un restart di PHP-FPM). L'errore "ho fatto rm ma il disco è ancora pieno" è classico e l'ho visto fare a più di un sviluppatore alle prese con un incidente di disco pieno - è esattamente lo stesso pattern di problema invisibile per chi non è abituato all'analisi di sistema in emergenza che ho descritto nella mia guida all'ottimizzazione delle performance PHP su Hetzner, OVH e Digital Ocean.

JSON strutturato con Monolog: dal grep al filtro semantico

Una volta risolto il problema dello spazio disco con logrotate, il livello successivo è la strutturazione dei log. Laravel usa Monolog come backend di logging - una libreria PHP estremamente potente, il cui repository ufficiale su GitHub di Seldaek è il riferimento per tutte le sue capacità - e di default scrive i log in formato testuale leggibile dall'umano: una riga per evento con timestamp, livello e messaggio. Questo formato è utile in sviluppo ma è quasi inutile in produzione, perché ogni informazione strutturata (utente coinvolto, payload della request, identificativo della sessione, metadata dell'eccezione) finisce concatenata in un'unica stringa di testo che richiede un parser regex per essere riestratta.

Il formato che uso in produzione è JSON, che Monolog supporta nativamente tramite il JsonFormatter. Si configura in config/logging.php modificando il canale primario:

'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'info'),
    'days' => 14,
    'formatter' => Monolog\Formatter\JsonFormatter::class,
    'formatter_with' => [
        'batchMode' => Monolog\Formatter\JsonFormatter::BATCH_MODE_NEWLINES,
        'appendNewline' => true,
    ],
],

Con questa configurazione, ogni evento di log diventa una riga JSON come {"datetime":"2025-03-12T11:24:33+01:00","level":"ERROR","level_name":"ERROR","message":"...","context":{"user_id":4172,"order_id":88421,"exception":{...}},"channel":"production"}. Le tre proprietà che cambiano tutto sono: la struttura dei campi (puoi filtrare via jq o tools di log management su level=ERROR, su context.user_id=4172, eccetera, in modo banale), la possibilità di iniettare context arrays ricchi (Log::error('checkout failed', ['user_id' => $u->id, 'order_id' => $o->id, 'amount' => $o->total])) che diventano automaticamente campi indicizzabili, e la compatibilità diretta con quasi tutti i sistemi di log management moderni che si aspettano JSON line. La documentazione ufficiale di Laravel sul logging descrive in dettaglio tutte le opzioni di configurazione disponibili e i vari driver supportati out-of-the-box.

L'altra cosa che faccio sempre quando passo i clienti a JSON Monolog è alzare il livello di default da debug a info in produzione (LOG_LEVEL=info nel .env di produzione). Questo elimina automaticamente tutto il rumore di basso livello - i 94% di log DEBUG del cliente cuneese, in pratica - e fa emergere solo gli eventi che meritano attenzione operativa. Il livello DEBUG ha senso in sviluppo, in produzione è un generatore di rumore che impedisce di vedere i segnali veri.

Canali separati per intento: errori, sicurezza, audit, performance

Il terzo livello, quello che separa i setup di logging adeguati da quelli ottimi, è la separazione dei canali per intento. Invece di scrivere tutto in un unico laravel.log, configuro tipicamente quattro canali distinti su config/logging.php: un canale errors per le eccezioni e i log di livello ERROR/CRITICAL (retention 30 giorni, va monitorato attivamente), un canale security per gli eventi di sicurezza come login falliti, tentativi di accesso non autorizzato, modifiche di permessi (retention 365 giorni per audit, mai cancellato sotto i 12 mesi), un canale audit per le operazioni business-critical come modifiche di prezzi, cancellazioni di ordini, accessi a dati sensibili (retention indefinita, archiviato off-host), e un canale performance per le query lente, i timeout, le metriche di latenza (retention 7 giorni, è l'unico canale dove DEBUG ha senso temporaneamente). Ognuno di questi canali ha un suo file dedicato, una sua policy di rotazione in /etc/logrotate.d/, e idealmente un suo destination remoto.

Il vantaggio di questa separazione è triplo. Primo, ogni canale ha le sue policy di retention e compliance, in modo che i log di security/audit non vengano cancellati prematuramente solo perché stanno nello stesso file dei log applicativi rumorosi. Secondo, ogni canale può essere monitorato in modo diverso - gli errori critici devono triggerare un alert immediato, gli eventi di security devono andare a un SIEM, le performance possono essere campionate. Terzo, in caso di incidente, sai esattamente dove guardare: un breach va indagato dal canale security, un bug applicativo dal canale errors, una regressione di performance dal canale performance. È esattamente questo livello di granularità che permette di ricostruire la cronologia di un incidente in caso di compromissione, come ho descritto nel mio protocollo di gestione di un sito PHP hackerato su Hetzner o OVH - senza canali separati, le evidenze di un attacco sono sepolte in megabyte di log applicativi e quasi impossibili da estrarre nelle prime ore.

Centralizzazione con Loki o ELK: log come sistema nervoso esterno

L'ultimo livello, quello che porta una PMI da "log gestiti decentemente" a "log come asset operativo", è la centralizzazione su un sistema di log management esterno al server di produzione. Le ragioni sono tre, e nessuna è secondaria. Prima ragione, sicurezza forensica: se il server di produzione viene compromesso, la prima cosa che un attaccante competente fa è cancellare i log locali per coprire le tracce. Avere i log spediti in tempo reale a un sistema esterno significa che le evidenze di un incidente sopravvivono anche se il server target viene completamente devastato - è un requisito di base per qualunque incident response degno del nome ed è esplicitamente raccomandato dalla direttiva NIS2 nella mia guida operativa alla compliance NIS2 su server Hetzner/OVH. Seconda ragione, performance: l'analisi di log su un sistema dedicato non consuma risorse del server applicativo, e gli indici full-text del log management permettono ricerche istantanee su volumi enormi che sarebbero impossibili con grep. Terza ragione, alerting: un buon sistema di log management ti permette di definire regole tipo "se compaiono più di 10 errori 500 in un minuto, mandami una notifica Slack" - il che trasforma il monitoring da reattivo (qualcuno chiama in panico) a proattivo (l'alert arriva prima che il cliente se ne accorga).

Lo stack che configuro su quasi tutti i clienti PMI è Grafana Loki + Promtail + Grafana - economico, self-hosted, scalabile. Promtail è un agente leggero che gira sul server applicativo e fa il tail dei file JSON di log, spedendoli a Loki. Loki è il backend di storage e indicizzazione, ottimizzato per log strutturati ed economico in termini di storage (indicizza solo i metadata, non il contenuto, quindi costa molto meno di Elasticsearch su volumi elevati). Grafana è la UI che permette di fare query, dashboard, e alert. La documentazione ufficiale di Grafana Loki è eccellente e mantiene una guida specifica per setup small/medium che è perfetta per le PMI italiane con un singolo server applicativo. Per i clienti che hanno già Elasticsearch in casa per altri motivi, lo stack ELK (Elasticsearch + Logstash + Kibana) è un'alternativa più potente ma più cara in termini di RAM e storage. Sul cliente cuneese, dopo l'incidente, abbiamo configurato Loki su un VPS dedicato da 4 GB di RAM (costo: 12 euro/mese su OVH), con due settimane di retention dei log su Loki e archiviazione dei security/audit log su S3 a basso costo per la conservazione legale. Il risultato è che oggi, sei mesi dopo, il responsabile può aprire una dashboard Grafana e in dieci secondi vedere quanti errori ci sono stati nell'ultimo giorno, su quale endpoint, da quale utente - la differenza fra avere una scatola nera e avere un sistema osservabile.

Se gestisci una PMI con un'applicazione Laravel in produzione e i tuoi log sono ancora in stato "default Laravel + un po' di paura", non aspettare che 87 GB di laravel.log fermino il tuo magazzino o il tuo e-commerce. Scopri come lavoro con i clienti sul tema dell'osservabilità delle applicazioni in produzione: in dieci anni di consulenza ho introdotto setup di logging strutturato + centralizzazione su decine di clienti Laravel, e il pattern è sempre lo stesso - quattro giorni di lavoro, retention sotto controllo, alert proattivi, zero incidenti causati da log fuori controllo nei mesi successivi. Se vuoi una valutazione operativa del tuo setup di logging attuale con un piano di adozione progressiva di logrotate, JSON Monolog, canali separati e centralizzazione, contattami per una consulenza: in due giornate di lavoro tipicamente configuro logrotate sui file critici, attivo Monolog JSON con canali multipli, deploya un'istanza Loki + Grafana sul tuo VPS, e ti consegno una dashboard di monitoring base pronta da estendere con le metriche specifiche del tuo business.

Ultima modifica: