Osservabilità minima per applicazioni PHP legacy: logging strutturato, metriche essenziali e alert senza riscrivere il codice
A dicembre 2024, su un gestionale PHP 7.2 di un cliente piemontese - azienda di distribuzione ricambi con 15 operatori - il pattern era sempre lo stesso: il titolare riceveva una telefonata da un cliente ("il sito non funziona"), chiamava me, e io iniziavo a investigare. Il tempo medio dalla telefonata del cliente alla risoluzione era circa 4 ore - non perché il fix fosse complesso, ma perché la diagnosi era un incubo. I log erano un error_log di PHP che mescolava notice, warning e fatal error in un unico file non strutturato da 2 GB, senza timestamp leggibili, senza contesto (quale utente? quale pagina? quale query?), e senza nessun meccanismo di alert. Scoprivo i problemi quando il danno era già fatto.
In cinque giorni ho introdotto osservabilità minima - logging strutturato in JSON, metriche operative raccolte da cron, e alert via Telegram per errori critici - senza toccare una riga di codice applicativo. Il MTTR (Mean Time To Recovery) è sceso da 4 ore a 22 minuti. In questo articolo ti racconto come, perché il metodo funziona su qualsiasi applicazione PHP legacy indipendentemente dal framework (o dall'assenza di framework).
Stai cercando un Consulente Informatico esperto per introdurre osservabilità sulla tua applicazione PHP? Nel mio profilo professionale trovi l'esperienza concreta su logging, monitoring e incident response per PMI. Contattami per una consulenza diretta.
Perché i log di default di PHP sono inutili per la diagnosi?
Il logging di default di PHP - error_log con display_errors Off e log_errors On - scrive errori in un formato non strutturato, senza contesto e senza separazione per severità. Un file error_log dopo sei mesi di produzione contiene migliaia di notice irrilevanti mescolate a errori critici, e trovare quello che ti serve è come cercare un ago in un pagliaio.
Il problema fondamentale è che error_log registra cosa è successo ma non il contesto: non sai quale utente stava facendo cosa, quale URL ha generato l'errore, quale query era in esecuzione, o quanto tempo ha impiegato la richiesta. Senza contesto, la diagnosi richiede la riproduzione manuale del problema - che su un sistema legacy con dati di produzione è spesso impossibile.
Il primo intervento: logging strutturato in JSON con Monolog
Monolog è la libreria di logging standard per PHP - è il componente usato da Laravel e Symfony, ma funziona perfettamente anche su applicazioni PHP plain senza framework. L'installazione via Composer è l'unica modifica al progetto:
composer require monolog/monologIl file di bootstrap che aggiungo - un singolo require all'inizio del punto di ingresso dell'applicazione:
<?php
// bootstrap_logging.php - includere PRIMA di qualsiasi altro codice
require_once __DIR__ . '/vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Formatter\JsonFormatter;
$logger = new Logger('app');
// Handler: file JSON con rotazione giornaliera, retention 30 giorni
$handler = new RotatingFileHandler(__DIR__ . '/logs/app.log', 30, Logger::WARNING);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
// Handler separato per errori critici
$criticalHandler = new StreamHandler(__DIR__ . '/logs/critical.log', Logger::CRITICAL);
$criticalHandler->setFormatter(new JsonFormatter());
$logger->pushHandler($criticalHandler);
// Registrare come error handler globale di PHP
set_error_handler(function ($errno, $errstr, $errfile, $errline) use ($logger) {
$logger->warning($errstr, [
'file' => $errfile,
'line' => $errline,
'errno' => $errno,
'url' => $_SERVER['REQUEST_URI'] ?? 'cli',
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
return false; // continua con il default error handler
});
// Catturare eccezioni non gestite
set_exception_handler(function ($exception) use ($logger) {
$logger->critical($exception->getMessage(), [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
'url' => $_SERVER['REQUEST_URI'] ?? 'cli',
'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
});
// Rendere il logger disponibile globalmente
$GLOBALS['app_logger'] = $logger;Questo singolo file, incluso con require_once 'bootstrap_logging.php' nel punto di ingresso dell'applicazione, trasforma il logging da "error_log non strutturato" a "JSON con contesto, rotazione e separazione per severità". Il file logs/app.log contiene righe JSON parsabili con jq:
# Cercare errori critici dell'ultima ora
cat logs/critical.log | jq 'select(.datetime > "2025-12-15T10:00:00")'
# Errori raggruppati per URL
cat logs/app.log | jq -r '.context.url' | sort | uniq -c | sort -rn | head -10
# Errori di un utente specifico
cat logs/app.log | jq 'select(.context.user_ip == "93.XX.XX.XX")'La differenza rispetto al grep su error_log è abissale: il JSON strutturato con contesto permette query precise in secondi invece di cercare pattern in un file di testo da 2 GB.
Il secondo intervento: metriche operative con cron
Le metriche che raccolgo su ogni applicazione PHP legacy sono sei - le minime necessarie per capire se il sistema sta degradando prima che i clienti se ne accorgano:
#!/usr/bin/env bash
# /root/scripts/php-metrics.sh - cron ogni 5 minuti
set -uo pipefail
METRICS_FILE="/var/log/php-metrics.jsonl"
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# 1. Errori PHP negli ultimi 5 minuti
ERRORS=$(find /var/www/html/logs -name "*.log" -newer /tmp/.metrics-last-run -exec grep -c "WARNING\|CRITICAL" {} + 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')
# 2. Response time medio Nginx (dall'access log)
AVG_RT=$(awk -v t=$(date -d '5 minutes ago' +%s) '$4 > t {sum+=$NF; n++} END {if(n>0) print sum/n; else print 0}' /var/log/nginx/access.log 2>/dev/null || echo 0)
# 3. Processi PHP-FPM attivi
FPM_ACTIVE=$(curl -sf http://127.0.0.1/fpm-status 2>/dev/null | grep "active processes" | awk '{print $NF}' || echo 0)
# 4. Query MySQL lente negli ultimi 5 minuti
SLOW_Q=$(mysql -u root -p"${MYSQL_PASS}" -N -e "SHOW GLOBAL STATUS LIKE 'Slow_queries';" 2>/dev/null | awk '{print $2}' || echo 0)
# 5. Uso disco
DISK_PCT=$(df / | awk 'NR==2 {print int($5)}')
# 6. Uso RAM
RAM_PCT=$(free | awk '/Mem:/ {printf "%d", $3/$2 * 100}')
echo "{\"ts\":\"${TIMESTAMP}\",\"errors\":${ERRORS},\"avg_rt\":${AVG_RT},\"fpm_active\":${FPM_ACTIVE},\"slow_queries\":${SLOW_Q},\"disk_pct\":${DISK_PCT},\"ram_pct\":${RAM_PCT}}" >> "$METRICS_FILE"
touch /tmp/.metrics-last-runSei numeri, ogni 5 minuti, in un file JSONL. È la telemetria minima che trasforma "il sito è lento" (affermazione soggettiva non azionabile) in "il response time medio è passato da 200ms a 1.8s alle 14:30, contemporaneamente a un picco di slow query" (dato oggettivo che punta alla causa).
Il terzo intervento: alert via Telegram per errori critici
I log e le metriche sono utili per la diagnosi, ma non risolvono il problema della discovery - devi ancora andare a guardarli. L'alert automatico chiude il cerchio: quando succede qualcosa di critico, vieni avvisato in tempo reale senza dover controllare manualmente.
#!/usr/bin/env bash
# /root/scripts/php-alert.sh - cron ogni 2 minuti
BOT_TOKEN="your_telegram_bot_token"
CHAT_ID="your_chat_id"
# Controllare errori critici negli ultimi 2 minuti
CRITICAL=$(find /var/www/html/logs -name "critical.log" -newer /tmp/.alert-last-run -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}')
if [ "${CRITICAL:-0}" -gt 0 ]; then
MSG="⚠ $(hostname): ${CRITICAL} errori critici negli ultimi 2 minuti"
LAST_ERROR=$(tail -1 /var/www/html/logs/critical.log | jq -r '.message' 2>/dev/null)
MSG="${MSG}%0A${LAST_ERROR}"
curl -sf "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}&text=${MSG}&parse_mode=HTML" > /dev/null
fi
touch /tmp/.alert-last-runSul gestionale piemontese, il primo alert Telegram è arrivato tre giorni dopo l'installazione - un errore di connessione MySQL intermittente alle 6:30 del mattino che nessuno avrebbe notato per ore. L'ho diagnosticato e risolto in 22 minuti (il wait_timeout di MySQL era troppo basso e le connessioni del cron mattutino venivano rifiutate). Senza l'alert, il titolare l'avrebbe scoperto dalle telefonate degli operatori alle 8:00 - quasi due ore dopo.
Per un monitoring più avanzato - Prometheus con Grafana, metodo RED per i servizi e USE per l'infrastruttura - rimando all'articolo sul monitoraggio IT proattivo. E per la gestione strategica dei log Laravel con Monolog JSON, canali separati e centralizzazione su Loki, ho scritto un articolo dedicato.
L'osservabilità minima non richiede un APM enterprise da migliaia di euro l'anno. Richiede Monolog (gratuito), tre script bash, e un bot Telegram. Cinque giorni di lavoro che trasformano un'applicazione PHP legacy da "scopro i problemi quando il cliente chiama" a "scopro i problemi prima del cliente e li risolvo prima che impattino il business". Se la tua applicazione PHP è una scatola nera senza logging strutturato né alert, il MTTR di ogni incidente è alto perché la diagnosi parte da zero ogni volta. Contattami e introduciamo l'osservabilità che ti manca.