Registros estructurados en Go con slog para observabilidad y alertas

Registros JSON consultables que se conectan a trazas.

Índice

Los registros son una interfaz de depuración que aún puedes usar cuando el sistema está en llamas. El problema es que los registros en texto plano envejecen mal: en cuanto necesitas filtrado, agregación y alertas, comienzas a analizar oraciones.

Espacio de trabajo con portátil y mascotas de Go para un mejor registro

La registro estructurada es el antídoto. Convierte cada línea de registro en un pequeño evento con campos estables, para que las herramientas puedan buscar y agregar de manera confiable. Para saber cómo se conectan los registros con las métricas, paneles y alertas en el conjunto más amplio, consulta la Guía de Observabilidad: Monitorización, Métricas, Prometheus y Grafana.

Qué es el registro estructurado y por qué escala

El registro estructurado es un registro donde un registro no es solo una cadena, sino un mensaje más atributos clave-valor tipados. La idea es aburrida de la mejor manera: una vez que los registros son legibles por máquina, un incidente deja de ser una competencia de grep.

Una comparación rápida:

Texto plano (prioridad humano, hostil a herramientas)

failed to charge card user=42 amount=19.99 ms=842 err=timeout

Estructurado (prioridad herramienta, aún legible)

{"msg":"failed to charge card","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}

En producción, ayuda pensar en los registros como un flujo de eventos emitido por el proceso, mientras que el enrutamiento y el almacenamiento viven fuera de la aplicación. Ese modelo mental te impulsa a escribir un evento por línea y mantener los eventos fáciles de enviar y reprocesar.

Slog en Go como interfaz de registro compartida

Go ha tenido el paquete clásico log desde siempre, pero los servicios modernos necesitan niveles y campos. El paquete log/slog (Go 1.21 y posteriores) introduce el registro estructurado en la biblioteca estándar y formaliza una forma común para los registros: hora, nivel, mensaje y atributos. Para un repaso compacto del lenguaje y los comandos junto con esta guía, consulta la Hoja de trucos de Go.

Las partes clave del modelo son:

Registro

Un registro es lo que sucedió. En términos de slog, contiene hora, nivel, mensaje y un conjunto de atributos. Creas registros mediante métodos como Info y Error, o mediante Log cuando deseas suministrar el nivel explícitamente.

Atributos

Los atributos son los pares clave-valor que hacen que los registros sean consultables. Si registras el mismo concepto bajo tres claves diferentes (user, userId, uid), obtienes tres conjuntos de datos diferentes. Las claves consistentes es donde se esconde el verdadero valor.

Manejador

Un manejador es cómo los registros se convierten en bytes. El TextHandler incorporado escribe salida key=value, mientras que JSONHandler escribe JSON delimitado por líneas. Los manejadores también son donde suele ocurrir la redacción, el renombrado de claves y el enrutamiento de salida.

Una característica poco valorada es que slog puede situarse frente a código existente. Cuando configuras un registrador slog predeterminado, las funciones de nivel superior de slog lo usan, y el paquete clásico log también puede redirigirse a él. Eso hace posible la migración incremental.

Grupos

Los grupos resuelven el problema de “cada subsistema usa id”. Puedes agrupar un conjunto de atributos para una solicitud (request.method, request.path) o crear un espacio de nombres para un subsistema completo con WithGroup para que las claves no colisionen.

Una configuración de slog orientada a producción

La siguiente configuración cumple los objetivos habituales. Los ejemplos usan un pequeño paquete logx; para saber dónde suelen vivir paquetes como ese en un módulo real, consulta Estructura de Proyectos en Go: Prácticas y Patrones.

  • un evento JSON por línea
  • registros escritos en stdout para su recolección
  • metadatos estables del servicio adjuntos una vez
  • registro consciente del contexto para ID de solicitud y trazas
  • redacción centralizada para claves sensibles
package logx

import (
	"log/slog"
	"os"
)

var level slog.LevelVar // predeterminado a INFO

func New() *slog.Logger {
	opts := &slog.HandlerOptions{
		Level:     &level, // puede cambiarse en tiempo de ejecución
		AddSource: true,   // incluir archivo y línea cuando esté disponible
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			// Redacción centralizada: consistente y difícil de eludir por accidente.
			switch a.Key {
			case "password", "token", "authorization", "api_key":
				return slog.String(a.Key, "[redacted]")
			}
			return a
		},
	}

	h := slog.NewJSONHandler(os.Stdout, opts)

	return slog.New(h).With(
		"service", os.Getenv("SERVICE_NAME"),
		"env", os.Getenv("ENV"),
		"version", os.Getenv("VERSION"),
	)
}

