PHP: Logging strutturato con JSON e identificativo della richiesta

by theArchitect
SHARE
PHP: Logging strutturato con JSON e identificativo della richiesta
© Guida-HTML5.it

Introduzione

Quando un’applicazione PHP cresce, i semplici messaggi testuali nei file di log diventano difficili da consultare. Una riga come Errore database non indica necessariamente quale richiesta l’ha generata, quale utente fosse coinvolto o quale eccezione sia stata sollevata.

Una soluzione pratica consiste nell’utilizzare il logging strutturato in formato JSON. Ogni evento viene scritto come un oggetto JSON contenente campi standardizzati: data e ora, livello, messaggio, URL, metodo HTTP, identificativo della richiesta ed eventuali dettagli dell’eccezione.

In questo tutorial costruiremo un piccolo sistema di logging per PHP che:

  • scrive eventi in formato JSON;
  • assegna un identificativo univoco a ogni richiesta;
  • registra automaticamente gli errori non gestiti;
  • separa i dati tecnici mostrati all’utente da quelli salvati nel log;
  • rende i log facilmente analizzabili da strumenti esterni.

L’esempio utilizza solo funzionalità native di PHP e può essere adattato a un’applicazione web reale.

Codice completo

<?php

declare(strict_types=1);

/**
 * Logger JSON minimale.
 */
final class JsonLogger
{
    public function __construct(
        private readonly string $filePath
    ) {
    }

    /**
     * Scrive un evento nel file di log.
     *
     * @param array<string, mixed> $context
     */
    public function log(
        string $level,
        string $message,
        array $context = []
    ): void {
        $entry = [
            ´timestamp´ => date(DATE_ATOM),
            ´level´ => $level,
            ´message´ => $message,
            ´request_id´ => $_SERVER[´REQUEST_ID´] ?? null,
            ´http_method´ => $_SERVER[´REQUEST_METHOD´] ?? null,
            ´uri´ => $_SERVER[´REQUEST_URI´] ?? null,
            ´client_ip´ => $_SERVER[´REMOTE_ADDR´] ?? null,
            ´context´ => $context,
        ];

        $json = json_encode(
            $entry,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );

        if ($json === false) {
            error_log(´Impossibile serializzare un evento di logging.´);
            return;
        }

        // FILE_APPEND evita di sovrascrivere gli eventi precedenti.
        // LOCK_EX riduce il rischio di scritture contemporanee corrotte.
        file_put_contents(
            $this->filePath,
            $json . PHP_EOL,
            FILE_APPEND | LOCK_EX
        );
    }

    /**
     * Registra un´eccezione includendo le informazioni utili al debug.
     */
    public function exception(Throwable $exception, string $message): void
    {
        $this->log(´error´, $message, [
            ´exception_class´ => $exception::class,
            ´exception_message´ => $exception->getMessage(),
            ´file´ => $exception->getFile(),
            ´line´ => $exception->getLine(),
            ´trace´ => $exception->getTraceAsString(),
        ]);
    }
}

// Creazione dell´identificativo univoco della richiesta.
$requestId = bin2hex(random_bytes(16));
$_SERVER[´REQUEST_ID´] = $requestId;

$logger = new JsonLogger(__DIR__ . ´/logs/application.log´);

// Gestione delle eccezioni non catturate.
set_exception_handler(
    function (Throwable $exception) use ($logger): void {
        $logger->exception(
            $exception,
            ´Eccezione non gestita durante la richiesta´
        );

        http_response_code(500);

        // Non mostrare dettagli tecnici in produzione.
        echo ´Si è verificato un errore. Codice richiesta: ´
            . ($_SERVER[´REQUEST_ID´] ?? ´sconosciuto´);
    }
);

$logger->log(´info´, ´Richiesta ricevuta´, [
    ´script´ => $_SERVER[´SCRIPT_NAME´] ?? null,
]);

