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 processedcon 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:
|
|
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_ide, quando utile,span_linka 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_idlocale 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/FATALse possibile, applica sampling aggressivo aDEBUG. - 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
/tmpsenza 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/dockervaluta driverjson-filevsjournald; 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):
|
|
Node.js (pino):
|
|
Python (structlog):
|
|
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, nontheTimeItTookInMilliseconds). - 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.lognon 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