func SetLevel(l slog.Level) { level.Set(l) }

Un detalle pequeño con grandes consecuencias: el manejador JSON incorporado usa claves estándar (time, level, msg, source). Cuando tu backend de registros espera un esquema diferente, ReplaceAttr es la válvula de alivio que te permite normalizar claves sin reescribir los sitios de llamada.

El esquema importa más que el registrador

La mayoría de los fallos de “registro estructurado” son fallos de esquema.

Campos esenciales que siguen pagando el alquiler

Cada backend de registros almacenará una marca de tiempo, nivel y mensaje. En la práctica, un esquema de aplicación útil a menudo añade un pequeño conjunto de campos estables:

  • service, env, version
  • component (o subsistema)
  • event (un nombre estable para lo que sucedió)
  • request_id (cuando existe una solicitud)
  • trace_id y span_id (cuando existe trazabilidad)
  • error (cadena) y error_kind (cubo estable)

Observa el patrón: estos campos responden preguntas operativas, no la curiosidad del desarrollador.

Las convenciones semánticas son un truco de consistencia barato

Si ya usas OpenTelemetry, sus convenciones semánticas proporcionan un vocabulario estándar para atributos en todas las señales de telemetría. Incluso si no exportas registros a través de OpenTelemetry, tomar prestados nombres de atributos reduce el impuesto de “¿cómo llamamos a este campo en el servicio B?”.

Alta cardinalidad y por qué los registros se vuelven costosos

Alta cardinalidad significa “demasiados valores únicos”. Está bien dentro de una carga JSON, pero se vuelve doloroso cuando un backend trata algunos campos como etiquetas indexadas o claves de flujo. Los IDs de usuario, direcciones IP, tokens de solicitud aleatorios y URLs completas tienden a explotar combinaciones.

El resultado práctico es simple: mantén las etiquetas y claves de índice aburridas (servicio, entorno, región) y mantén los campos de alta cardinalidad dentro de la carga estructurada para filtrado en tiempo de consulta.

Correlación con IDs de solicitud y trazas

La correlación es el punto donde los registros dejan de ser solo texto y comienzan a comportarse como telemetría.

El ID de solicitud como la clave de correlación de menor fricción

Un ID de solicitud es el puente más simple entre una solicitud entrante y todo lo que sucede a causa de ella. Suele funcionar incluso sin trazabilidad distribuida, y sigue siendo útil cuando las trazas se muestrean. Para ver la imagen completa de cómo deben almacenarse y recuperarse los IDs de solicitud y otros metadatos en y desde el contexto, incluidas patrones de claves tipadas y ejemplos de middleware, consulta Go context.Context Bien Hecho.

Un patrón común es adjuntar un registrador por solicitud al contexto:

package logx

import (
	"context"
	"log/slog"
)

type ctxKey struct{}

func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
	return context.WithValue(ctx, ctxKey{}, l)
}

func FromContext(ctx context.Context) *slog.Logger {
	if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok && l != nil {
		return l
	}
	return slog.Default()
}

Correlación de trazas con W3C Trace Context y OpenTelemetry

