Logging Strutturato con JSONL
Una guida completa al logging strutturato usando il formato JSONL (JSON Lines). Impara a integrare con ELK Stack, Fluentd, CloudWatch, GCP Logging e Azure Monitor con esempi di codice pronti per la produzione.
Ultimo aggiornamento: Febbraio 2026
Perché il Logging Strutturato con JSONL?
I log tradizionali in testo puro sono leggibili dall'uomo ma quasi impossibili da analizzare in modo affidabile per le macchine. Una riga come '2024-01-15 ERROR User login failed for admin' nasconde la severità, il tipo di evento e il nome utente all'interno di una stringa non strutturata. Ogni strumento di aggregazione dei log deve ricorrere a espressioni regolari fragili per estrarre significato, e qualsiasi modifica al formato del log rompe la pipeline.
Il logging strutturato risolve questo problema emettendo ogni voce di log come un oggetto JSON con campi ben definiti. JSONL (JSON Lines) fa un passo ulteriore: un oggetto JSON per riga, nessun array avvolgente, nessuna virgola finale. Questo rende i log JSONL adatti all'append, allo streaming e banalmente analizzabili da ogni piattaforma di log principale. In questa guida imparerai come produrre log JSONL da Python, Node.js e Go, e come inviarli a ELK, Fluentd e ai tre principali provider cloud.
Rispetto ad altri formati strutturati come CSV o XML, JSONL offre il miglior equilibrio di flessibilità, supporto degli strumenti e leggibilità umana. I campi possono essere nidificati, i tipi sono preservati e non è mai necessario definire uno schema fisso in anticipo. Ecco perché JSONL è diventato lo standard de facto per il logging strutturato nelle infrastrutture moderne.
Librerie di Logging per Linguaggio
La maggior parte delle librerie di logging moderne supporta l'output JSON strutturato nativamente o tramite una semplice modifica della configurazione. Di seguito le librerie consigliate per Python, Node.js e Go, ciascuna produce output compatibile con JSONL.
Python: structlog
Consigliatostructlog è la libreria di logging strutturato leader per Python. Avvolge il modulo logging standard e produce output JSON per default. La sua pipeline di processori ti permette di arricchire, filtrare e trasformare le voci di log prima della serializzazione.
import structlog# Configure structlog for JSONL outputstructlog.configure(processors=[structlog.processors.TimeStamper(fmt="iso"),structlog.processors.add_log_level,structlog.processors.StackInfoRenderer(),structlog.processors.JSONRenderer(),],wrapper_class=structlog.BoundLogger,context_class=dict,logger_factory=structlog.PrintLoggerFactory(),)log = structlog.get_logger()# Each call produces one JSONL linelog.info("user.login", user_id="u-42", ip="10.0.0.1")# {"event":"user.login","user_id":"u-42","ip":"10.0.0.1","level":"info","timestamp":"2026-02-15T08:30:00Z"}log.error("db.connection_failed", host="db-primary", retries=3)# {"event":"db.connection_failed","host":"db-primary","retries":3,"level":"error","timestamp":"2026-02-15T08:30:01Z"}
Node.js: pino
Più Velocepino è il logger Node.js più veloce, produce output JSONL per default senza alcuna configurazione. Raggiunge un basso overhead attraverso la scrittura asincrona e un percorso di serializzazione minimo. Il pacchetto companion pino-pretty fornisce output leggibile per lo sviluppo locale.
import pino from 'pino';// pino outputs JSONL by defaultconst log = pino({level: 'info',timestamp: pino.stdTimeFunctions.isoTime,});// Each call produces one JSONL linelog.info({ userId: 'u-42', ip: '10.0.0.1' }, 'user.login');// {"level":30,"time":"2026-02-15T08:30:00.000Z","userId":"u-42","ip":"10.0.0.1","msg":"user.login"}log.error({ host: 'db-primary', retries: 3 }, 'db.connection_failed');// {"level":50,"time":"2026-02-15T08:30:01.000Z","host":"db-primary","retries":3,"msg":"db.connection_failed"}// Child loggers inherit contextconst reqLog = log.child({ requestId: 'req-abc-123' });reqLog.info({ path: '/api/users' }, 'request.start');
Go: zerolog
Zero Allocazionizerolog è un logger JSON a zero allocazioni per Go, progettato per le massime prestazioni in servizi ad alto throughput. Scrive direttamente su un io.Writer senza allocazioni intermedie, rendendolo ideale per applicazioni sensibili alla latenza. La sua API a catena è sia ergonomica che efficiente.
package mainimport ("os""github.com/rs/zerolog""github.com/rs/zerolog/log")func main() {// Configure JSONL output to stdoutzerolog.TimeFieldFormat = zerolog.TimeFormatUnixlog.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()// Each call produces one JSONL linelog.Info().Str("user_id", "u-42").Str("ip", "10.0.0.1").Msg("user.login")// {"level":"info","user_id":"u-42","ip":"10.0.0.1","time":1739608200,"message":"user.login"}log.Error().Str("host", "db-primary").Int("retries", 3).Msg("db.connection_failed")}
Integrazione ELK Stack
Lo ELK Stack (Elasticsearch, Logstash, Kibana) è la piattaforma open-source di gestione dei log più diffusa. I log JSONL si integrano perfettamente perché Logstash e Filebeat possono analizzare JSON nativamente, eliminando la necessità di pattern grok complessi.
Filebeat è il log shipper leggero che segue i tuoi file di log JSONL e li inoltra a Logstash o Elasticsearch. Impostare json.keys_under_root a true promuove i campi JSON a campi Elasticsearch di primo livello, rendendoli direttamente ricercabili in Kibana.
# filebeat.ymlfilebeat.inputs:- type: logenabled: truepaths:- /var/log/app/*.jsonljson.keys_under_root: truejson.add_error_key: truejson.message_key: messagejson.overwrite_keys: trueoutput.elasticsearch:hosts: ["http://elasticsearch:9200"]index: "app-logs-%{+yyyy.MM.dd}"# Optional: send to Logstash instead# output.logstash:# hosts: ["logstash:5044"]
Se hai bisogno di trasformare o arricchire i log prima dell'indicizzazione, Logstash si posiziona tra Filebeat ed Elasticsearch. Il codec json analizza automaticamente ogni riga JSONL. Usa il filtro mutate per rinominare campi, aggiungere tag o rimuovere dati sensibili prima dell'archiviazione.
# logstash.confinput {beats {port => 5044codec => json}}filter {# Parse timestamp from the JSON fielddate {match => ["timestamp", "ISO8601"]target => "@timestamp"}# Add environment tagmutate {add_field => { "environment" => "production" }remove_field => ["agent", "ecs", "host"]}# Route by log levelif [level] == "error" or [level] == "fatal" {mutate { add_tag => ["alert"] }}}output {elasticsearch {hosts => ["http://elasticsearch:9200"]index => "app-logs-%{+YYYY.MM.dd}"}}
Fluentd e Fluent Bit
Fluentd e il suo fratello leggero Fluent Bit sono progetti CNCF graduated ampiamente usati negli ambienti Kubernetes. Entrambi gestiscono log JSONL nativamente e possono inoltrarli a virtualmente qualsiasi destinazione, da Elasticsearch a S3 ai servizi di log cloud-nativi.
Fluent Bit è il raccoglitore di log preferito per Kubernetes e ambienti con risorse limitate. Consuma circa 450KB di memoria e può elaborare centinaia di migliaia di record al secondo. Il parser json gestisce l'input JSONL senza alcuna configurazione personalizzata.
[SERVICE]Flush 5Daemon OffLog_Level infoParsers_File parsers.conf[INPUT]Name tailPath /var/log/app/*.jsonlParser jsonTag app.*Refresh_Interval 5[FILTER]Name modifyMatch app.*Add cluster production-us-east-1Add service my-api[OUTPUT]Name esMatch app.*Host elasticsearchPort 9200Index app-logsType _docLogstash_Format On
Fluentd offre un ecosistema di plugin più ricco rispetto a Fluent Bit, con oltre 500 plugin della comunità. Usalo quando hai bisogno di routing avanzato, buffering o trasformazioni. Il plugin in_tail con format json legge nativamente i file JSONL.
# fluentd.conf<source>@type tailpath /var/log/app/*.jsonlpos_file /var/log/fluentd/app.postag app.logs<parse>@type jsontime_key timestamptime_format %Y-%m-%dT%H:%M:%S%z</parse></source><filter app.logs>@type record_transformer<record>hostname "#{Socket.gethostname}"environment production</record></filter><match app.logs>@type elasticsearchhost elasticsearchport 9200index_name app-logslogstash_format true<buffer>@type memoryflush_interval 5schunk_limit_size 5m</buffer></match>
Piattaforme di Logging Cloud
Tutti e tre i principali provider cloud accettano log in formato JSONL e analizzano automaticamente i campi JSON in attributi ricercabili e filtrabili. Questo elimina la necessità di gestire la propria infrastruttura di log pur offrendo le stesse capacità di query strutturate.
AWS CloudWatch Logs
AWSCloudWatch Logs analizza automaticamente le voci di log JSON, rendendo ogni campo interrogabile con CloudWatch Logs Insights. Quando la tua applicazione gira su ECS, Lambda o EC2 con l'agente CloudWatch, l'output JSONL viene indicizzato come dati strutturati senza configurazione aggiuntiva. Usa la sintassi simile a SQL di Logs Insights per interrogare milioni di voci di log in pochi secondi.
# CloudWatch Logs Insights query examples# Find all errors in the last hourfields @timestamp, level, event, user_id| filter level = "error"| sort @timestamp desc| limit 100# Aggregate error counts by event typefields event| filter level = "error"| stats count(*) as error_count by event| sort error_count desc# P99 latency by endpointfields path, duration_ms| filter ispresent(duration_ms)| stats pct(duration_ms, 99) as p99 by path| sort p99 desc
GCP Cloud Logging
GCPGoogle Cloud Logging (precedentemente Stackdriver) tratta i payload JSON come voci di log strutturate con supporto di prima classe. Quando scrivi JSONL su stdout su Cloud Run, GKE o Cloud Functions, l'agente di logging analizza ogni riga in un LogEntry strutturato. Il campo severity si mappa direttamente ai livelli di severità di Cloud Logging, e i campi jsonPayload diventano indicizzati e ricercabili.
# Python example for Cloud Run / Cloud Functionsimport jsonimport sysdef log(severity: str, message: str, **fields):"""Write a JSONL log entry compatible with GCP Cloud Logging."""entry = {"severity": severity.upper(), # Maps to Cloud Logging severity"message": message,**fields,}print(json.dumps(entry), file=sys.stdout, flush=True)# GCP automatically parses these as structured logslog("INFO", "user.login", user_id="u-42", ip="10.0.0.1")log("ERROR", "db.timeout", host="db-primary", latency_ms=5200)# Query in Cloud Logging:# jsonPayload.user_id = "u-42"# severity >= ERROR
Azure Monitor Logs
AzureAzure Monitor ingerisce log JSONL tramite l'Azure Monitor Agent o l'ingestione diretta via API. Application Insights analizza automaticamente le tracce JSON e gli eventi personalizzati. KQL (Kusto Query Language) fornisce interrogazioni potenti su dati di log strutturati con supporto per aggregazione, analisi delle serie temporali e rilevamento delle anomalie.
// KQL queries for JSONL logs in Azure Monitor// Find recent errorsAppTraces| where SeverityLevel >= 3| extend parsed = parse_json(Message)| project TimeGenerated, parsed.event, parsed.user_id,parsed.error_message| order by TimeGenerated desc| take 100// Error rate over time (5-minute buckets)AppTraces| where SeverityLevel >= 3| summarize ErrorCount = count() by bin(TimeGenerated, 5m)| render timechart// Top error eventsAppTraces| where SeverityLevel >= 3| extend parsed = parse_json(Message)| summarize Count = count() by tostring(parsed.event)| top 10 by Count
Best Practice per il Logging JSONL
Seguire convenzioni coerenti tra i tuoi servizi rende i log JSONL più facili da interrogare, correlare e utilizzare per gli alert. Queste best practice derivano da sistemi di produzione che elaborano miliardi di voci di log al giorno.
Usa Livelli di Log Coerenti
Adotta una scala di severità standard e usala in modo coerente su tutti i servizi. La convenzione più comune utilizza cinque livelli: DEBUG per la diagnostica di sviluppo, INFO per le operazioni normali, WARN per problemi recuperabili, ERROR per guasti che richiedono attenzione e FATAL per crash irrecuperabili. Mappa i livelli numerici (come la scala 10-60 di pino) a questi nomi stringa al momento dell'ingestione per query uniformi.
{"level":"debug","event":"cache.miss","key":"user:42","timestamp":"2026-02-15T08:30:00Z"}{"level":"info","event":"request.completed","method":"GET","path":"/api/users","status":200,"duration_ms":45,"timestamp":"2026-02-15T08:30:01Z"}{"level":"warn","event":"rate_limit.approaching","client_id":"c-99","current":950,"limit":1000,"timestamp":"2026-02-15T08:30:02Z"}{"level":"error","event":"payment.failed","order_id":"ord-123","error":"card_declined","timestamp":"2026-02-15T08:30:03Z"}{"level":"fatal","event":"startup.failed","reason":"missing DATABASE_URL","timestamp":"2026-02-15T08:30:04Z"}
Standardizza i Nomi dei Campi
Definisci uno schema condiviso per i campi comuni tra tutti i servizi. Usa snake_case per i nomi dei campi, ISO 8601 per i timestamp e nomi coerenti per gli identificatori di correlazione. Come minimo, ogni voce di log dovrebbe contenere: timestamp, level, event (un nome evento leggibile dalla macchina) e service. Le voci con scope di richiesta dovrebbero includere request_id e user_id per il tracing.
// Recommended field schema{"timestamp": "2026-02-15T08:30:00.000Z", // ISO 8601, always UTC"level": "info", // debug|info|warn|error|fatal"event": "order.created", // dot-notation event name"service": "order-api", // emitting service"version": "1.4.2", // service version"request_id": "req-abc-123", // correlation ID"trace_id": "4bf92f3577b34da6", // distributed trace ID"user_id": "u-42", // authenticated user"duration_ms": 120, // numeric, not string"order_id": "ord-789", // domain-specific context"items_count": 3 // domain-specific context}
Considerazioni sulle Prestazioni
Il logging strutturato aggiunge overhead di serializzazione a ogni chiamata di log. Nei servizi ad alto throughput, questo può diventare significativo. Usa il logging asincrono (pino, zerolog) per spostare la serializzazione fuori dal percorso critico. Evita di loggare oggetti grandi o body di richiesta/risposta a livello INFO. Usa il campionamento per i log di debug ad alto volume. Scrivi su file locali e lascia che un sidecar (Filebeat, Fluent Bit) gestisca l'invio, piuttosto che inviare log sulla rete in modo sincrono.
Usa logger asincroni (pino, zerolog) per evitare di bloccare il thread principale durante la serializzazione JSON.
Non loggare mai body completi di richiesta o risposta a livello INFO. Usa il livello DEBUG e attivalo solo durante il troubleshooting.
Campiona gli eventi ad alto volume. Logga 1 ogni 100 richieste di health-check invece di tutte.
Scrivi i log su file locali e inviali in modo asincrono con Filebeat o Fluent Bit. Evita chiamate di rete sincrone nel percorso di log.
Imposta una dimensione massima della riga di log (es. 16KB) per impedire che voci sovradimensionate intasino la pipeline.
Ruota i file di log con logrotate o la rotazione integrata nella libreria per prevenire l'esaurimento del disco.
Valida i Tuoi Log JSONL Online
Usa i nostri strumenti gratuiti basati su browser per ispezionare, validare e convertire file di log JSONL. Nessun upload necessario, tutta l'elaborazione avviene localmente.