slog en un ejemplo
log/slog (Go 1.21) escribe registros estructurados: un mensaje, un nivel y atributos clave-valor.
Cada línea sale como time=... level=INFO msg="user logged in" user=ada attempts=1. Como cada valor es un campo aparte, un recolector de logs (Loki, Elasticsearch, CloudWatch, Datadog) puede filtrar por user=ada o level=ERROR sin expresiones regulares sobre texto libre.
Los ejemplos de esta página escriben en os.Stdout para que la salida aparezca en orden. En un servicio real, los logs suelen ir a os.Stderr, que es también donde escribe el logger por defecto.
Niveles
| Nivel | Valor | Para qué |
|---|---|---|
slog.LevelDebug | -4 | detalles para desarrolladores, desactivado en producción |
slog.LevelInfo | 0 | eventos normales: arrancado, petición atendida, trabajo terminado |
slog.LevelWarn | 4 | algo inesperado que el programa ha gestionado |
slog.LevelError | 8 | una operación ha fallado |
El handler descarta los registros por debajo de su nivel mínimo, y el mínimo por defecto es Info. Por eso slog.Debug(...) no imprime nada hasta que configuras un handler con Level: slog.LevelDebug. Los huecos entre valores dejan sitio para niveles propios como slog.Level(2).
Para cambiar el nivel en tiempo de ejecución (desde un flag, un endpoint de administración o una señal), pon un slog.LevelVar en las opciones y llama a su Set más adelante:
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
Texto o JSON
slog.NewTextHandler escribe pares key=value, fáciles de leer en una terminal. slog.NewJSONHandler escribe un objeto JSON por línea, el formato que esperan la mayoría de pipelines de logs. Las llamadas de logging no cambian; solo cambia el handler.
Los valores conservan sus tipos: status es un número en la salida JSON y retry un booleano, un time.Duration se imprime como 42ms en texto y en nanosegundos en JSON, y un error imprime su mensaje. ReplaceAttr es el gancho para reescribir o quitar atributos; aquí se usa para quitar la marca de tiempo, y en la práctica para renombrar claves (msg a message) o censurar valores.
Atributos
La forma poco tipada alterna claves y valores: "user", "ada", "attempts", 3. Es corta y tiene un modo de fallo: un número impar de argumentos. El valor sobrante se registra bajo la clave !BADKEY. go vet lo detecta:
./main.go:14:2: call to slog.Info missing a final value
Para tener seguridad de tipos y algo menos de reservas de memoria, usa los constructores de atributos, y LogAttrs cuando registres en una ruta caliente:
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 convención de nombres para las claves en todo el código (user_id en todas partes, no userID en un paquete y uid en otro). Las consultas en tu sistema de logs dependen de ello.
With: loggers que llevan contexto
logger.With(attrs...) devuelve un logger nuevo que añade esos atributos a cada registro. Crea uno por petición o por trabajo, y todas las líneas que escriba se podrán relacionar entre sí:
Cada línea lleva service, version, request_id y user sin repetirlos en cada llamada. slog.Group anida atributos, que el handler JSON escribe como un objeto anidado ("payment":{"amount":25,"currency":"USD"}) y el handler de texto como claves con puntos (payment.amount=25). logger.WithGroup("db") mete en un grupo todos los atributos posteriores de ese logger.
Pasa el logger de la petición hacia abajo como parámetro o como campo de un struct. Guardarlo en un context.Context es posible, pero esconde la dependencia; los métodos InfoContext(ctx, ...) de slog pasan el context al handler, que un handler propio puede usar para extraer IDs de traza.
Ocultar secretos con LogValuer
Un tipo puede controlar cómo se registra implementando slog.LogValuer. Así las contraseñas y los tokens quedan fuera de los logs, lo registre quien lo registre:
User registra solo su ID y su email, y un Token registrado por sí solo imprime REDACTED. El handler llama a LogValue solo cuando el registro se escribe de verdad, así que también sirve para valores costosos de calcular.
El paquete clásico log
log es anterior a slog y sigue siendo adecuado para programas pequeños y scripts. Escribe líneas en la salida de error estándar con un prefijo de fecha y hora:
| Flag | Añade |
|---|---|
log.LstdFlags (el predeterminado) | fecha y hora 2009/11/10 23:00:00 |
log.Lmicroseconds | microsegundos en la hora |
log.LUTC | hora en UTC |
log.Lshortfile / log.Llongfile | main.go:14 / la ruta completa |
log.Lmsgprefix | pone el prefijo delante del mensaje en lugar de al principio de la línea |
Tres familias de funciones terminan el programa o provocan un panic, y la diferencia importa:
log.Fatal,log.Fatalfylog.Fatallnimprimen y luego llaman aos.Exit(1). Las llamadas diferidas no se ejecutan. Úsalas enmainpara fallos de arranque, nunca en código de librería ni en handlers de peticiones.log.Panicy sus variantes imprimen y luego provocan un panic, así que las llamadas diferidas se ejecutan y el panic se puede recuperar.- Todas las demás funciones solo escriben una línea.
Para registrar en un archivo, ábrelo y pásalo a log.New o log.SetOutput; io.MultiWriter(os.Stderr, f) escribe en los dos.
log y slog juntos
slog.SetDefault(logger) hace de logger el predeterminado para las funciones de nivel superior slog.Info, y además dirige la salida del paquete log a través de él. Las llamadas log.Printf que ya existan en tu código o en tus dependencias salen entonces como registros estructurados de nivel Info:
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
Antes de SetDefault, el logger de slog por defecto escribe a través del paquete log, y por eso un slog.Info("hi") a secas imprime 2026/09/23 14:30:00 INFO hi.
Reglas prácticas
- Registra o devuelve un error, no las dos cosas. Una función que registra un error y lo devuelve hace que el mismo fallo se registre en cada nivel de la pila de llamadas. Devuelve los errores hacia arriba con contexto, y regístralos una vez donde se manejan.
- Pon los datos variables en atributos, no en el mensaje.
logger.Info("user created", "user_id", id)se agrupa bien en un sistema de logs;logger.Info(fmt.Sprintf("user %d created", id))crea un mensaje distinto por cada usuario. - Nunca registres secretos ni bodies completos de peticiones. Usa
LogValueroReplaceAttrpara censurarlos. - JSON en producción, texto en desarrollo. Elige el handler al arrancar a partir de un flag o una variable de entorno.
- Elige los niveles a conciencia. Si todo se registra como Error, las alertas por errores se convierten en ruido.
Preguntas frecuentes
¿Qué es slog en Go?
log/slog es el paquete de logging estructurado que se añadió a la librería estándar en Go 1.21. En lugar de strings con formato, cada registro tiene un mensaje, un nivel (Debug, Info, Warn, Error) y atributos clave-valor, y un handler lo escribe como texto key=value o como JSON: slog.Info("login", "user", "ada", "attempts", 3).
¿Cómo activo los logs de depuración en slog?
El nivel mínimo por defecto es Info, así que slog.Debug no imprime nada. Crea un handler con un nivel más bajo y hazlo el predeterminado: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Usa un slog.LevelVar en lugar de una constante si quieres cambiar el nivel mientras el programa se ejecuta.
¿Qué diferencia hay entre log y slog en Go?
log escribe líneas de formato libre con un prefijo opcional de fecha y hora, y no tiene niveles. slog escribe registros con niveles y atributos clave-valor tipados que los recolectores de logs pueden parsear y filtrar. Los dos están en la librería estándar; slog.SetDefault además redirige la salida del paquete log a través del handler de slog.
¿log.Fatal ejecuta las funciones diferidas?
No. log.Fatal y log.Fatalf imprimen el mensaje y llaman a os.Exit(1), que se salta todas las llamadas diferidas. Úsalos solo en main o en código de arranque donde no haya nada que limpiar. log.Panic provoca un panic en su lugar, así que las llamadas diferidas sí se ejecutan.