{ } Programmazione

Logging nel codice: livelli, contesto e cosa non registrare

Come scrivere log utili nel codice: livelli di gravità, messaggi con contesto, log strutturati in JSON e dati da non registrare mai, con esempi.

Quando un programma gira sul tuo computer puoi osservarlo, fermarlo, ispezionarlo con il debugger. Quando gira su un server, alle tre di notte, servendo centinaia di utenti, l'unica finestra su ciò che accade sono i log. Un buon sistema di log trasforma un'indagine di ore in una ricerca di minuti; uno cattivo produce gigabyte di testo in cui l'informazione utile è introvabile.

Log e print non sono la stessa cosa

Molti iniziano con print() sparsi nel codice. Funziona per un esperimento, ma in un'applicazione reale mostra subito i limiti: non si può filtrare per importanza, non indica quando né dove è stato scritto il messaggio, non si può indirizzare a un file o a un servizio centralizzato senza modificare il codice. Le librerie di logging, come il modulo logging della libreria standard di Python, risolvono questi problemi separando cosa si registra da dove e come viene registrato.

import logging

logger = logging.getLogger(__name__)

def importa_clienti(percorso):
    logger.info("Inizio importazione da %s", percorso)
    ...

Usare getLogger(__name__) dà a ogni modulo un logger con il proprio nome, così nei log si vede subito da quale parte del codice arriva un messaggio.

I livelli di gravità

Ogni messaggio ha un livello. Usarli con coerenza è ciò che rende i log filtrabili:

Livello Quando usarlo Esempio
DEBUG Dettagli utili solo durante lo sviluppo o un'indagine Valori intermedi di un calcolo
INFO Eventi normali e significativi Avvio del servizio, importazione completata
WARNING Qualcosa di inatteso, ma il programma prosegue Tentativo ripetuto dopo un timeout
ERROR Un'operazione è fallita Impossibile salvare un ordine
CRITICAL Il sistema non può continuare a funzionare Database irraggiungibile all'avvio

In produzione si registra di solito da INFO in su; DEBUG si attiva temporaneamente quando serve. Un errore frequente è registrare tutto come ERROR: dopo qualche giorno nessuno guarda più gli errori, perché sono rumore.

Messaggi con contesto

Un messaggio come «Errore nel salvataggio» è quasi inutile. Quale salvataggio? Di cosa? Per chi? Un buon messaggio di log risponde alle domande che ti porrai quando lo leggerai:

logger.error(
    "Salvataggio ordine fallito: ordine=%s cliente=%s importo=%.2f",
    ordine.id, ordine.cliente_id, ordine.totale,
)

Includi gli identificativi che permettono di collegare il messaggio ai dati: ID dell'ordine, dell'utente, della richiesta. In un'applicazione web è molto utile assegnare a ogni richiesta un identificativo univoco e includerlo in tutti i messaggi che la riguardano: così si ricostruisce l'intero percorso di una singola richiesta tra decine di migliaia di righe.

Nota anche la forma: con il modulo logging è preferibile passare i parametri separatamente invece di costruire la stringa in anticipo. La formattazione avviene solo se il messaggio viene effettivamente registrato.

Registrare le eccezioni

All'interno di un blocco except, logger.exception() registra il messaggio a livello ERROR e aggiunge automaticamente il traceback completo:

try:
    invia_fattura(ordine)
except ErroreInvio:
    logger.exception("Invio fattura fallito per ordine %s", ordine.id)
    raise

Attenzione a un'abitudine dannosa: registrare la stessa eccezione a ogni livello mentre risale. Il risultato è lo stesso errore ripetuto cinque volte, che confonde chi legge. Registra l'errore una volta, nel punto in cui viene gestito.

Log strutturati

I log in testo libero sono comodi da leggere a occhio, ma difficili da analizzare con strumenti automatici. I log strutturati registrano ogni evento come un insieme di campi, di solito in formato JSON:

{"ts": "...", "livello": "ERROR", "evento": "salvataggio_ordine_fallito",
 "ordine": 4821, "cliente": 77, "durata_ms": 1530}

Con un formato di questo tipo un sistema di raccolta dei log può filtrare tutti gli eventi di un cliente, contare gli errori per tipo o calcolare tempi medi. Esistono librerie dedicate, come structlog per Python, ma anche il modulo standard può produrre JSON con un formatter personalizzato.

Cosa non registrare mai

I log vengono copiati, conservati, inviati a servizi esterni e letti da molte persone. Per questo alcune informazioni non devono mai finirci:

  • password, token di accesso, chiavi API;
  • numeri completi di carte di pagamento e codici di sicurezza;
  • contenuto integrale di documenti o messaggi privati;
  • dati personali non necessari all'indagine, anche per rispettare il principio di minimizzazione previsto dal GDPR.

Un caso tipico è registrare l'intero corpo di una richiesta HTTP per comodità: insieme ai dati utili finiscono nei log anche le credenziali. Meglio registrare solo i campi che servono, o mascherare quelli sensibili.

Configurazione separata dal codice

Il codice dovrebbe solo emettere messaggi; dove finiscono, con quale formato e da quale livello è una scelta di configurazione. In sviluppo puoi volere output colorato sul terminale a livello DEBUG, in produzione JSON a livello INFO verso un sistema centralizzato. Tenere questa configurazione in un unico punto, all'avvio dell'applicazione, evita di dover modificare decine di file.

In sintesi

  • Usa una libreria di logging al posto dei print, con un logger per modulo.
  • Applica i livelli con coerenza: non tutto è un errore.
  • Scrivi messaggi con contesto e identificativi, come l'ID della richiesta o dell'ordine.
  • Registra le eccezioni una sola volta, con il traceback, e valuta i log strutturati in JSON.
  • Non registrare mai password, token, dati di pagamento o dati personali non necessari.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche