Health check applicativi Laravel 12: da controller custom a Health Routing con DiagnosingHealth e spatie/laravel-health

Health check applicativi Laravel 12: da controller custom a Health Routing con DiagnosingHealth e spatie/laravel-health

In una piattaforma marketplace con migliaia di utenti attivi, l'health check era un controller custom con 180 righe che verificava database, Redis, coda e 3 API esterne - con un timeout complessivo di 15 secondi. Quando il gateway di pagamento ha iniziato a rispondere in 12 secondi (invece dei soliti 200ms), l'health check superava il timeout del load balancer AWS, che marcava tutte le istanze come unhealthy e le rimuoveva dal target group. Il risultato: un'API di pagamento lenta ha causato un outage completo dell'applicazione. La AWS Builders' Library documenta esattamente questo anti-pattern: un deep health check che verifica dipendenze esterne può causare cascading failure quando una singola dipendenza degrada.

Come funziona l'Health Routing di Laravel 12 e cosa sostituisce?

L'Health Routing, introdotto in Laravel 11 (marzo 2024) come parte della ristrutturazione dello skeleton applicativo, espone un endpoint /up configurabile in bootstrap/app.php. L'endpoint dispatcha l'evento Illuminate\Foundation\Events\DiagnosingHealth: se nessun listener lancia eccezioni, la risposta è HTTP 200; se un listener lancia un'eccezione, la risposta è HTTP 500. È un meccanismo pass/fail - non restituisce un JSON dettagliato con lo stato di ogni componente. Questa semplicità è intenzionale: i load balancer (AWS ALB, Nginx, HAProxy) e i probe Kubernetes necessitano di un segnale binario (healthy/unhealthy), non di un report diagnostico.

In Laravel 9/10, l'health check era tipicamente un controller custom che raccoglieva lo stato di database, cache e servizi esterni in un array JSON. Il problema: ogni applicazione reinventava il formato di risposta, la gestione degli errori e i timeout - codice boilerplate duplicato in ogni progetto. L'Health Routing standardizza il check di base; per monitoring dettagliato con dashboard, storage dei risultati e notifiche, il pacchetto spatie/laravel-health (oltre 10 milioni di installazioni) offre 16+ check integrati: DatabaseCheck, RedisCheck, CacheCheck, QueueCheck, UsedDiskSpaceCheck, ScheduleCheck, HorizonCheck, DebugModeCheck e altri:

/* bootstrap/app.php - Health Routing base (Laravel 11/12) */
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up'
    )
    ->create();

/* app/Listeners/VerifyDatabaseConnection.php
 * L'auto-discovery registra il listener dal type-hint di handle().
 * Se il DB non risponde, l'eccezione causa HTTP 500 su /up. */
namespace App\Listeners;

use Illuminate\Foundation\Events\DiagnosingHealth;
use Illuminate\Support\Facades\DB;

class VerifyDatabaseConnection
{
    public function handle(DiagnosingHealth $event): void
    {
        DB::connection()->getPdo();
    }
}

/* app/Listeners/VerifyRedisConnection.php */
namespace App\Listeners;

use Illuminate\Foundation\Events\DiagnosingHealth;
use Illuminate\Support\Facades\Redis;

class VerifyRedisConnection
{
    public function handle(DiagnosingHealth $event): void
    {
        Redis::connection()->ping();
    }
}

/* spatie/laravel-health - per monitoring dettagliato con dashboard */
/* app/Providers/AppServiceProvider.php */
use Spatie\Health\Facades\Health;
use Spatie\Health\Checks\Checks\DatabaseCheck;
use Spatie\Health\Checks\Checks\RedisCheck;
use Spatie\Health\Checks\Checks\CacheCheck;
use Spatie\Health\Checks\Checks\UsedDiskSpaceCheck;
use Spatie\Health\Checks\Checks\QueueCheck;

public function boot(): void
{
    Health::checks([
        DatabaseCheck::new(),
        RedisCheck::new(),
        CacheCheck::new(),
        UsedDiskSpaceCheck::new()
            ->warnWhenUsedSpaceIsAbovePercentage(70)
            ->failWhenUsedSpaceIsAbovePercentage(90),
        QueueCheck::new()
            ->failWhenHealthCheckJobNotRanInMinutes(10),
    ]);
}

