slog in un esempio
log/slog (Go 1.21) scrive record strutturati: un messaggio, un livello e attributi chiave-valore.
Ogni riga esce come time=... level=INFO msg="user logged in" user=ada attempts=1. Siccome ogni valore è un campo separato, un sistema di raccolta log (Loki, Elasticsearch, CloudWatch, Datadog) può filtrare su user=ada o level=ERROR senza espressioni regolari sul testo libero.
Gli esempi di questa pagina scrivono su os.Stdout così l'output compare in ordine. In un servizio reale, i log di solito vanno su os.Stderr, che è anche dove scrive il logger predefinito.
Livelli
| Livello | Valore | Da usare per |
|---|---|---|
slog.LevelDebug | -4 | dettagli per chi sviluppa, disattivati in produzione |
slog.LevelInfo | 0 | eventi normali: avvio, richiesta servita, job terminato |
slog.LevelWarn | 4 | qualcosa di inatteso che il programma ha gestito |
slog.LevelError | 8 | un'operazione è fallita |
L'handler scarta i record sotto il suo livello minimo, e il minimo predefinito è Info. Per questo slog.Debug(...) non stampa niente finché non configuri un handler con Level: slog.LevelDebug. Gli spazi tra i valori lasciano posto a livelli personalizzati come slog.Level(2).
Per cambiare il livello a runtime (da un flag, da un endpoint di amministrazione o da un segnale), metti uno slog.LevelVar nelle opzioni e chiama Set su di esso più avanti:
var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written
Testo o JSON
slog.NewTextHandler scrive coppie key=value, facili da leggere in un terminale. slog.NewJSONHandler scrive un oggetto JSON per riga, il formato che si aspetta la maggior parte delle pipeline di log. Le chiamate di logging restano uguali; cambia solo l'handler.
I valori mantengono il loro tipo: status è un numero nell'output JSON e retry un booleano, un time.Duration si stampa come 42ms nel testo e in nanosecondi nel JSON, e un error stampa il suo messaggio. ReplaceAttr è il punto di aggancio per riscrivere o rimuovere attributi, usato qui per togliere il timestamp, e in pratica per rinominare le chiavi (msg in message) o oscurare valori.
Attributi
La forma a tipizzazione libera alterna chiavi e valori: "user", "ada", "attempts", 3. È breve, e ha un solo modo di sbagliare: un numero dispari di argomenti. Il valore avanzato viene registrato sotto la chiave !BADKEY. go vet lo intercetta:
./main.go:14:2: call to slog.Info missing a final value
Per la sicurezza dei tipi e qualche allocazione in meno, usa i costruttori di attributi, e LogAttrs quando fai logging in un percorso critico per le prestazioni:
logger.Info("order placed",
slog.Int("order_id", 1017),
slog.String("currency", "EUR"),
slog.Float64("total", 59.90),
slog.Duration("took", elapsed),
)
logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))
Usa una sola convenzione di nomi per le chiavi in tutto il codice (user_id ovunque, non userID in un package e uid in un altro). Le query nel tuo sistema di log dipendono da questo.
With: logger che portano il contesto
logger.With(attrs...) restituisce un nuovo logger che aggiunge quegli attributi a ogni record. Creane uno per ogni richiesta o per ogni job, e ogni riga che scrive si può collegare alle altre:
Ogni riga porta service, version, request_id e user senza ripeterli a ogni chiamata. slog.Group annida gli attributi, che l'handler JSON scrive come oggetto annidato ("payment":{"amount":25,"currency":"USD"}) e l'handler testuale come chiavi con il punto (payment.amount=25). logger.WithGroup("db") mette sotto un gruppo ogni attributo successivo di quel logger.
Passa il logger legato alla richiesta verso il basso come parametro o come campo di una struct. Salvarlo in un context.Context è possibile ma nasconde la dipendenza; i metodi InfoContext(ctx, ...) di slog passano il context all'handler, e un handler personalizzato può usarlo per estrarre gli ID di trace.
Nascondere i segreti con LogValuer
Un tipo può controllare come viene registrato nei log implementando slog.LogValuer. Così password e token restano fuori dai log, chiunque sia a registrare il valore:
User registra solo il suo ID e l'email, e un Token registrato da solo stampa REDACTED. L'handler chiama LogValue solo quando il record viene effettivamente scritto, quindi funziona anche per valori costosi da calcolare.
Il classico package log
log è nato prima di slog e va ancora bene per piccoli programmi e script. Scrive righe sullo standard error con un prefisso di data e ora:
| Flag | Aggiunge |
|---|---|
log.LstdFlags (il predefinito) | data e ora 2009/11/10 23:00:00 |
log.Lmicroseconds | i microsecondi sull'ora |
log.LUTC | l'ora in UTC |
log.Lshortfile / log.Llongfile | main.go:14 / il percorso completo |
log.Lmsgprefix | mette il prefisso prima del messaggio invece che all'inizio della riga |
Tre funzioni terminano il programma o vanno in panic, e la differenza conta:
log.Fatal,log.Fatalf,log.Fatallnstampano e poi chiamanoos.Exit(1). Le chiamate in defer non vengono eseguite. Usale inmainper gli errori di avvio, mai nel codice di una libreria o negli handler delle richieste.log.Panice simili stampano e poi vanno in panic, quindi le chiamate in defer vengono eseguite e il panic si può recuperare.- Tutte le altre funzioni scrivono solo una riga.
Per scrivere il log su un file, aprilo e passalo a log.New o a log.SetOutput; io.MultiWriter(os.Stderr, f) scrive su entrambi.
log e slog insieme
slog.SetDefault(logger) rende logger quello predefinito per le funzioni di primo livello come slog.Info, e fa passare attraverso di esso anche l'output del package log. Le chiamate log.Printf già presenti nel tuo codice o nelle dipendenze escono allora come record strutturati a livello Info:
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
Prima di SetDefault, il logger slog predefinito scrive attraverso il package log, ed è per questo che un semplice slog.Info("hi") stampa 2026/09/23 14:30:00 INFO hi.
Regole pratiche
- Registra un errore o restituiscilo, non entrambe le cose. Una funzione che registra un errore e lo restituisce fa sì che lo stesso fallimento finisca nei log a ogni livello dello stack di chiamate. Restituisci gli errori verso l'alto con contesto, e registrali una volta sola dove vengono gestiti.
- Metti i dati variabili negli attributi, non nel messaggio.
logger.Info("user created", "user_id", id)si raggruppa bene in un sistema di log;logger.Info(fmt.Sprintf("user %d created", id))crea un messaggio diverso per ogni utente. - Non registrare mai segreti o body completi delle richieste. Usa
LogValueroReplaceAttrper oscurarli. - Usa JSON in produzione, testo in sviluppo. Scegli l'handler all'avvio da un flag o da una variabile d'ambiente.
- Scegli i livelli con cura. Se tutto viene registrato come Error, gli allarmi sugli errori diventano rumore.
Domande frequenti
Cos'è slog in Go?
log/slog è il package di logging strutturato aggiunto alla libreria standard in Go 1.21. Invece di stringhe formattate, ogni record ha un messaggio, un livello (Debug, Info, Warn, Error) e attributi chiave-valore, e un handler lo scrive come testo key=value o come JSON: slog.Info("login", "user", "ada", "attempts", 3).
Come attivo i log di debug in slog?
Il livello minimo predefinito è Info, quindi slog.Debug non stampa niente. Crea un handler con un livello più basso e rendilo quello predefinito: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Usa uno slog.LevelVar invece di una costante se vuoi cambiare il livello mentre il programma gira.
Qual è la differenza tra log e slog in Go?
log scrive righe libere con un prefisso facoltativo di data e ora e non ha livelli. slog scrive record con livelli e attributi chiave-valore tipizzati che i sistemi di raccolta log possono analizzare e filtrare. Sono entrambi nella libreria standard; slog.SetDefault reindirizza anche l'output del package log attraverso l'handler di slog.
log.Fatal esegue le funzioni in defer?
No. log.Fatal e log.Fatalf stampano il messaggio e chiamano os.Exit(1), che salta tutte le chiamate in defer. Usali solo in main o nel codice di avvio dove non c'è niente da ripulire. log.Panic invece va in panic, quindi le chiamate in defer vengono eseguite.