W3C Trace Context define una forma estándar de propagar la identidad de la traza (para HTTP, vía traceparent y tracestate). OpenTelemetry se basa en eso para que los IDs de traza y los IDs de span puedan extraerse del contexto.

Este ejemplo de middleware registra tanto request_id como identificadores de traza cuando están disponibles:

package middleware

import (
	"crypto/rand"
	"encoding/hex"
	"net/http"

	"go.opentelemetry.io/otel/trace"
	"log/slog"

	"example.com/project/logx"
)

func requestID() string {
	var b [16]byte
	_, _ = rand.Read(b[:])
	return hex.EncodeToString(b[:])
}

func WithRequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			rid := r.Header.Get("X-Request-Id")
			if rid == "" {
				rid = requestID()
			}

			l := base.With(
				"request_id", rid,
				"method", r.Method,
				"path", r.URL.Path,
			)

			if sc := trace.SpanContextFromContext(r.Context()); sc.IsValid() {
				l = l.With(
					"trace_id", sc.TraceID().String(),
					"span_id", sc.SpanID().String(),
				)
			}

			ctx := logx.WithLogger(r.Context(), l)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

Una vez que existen los campos de correlación, la línea de registro se convierte en un índice hacia otros datos. La diferencia en un incidente en vivo no es sutil.

Convertir registros estructurados en señales de monitorización y alertas

Los registros son excelentes para responder “qué sucedió”. Las alertas suelen ser sobre “con qué frecuencia y qué tan grave”.

Un enfoque práctico es tratar ciertos eventos de registro como contadores:

  • event=payment_failed
  • event=db_timeout
  • event=cache_miss

Muchas plataformas pueden derivar métricas basadas en registros contando registros coincidentes en una ventana. Los registros estructurados hacen que ese conteo sea resistente, porque se basa en un valor de campo en lugar de una coincidencia de texto frágil. Cuando estés listo para visualizar y explorar esas señales, Instalar y Usar Grafana en Ubuntu: Guía Completa describe una configuración completa de Grafana que puedes dirigir a backends comunes de registros y métricas.

Este es también donde los niveles de registro comienzan a importar. Los registros de depuración a menudo son valiosos, pero también es donde se esconden el costo y el ruido. Usar un nivel dinámico (LevelVar) permite que el sistema se mantenga silencioso por defecto, mientras aún permite detalles dirigidos cuando sea necesario.

Pensamientos finales

El registro estructurado en Go ya no es un debate de bibliotecas. La parte interesante es si tus registros son consistentes, correlacionables y asequibles de almacenar.

Cuando tus registros llevan campos estables como event, request_id y trace_id, dejan de ser “cadenas que alguien escribió” y comienzan a ser un conjunto de datos con el que puedes operar.

Notas

El equipo de Go introdujo log/slog en Go 1.21 y enfatizó que los registros estructurados usan pares clave-valor para que puedan analizarse, filtrarse, buscarse y analizarse de manera confiable, y también señaló la motivación de proporcionar un marco común compartido en todo el ecosistema.

La documentación del paquete log/slog define el modelo de registro (hora, nivel, mensaje, pares clave-valor) y los manejadores incorporados (TextHandler para key=value y JSONHandler para JSON delimitado por líneas), y documenta la integración SetDefault con el paquete clásico log.

Para la correlación distribuida, la especificación W3C Trace Context estandariza la propagación de traceparent y tracestate, y OpenTelemetry especifica que su SpanContext cumple con W3C Trace Context y expone TraceId y SpanId, haciendo que la correlación registro-traza sea straightforward cuando hay un span presente.

Para el costo y rendimiento del almacenamiento de registros, la documentación de Grafana Loki recomienda fuertemente etiquetas estáticas y acotadas y advierte sobre las etiquetas de alta cardinalidad creando demasiadas corrientes y un índice enorme, lo cual es directamente relevante al decidir qué se convierte en una etiqueta frente a qué permanece como un campo JSON no indexado.

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.