La distinzione tra i due approcci mappa direttamente sui probe Kubernetes: l'endpoint /up di Laravel è il liveness probe (il processo è vivo?); i DiagnosingHealth listener con check su database e Redis sono il readiness probe (l'app è pronta a ricevere traffico?); spatie/laravel-health con check schedulati fornisce il monitoring continuo. La Twelve-Factor App non include esplicitamente gli health check, ma il framework Beyond the Twelve-Factor App (Kevin Hoffman, O'Reilly) aggiunge la Telemetry come fattore dedicato - con gli health check come uno dei tre pilastri.

Come scegliere tra shallow e deep health check per il tuo applicativo?

La AWS Builders' Library identifica tre strategie: liveness check (il processo risponde?), shallow check (le dipendenze locali funzionano?), deep check (le dipendenze esterne rispondono?). Ogni livello aggiunge informazione ma anche rischio. Un deep check che verifica un'API di pagamento esterna con timeout di 5 secondi può causare il problema descritto nell'intro - se il servizio esterno degrada, il load balancer rimuove istanze che sarebbero perfettamente funzionanti per tutto il resto del traffico.

La regola pratica: il check usato dal load balancer deve essere shallow (database locale, Redis locale, filesystem) con timeout aggressivo (2-3 secondi). I check su servizi esterni devono essere separati, con risultati che alimentano dashboard e alert ma non influenzano il routing del traffico. In Laravel, questo si traduce in: /up con listener DiagnosingHealth che verificano solo database e Redis (shallow, usato da ALB/Kubernetes); spatie/laravel-health schedulato ogni minuto con check più profondi (queue size, disk space, servizi esterni) che notificano via Slack/email ma non causano il deregistramento delle istanze.

Laravel Pulse completa il quadro con metriche aggregate nel tempo: slow queries, slow requests, eccezioni, utilizzo cache, throughput dei job. L'health check dice "l'app funziona adesso?", Pulse dice "come sta funzionando nel tempo?" - sono complementari, non alternativi.

Errori comuni negli health check applicativi Laravel

Il primo errore è il cascading failure da deep check. Verificare un servizio esterno nell'endpoint del load balancer significa che un timeout del servizio esterno rende l'intera applicazione "unhealthy". AWS documenta il comportamento fail-open: quando tutti i target sono unhealthy, il load balancer invia traffico a tutti (meglio un servizio degradato che nessun servizio). Ma questo comportamento è un'ultima risorsa - il check deve essere progettato per non arrivarci.

Il secondo è non separare liveness da readiness. Un listener DiagnosingHealth che lancia eccezione perché Redis è temporaneamente irraggiungibile causa HTTP 500 su /up, e il load balancer rimuove l'istanza. Ma se l'applicazione funziona senza cache (fallback a database), l'istanza era ancora utile. La soluzione: nel listener, lanciare eccezione solo per dipendenze critiche (database primario), loggare warning per dipendenze non critiche (cache, servizi esterni).

Il terzo è esporre dettagli infrastrutturali nell'endpoint pubblico. Un JSON con versione MySQL, host Redis e URL dei servizi esterni è un regalo per un attaccante in fase di ricognizione. L'endpoint /up deve restituire solo HTTP 200/500; il monitoring dettagliato via spatie/laravel-health deve essere protetto da autenticazione o accessibile solo da IP interni.

Il quarto è non testare i listener DiagnosingHealth. Un listener che verifica il database con DB::connection()->getPdo() funziona quando il database è raggiungibile - ma il test deve verificare anche il caso di fallimento. Forzare un \PDOException con Mockery e verificare che il listener propaghi l'eccezione (o la gestisca correttamente) è il test minimo.

Gli health check applicativi sono il complemento del monitoraggio infrastrutturale con Prometheus e Grafana - Prometheus monitora le metriche di sistema, l'health check verifica che l'applicazione stessa sia operativa. L'event discovery di Laravel 12 registra automaticamente i listener DiagnosingHealth dal type-hint, senza configurazione manuale nell'EventServiceProvider. Per conoscere il mio approccio al monitoraggio applicativo in Laravel, visita la mia pagina professionale. Se il tuo health check è un controller da 200 righe che verifica servizi esterni con timeout di 15 secondi, contattami per una consulenza dedicata - partiamo dalla separazione tra shallow e deep check e dall'integrazione con il load balancer.

Ultima modifica: