Logging best practices

Logging: best practices essenziali 🪵

Scrivere log non è un atto di fede: è un investimento di affidabilità. Log ben progettati riducono i tempi di diagnosi, evitano notti insonni e, sorpresa, fanno anche risparmiare. Qui trovi un set di pratiche da applicare subito, senza religioni né magie.

Usa una libreria di logging standard 📦

Evita soluzioni artigianali: le librerie mature gestiscono meglio concorrenza, formati, livelli, sink e prestazioni.

  • In produzione preferisci output strutturato (es. JSON) per l’ingestione nei sistemi di log.
  • Esempi affidabili: log/slog (Go), pino/winston (TypeScript/Node.js).
  • Configura formatter, livelli e destinazioni tramite configurazione, non codice.

Perché conta: una libreria standard risolve problemi noiosi (buffering, backpressure, formati, performance) così tu puoi concentrarti sul contenuto. Inoltre garantisce coerenza tra servizi e ambienti.

Scrivi messaggi significativi 🧠

Il log deve spiegare il “perché”, non solo il “cosa”.

  • Descrivi l’azione e l’esito: «payment autorizzato», «tentativo di login fallito».
  • Indica l’entità coinvolta: risorsa, operazione, quantità, durata.
  • Evita rumore: niente log che ripetono ovvietà o spammati in loop.

Meglio evitare frasi criptiche o onomatopee digitali. Un buon messaggio risponde a: cosa stavo facendo, su quale oggetto, con quale risultato, in quanto tempo. Se serve, aggiungi il «perché» (parametri e condizioni).

Esempi:

  • Male: Elaborazione completata (…di cosa? con che esito?)
  • Bene: order processed con campi {order_id, amount, duration_ms, outcome: 'authorized'}

Aggiungi contesto ai log 🧭

Il contesto trasforma una riga in una diagnosi. Aggiungi campi coerenti e stabili:

  • request_id / trace_id, user_id/session_id, service/component, version/build, region/zone.
  • Mantieni nomi dei campi consistenti tra servizi per facilitare le query.
  • Propaga gli identificatori di tracciamento lungo la call-chain.

Suggerimento: usa logger child/scoped per non dover ripetere gli stessi campi su ogni riga. In TypeScript: const log = logger.child({ trace_id, user_id }).

Evita informazioni sensibili 🔒

La privacy non è opzionale. Non loggare PII, segreti, token o payload non necessari.

  • Maschera selettivamente: email, IBAN, numeri di carta, indirizzi, token.
  • Implementa un redactor lato applicazione e, se possibile, un filtro a valle nell’ingestion pipeline.
  • Verifica la retention in base a compliance (GDPR, PCI-DSS, ecc.).

Applica il principio di data minimization: logga solo ciò che è utile a diagnosticare. Cifra i log in transito e a riposo nel sistema centrale. Esegui periodicamente una review dei campi per scovare dati inopportuni.

Scegli i livelli giusti di log 🎚️

Livelli coerenti = pagine on-call più leggere.

  • DEBUG: dettagli per lo sviluppo. Disabilitato in prod (o solo a runtime per tempi limitati).
  • INFO: eventi di business/operativi rilevanti.
  • WARN: anomalie recuperabili o degradazioni non bloccanti.
  • ERROR: fallimenti dell’operazione corrente, azione richiesta.
  • (Opz.) FATAL: termina il processo; usalo con parsimonia.

Regola pratica per HTTP:

  • 2xx/3xx → INFO
  • 4xx attese (es. 401/403) → WARN o INFO contestualizzato
  • 5xx → ERROR (investigazione necessaria)

Permetti override per modulo/componente e variazione dinamica del livello via env/feature‑flag.

Configura rotazione e retention dei log 🔁

I file crescono. Sempre. Metti limiti chiari.

  • Rotazione per dimensione e/o tempo; conserva N file compressi.
  • Spedisci su STDOUT in container, demanda la retention allo stack di log.
  • Evita log locali persistenti in ambienti effimeri (k8s, serverless).

In ambienti non containerizzati, usa strumenti di sistema (logrotate) con compressione e una retention definita. In cluster, preferisci shipper (es. Fluent Bit) con buffer su disco a capienza limitata.

Sincronizza gli orologi dei server ⏱️

Timestamp incoerenti = timeline impossibili.

  • Abilita NTP/Chrony e monitora skew e drift.
  • Logga in UTC e mostra timezone solo in UI/analisi.

Per misurazioni affidabili, registra anche le durate (ms) accanto ai timestamp. Per i calcoli interni, usa timer monotoni per evitare salti di clock.

Registra gli errori con stack trace 🧩

Senza stack, il debugging è archeologia.

  • Includi stack trace per ERROR/FATAL; per WARN solo se utile.
  • Arricchisci con causa radice (error wrapping) e metadati (input chiave, tentativi, latenza).

Evita di tagliare gli stack: i frammenti fuori contesto servono a poco. Se lo stack è molto verboso, invialo intero ma separa il campo (es. error.stack) per ricerche più rapide.

Evita di registrare in eccesso 🧹

Troppi log nascondono i problemi veri e costano.

  • Evita logging dentro loop stretti; deduplica e rate limit.
  • Non duplicare lo stesso evento a più livelli della call-stack.
  • Rendi il livello configurabile a runtime e documenta le policy.

Quando il traffico aumenta, applica sampling (es. 1% dei DEBUG, 10% degli INFO) e burst control. Per gli errori ripetitivi, usa chiavi di deduplica (es. error_fingerprint).

Centralizza i log 🗂️

Raccogli tutto in un log lake per correlazioni e ricerche.

  • Stack tipici: ELK/OpenSearch, Loki, Cloud Logging del provider.
  • Standardizza schema e campi per query cross‑service.
  • Collega log, metriche e tracing per osservabilità completa.

Definisci indici/stream per servizio e policy di ciclo di vita (ILM) per controllare spazio e tempi di conservazione. Riduci la cardinalità dei campi indicizzati: è la prima causa di costi fuori controllo.

Analizza e monitora i log 📈

I log non servono se nessuno li guarda.

  • Estrai metriche derivate (error rate, p95 latency, timeouts).
  • Crea alert azionabili su pattern e soglie (non su singoli eventi isolati).
  • Usa regole di correlazione per ridurre il rumore durante gli incidenti.

Conserva query salvate e dashboard per i casi ricorrenti (timeout DB, rate‑limit esterni, code piene). Collega gli alert a runbook chiari e brevi.

Testa la configurazione dei log 🧪

Fidarsi è bene, testare è meglio.

  • Test di integrazione: verifica formato, livelli, campi obbligatori e redaction.
  • Chaos logging: simula volumi alti, error burst, perdita del backend.
  • Convalida la pipeline end‑to‑end (app → shipper → indice → dashboard → alert).

Aggiungi snapshot test sul formato JSON e un check di schema drift. In staging, valida che i log di un incidente simulato generino gli stessi alert previsti.

Definisci uno schema e versioning dei log 🧬

Stabilisci un contratto: chiavi, tipi, semantica. Introduci log_schema_version e gestisci l’evoluzione in modo retro‑compatibile. Documenta i campi obbligatori e quelli opzionali.

Benefici: query più semplici, meno sorprese e possibilità di validazione automatica lato ingestion.

Esempio di evento JSON minimale ma utile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "ts": "2025-09-09T08:00:00Z",
  "level": "info",
  "msg": "order processed",
  "service": "checkout",
  "version": "1.8.3",
  "trace_id": "3f0b6b0e2a1c9b7d",
  "span_id": "e7b2d1a3",
  "request_id": "req_01HZW...",
  "user_id": "u_12345",
  "order_id": "ord_98765",
  "amount": 129.90,
  "currency": "EUR",
  "duration_ms": 84,
  "outcome": "authorized",
  "log_schema_version": 2
}

Collega log, metriche e tracing distribuito 🔗

Usa OpenTelemetry e propaga trace_id/span_id (W3C traceparent). I log dovrebbero permettere di saltare a metriche e trace con un click. Le indagini passano da «cosa» a «dove/quando» in un attimo.

  • Adotta le Semantic Conventions OTel per campi comuni (http.method, url.path, db.system, net.host.name, peer.service).
  • Collega log → span: allega trace_id/span_id e, quando utile, span_link a eventi significativi (retry, timeouts, errori).
  • Collega metriche → trace con exemplars (es. latenza p95 con puntatori ai trace più lenti).
  • Fallback: se mancano header di propagazione, genera e logga un trace_id locale per mantenere correlabilità almeno intra-servizio.

Gestisci i costi e la cardinalità 💸

Budget di cardinalità per chiavi libere (es. user_id). Evita di indicizzare campi ad altissima variabilità. Applica sampling adattivo e retention differenziata: breve per i raw log, lunga per le metriche derivate.

  • Indica quali campi sono indicizzati e con quale precisione; limita gli indici a pochi campi realmente usati nelle query operative.
  • Riduci cardinalità: normalizza valori (es. raggruppa messaggi simili via template + error_fingerprint).
  • Storage tiering: hot (7–14gg), warm (30–90gg), cold/archivio (S3/GCS con compressione); conserva solo metriche derivate a lungo termine.
  • Imposta quote di query e timebox (es. 7 giorni) per evitare ricerche costose involontarie.
  • Usa rollup/downsampling per analisi storiche (es. aggregati orari/giornalieri) invece dei raw log.

Progetta la resilienza dello shipping dei log 🚚

Decidi la strategia in caso di backend non disponibile:

  • Fail‑open: l’app continua e scarta i log in eccesso
  • Fail‑closed: l’app degrada/stoppa (solo per casi regolamentati)

Usa buffer locali con limiti, backoff esponenziale e telemetria sullo stato dello shipper.

  • Policy di drop per severità: non perdere ERROR/FATAL se possibile, applica sampling aggressivo a DEBUG.
  • Backoff con jitter e circuit breaker quando il backend è degradato; riabilita gradualmente al recupero.
  • Limiti chiari: max_batch_size, max_queue_bytes, max_retry_interval; evidenzia superamenti con metriche/alert.
  • Spool su disco con cap e scadenza (TTL) per evitare out‑of‑disk; invia conteggi di dropped_events_total.
  • Dead letter per eventi non conformi allo schema (ispezionabili a parte) per non bloccare il flusso.
  • Telemetria minima: queue_depth, flush_latency, retries_total, success_rate, backend_status.

Logging in container e Kubernetes 🧩

  • Scrivi su STDOUT/STDERR; lascia la raccolta a container runtime + log shipper (es. Fluent Bit/Vector).
  • In k8s, evita sidecar inutili: preferisci DaemonSet di shipper con parsing strutturato.
  • Aggiungi label/annotazioni utili: app, version, pod, node, namespace, cluster.
  • Gestisci rotazione dei file del runtime (containerd/docker): imposta limiti per dimensione e count.
  • Non persistere log nell’immagine; niente log in /tmp senza limiti.

Ulteriori note pratiche:

  • Preferisci un log per riga in JSON per evitare problemi di multiline; se hai stack multilinea, configura il parser del shipper.
  • Con containerd/docker valuta driver json-file vs journald; standardizza a livello di cluster.
  • Evita colori/ANSI nei log: sporcano l’output JSON e complicano il parsing.
  • Assegna risorse allo shipper (CPU/memoria) e limita la raccolta di namespace rumorosi con filtri/regex.
  • Usa ephemeral containers per il debug, non modificare runtime di logging dei pod in produzione.

Esempi rapidi di implementazione 🧪

Go (slog, JSON):

1
2
3
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
log := logger.With("service", "checkout", "version", "1.8.3", "trace_id", traceID)
log.Info("order processed", "order_id", ordID, "amount", amount, "duration_ms", dur.Milliseconds(), "outcome", "authorized")

Node.js (pino):

1
2
3
4
const pino = require('pino')
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
const log = logger.child({ service: 'checkout', version: '1.8.3', trace_id })
log.info({ order_id: ordId, amount, duration_ms: 84, outcome: 'authorized' }, 'order processed')

Python (structlog):

1
2
3
4
import structlog, sys
structlog.configure(processors=[structlog.processors.JSONRenderer()])
log = structlog.get_logger(service="checkout", version="1.8.3", trace_id=trace_id)
log.info("order processed", order_id=ord_id, amount=amount, duration_ms=84, outcome="authorized")

Prestazioni e overhead ⚙️

  • Evita interpolazioni costose quando il livello è disabilitato; usa logging lazily evaluato.
  • Batch e buffering: abilita flush asincroni e backpressure sicuri.
  • Riduci il payload: usa chiavi corte ma chiare (duration_ms, non theTimeItTookInMilliseconds).
  • Evita I/O sincrono su percorsi di hot‑path; sposta i log non critici su code asincrone.

Altri accorgimenti:

  • Evita serializzazioni JSON di grandi strutture; logga hash o dimensioni e conserva i dettagli altrove.
  • Riutilizza istanze di logger e buffer per ridurre allocazioni (pooling).
  • Considera il trade‑off compressione CPU/spazio nella pipeline di shipping.
  • Standardizza timestamp (RFC3339 UTC) e valuta epoch per ridurre costi di parsing nel backend.

Governance, linters e convenzioni 📏

Standardizza il naming (es. snake_case o lowerCamelCase), evita sinonimi (user_id vs uid). Aggiungi un linter dei log nelle pipeline che rifiuti formati non conformi o chiavi vietate.

  • Mantieni un piccolo schema registry con versioni e changelog; richiedi approvazione per breaking changes.
  • Esegui validazioni in CI (sample di log generati dai test) contro lo schema.
  • Definisci chiavi vietate (PII, segreti) e un processo di deprecazione dei campi.
  • Pre‑commit: hook che impediscono l’introduzione di console.log non strutturati o print sporadici.

Eventi di sicurezza e audit immutabile 🔐

Separa i security/audit log (append‑only, marcati temporalmente, firma/immutabilità). Definisci ruoli di accesso e retention conformi alle policy.

  • Valuta storage WORM (es. S3 Object Lock) e timestamp RFC3161 per non ripudio.
  • Usa hash chain/firma dei blocchi di log per evidenziare manomissioni.
  • Segrega chiavi KMS e audit trail degli accessi; alert su cancellazioni o gap temporali.

Lingua e localizzazione dei log 🌐

Preferisci messaggi in lingua inglese o terminologia neutra per facilitare ricerca e condivisione. La localizzazione va riservata alle UI; i log servono a macchine e on‑call globali.

  • Separa il testo umano dal codice d’errore (error_code, event_id); le query usano i codici, non le frasi.
  • Mantieni template di messaggi stabili; evita variazioni linguistiche che complicano le ricerche.

Runbook, query e dashboard pronte all’uso 🧰

Ogni alert deve avere:

  • Query di diagnosi salvata
  • Dashboard collegata
  • Runbook con passi riproducibili e tempi attesi

Suggerimenti pratici:

  • Query tipiche: error rate ERROR/5m, timeouts per dipendenza, spike di latenza p95 per endpoint.
  • Dashboard minime: panorama errori per servizio, latenza per rotta, correlazione con rilasci (deploy overlay).
  • Link dagli alert al runbook e alle query pronte con filtri (servizio, versione, trace_id).

Checklist veloce 📝

  • Libreria standard con output strutturato
  • Messaggi chiari e utili, con contesto coerente
  • Redaction attiva per dati sensibili
  • Livelli configurati e documentati
  • Rotazione e retention impostate
  • Sincronizzazione oraria abilitata (UTC)
  • Stack trace sugli errori significativi
  • Rumore sotto controllo (dedup/rate limit)
  • Centralizzazione e correlazione con metriche/tracing
  • Alert e test end‑to‑end della pipeline
  • Schema/versioning dei log documentato
  • Budget di cardinalità e cost governance
  • Strategia di fail‑open/fail‑closed per lo shipping
  • Linter e convenzioni di naming applicati
  • Runbook, query e dashboard collegate
Ultimo aggiornamento il