try {
    // Simulazione di una condizione anomala.
    throw new RuntimeException(´Connessione al servizio non disponibile´);
} catch (Throwable $exception) {
    $logger->exception(
        $exception,
        ´Errore durante l’elaborazione della richiesta´
    );

    http_response_code(503);

    echo ´Servizio temporaneamente non disponibile. ´
       . ´Codice richiesta: ´ . $requestId;
}

$logger->log(´info´, ´Richiesta completata´);

Spiegazione

Perché utilizzare JSON

Con il formato JSON ogni riga del file rappresenta un evento indipendente. Per esempio:

{"timestamp":"2026-09-03T10:15:20+00:00","level":"error","message":"Errore durante l’elaborazione della richiesta","request_id":"a13f...","http_method":"GET","uri":"/report.php","context":{"exception_class":"RuntimeException","file":"/var/www/report.php","line":72}}

Questa struttura è più utile rispetto a una stringa libera perché i campi possono essere filtrati e aggregati facilmente. Un sistema di monitoraggio può cercare tutti gli eventi con level uguale a error, mentre uno sviluppatore può trovare rapidamente tutti i messaggi appartenenti allo stesso request_id.

Il ruolo del request ID

L’identificativo della richiesta, chiamato spesso correlation ID o request ID, viene generato all’inizio dell’esecuzione. Lo stesso valore accompagna tutti i messaggi prodotti durante quella richiesta.

È particolarmente utile quando una pagina esegue molte operazioni: accesso al database, chiamate HTTP, validazione e generazione della risposta. Cercando l’identificativo nel file di log è possibile ricostruire il percorso completo dell’operazione.

In un’architettura con più servizi, il valore può anche essere inviato tramite un header HTTP, ad esempio X-Request-ID, così da collegare i log prodotti da applicazioni differenti.

Gestione delle eccezioni

set_exception_handler() intercetta le eccezioni che non sono state gestite da un blocco try...catch. Il logger salva classe, messaggio, file, riga e stack trace. Queste informazioni devono rimanere nei log e non essere mostrate direttamente agli utenti.

Nel codice gli errori catturati localmente vengono registrati e producono una risposta HTTP appropriata, in questo caso 503 Service Unavailable. L’utente riceve soltanto un messaggio generico e il codice della richiesta, utile per comunicare l’accaduto all’assistenza.

Best practice

  • Non registrare password o token: rimuovi sempre credenziali, cookie, numeri di carta e dati personali non necessari.
  • Usa nomi di campo coerenti: ad esempio mantieni sempre request_id, timestamp e context.
  • Proteggi la directory dei log: il file non dovrebbe essere scaricabile tramite il browser. È preferibile conservarlo fuori dalla directory pubblica.
  • Non mostrare lo stack trace in produzione: può rivelare percorsi interni, query o dettagli dell’infrastruttura.
  • Usa codici HTTP corretti: distingui tra errori del client, come 400, errori di autenticazione, come 401, ed errori del server, come 500.
  • Evita log eccessivi: registra eventi utili, ma non dati sensibili o informazioni ripetitive a ogni riga.
  • Controlla la scrittura: in un progetto reale verifica il valore restituito da file_put_contents() e gestisci eventuali problemi di permessi o spazio.
  • Valuta una libreria: per applicazioni complesse, strumenti come Monolog offrono handler, formattatori e integrazioni già pronti.

Riepilogo

Il logging strutturato in JSON rende gli errori PHP più facili da cercare, analizzare e collegare. Aggiungendo un identificativo univoco a ogni richiesta è possibile seguire una specifica operazione anche quando genera numerosi eventi.

La soluzione mostrata separa correttamente due responsabilità: il logger conserva dettagli tecnici per gli sviluppatori, mentre la risposta HTTP presenta all’utente un messaggio sicuro e comprensibile. Questa distinzione è essenziale sia per la sicurezza sia per la manutenzione dell’applicazione.

Approfondisci con risorse ufficiali

SHARE