Implementación de Métricas y Logging con Prometheus para Microservicios en Go
En el ecosistema de los microservicios de alto rendimiento, la visibilidad es sinónimo de control. Desplegar un servicio sin un sistema robusto de métricas y logging es como pilotar un avión a ciegas: puedes estar volando, pero no tienes ni idea de la altitud, la velocidad o si los motores están a punto de fallar. Esta lección se adentra en la integración de Prometheus, el estándar de facto para el monitoreo en entornos nativos de la nube, dentro de nuestros microservicios construidos con Go y Gorilla/Mux. No solo aprenderás a exponer números, sino a instrumentar tu aplicación para que revele su estado interno, su salud y su comportamiento, transformando datos crudos en conocimiento accionable para mantener APIs resilientes y performantes.
Prometheus opera bajo un modelo de extracción (pull), donde un servidor central "raspa" (scrape) endpoints HTTP específicos de tus servicios para recolectar métricas. Esto contrasta con el modelo de envío (push) y es fundamental para su diseño escalable y confiable. Nuestro objetivo será exponer un endpoint, típicamente /metrics, que sirva un formato de texto plano que Prometheus pueda interpretar. Utilizaremos la biblioteca oficial client_golang para Go, que nos proporciona los instrumentos (contadores, gauges, histogramas) para crear y exponer estas métricas de manera eficiente y segura para concurrencia.
Concepto Clave: El Modelo de Extracción y las 4 Señales de Oro
Imagina que Prometheus es un inspector de salud que realiza visitas programadas a tu fábrica (el microservicio). En cada visita, el inspector no espera que le cuentes cómo estuvo el día; en su lugar, camina por la fábrica, lee los paneles de control (los exportadores de métricas) y anota los valores actuales de producción, consumo de energía y niveles de inventario. Este es el modelo de extracción. La fábrica simplemente mantiene los paneles actualizados y accesibles. La ventaja es monumental: si la fábrica se cae, Prometheus lo sabrá inmediatamente (la extracción fallará), y el servidor central no se verá abrumado por miles de fábricas tratando de enviar datos simultáneamente durante una caída.
Las métricas que exponemos no deben ser aleatorias. Nos guiamos por las Cuatro Señales de Oro del Monitoreo: Latencia (cuánto tarda en responder una solicitud), Tráfico (cuánta demanda tiene, ej. peticiones por segundo), Errores (la tasa de respuestas fallidas) y Saturación (cuán "lleno" está el servicio, como el uso de memoria o gorutinas). Instrumentar para estas señales nos da una visión completa. Por ejemplo, un alto tráfico con baja latencia y cero errores es una buena señal; un tráfico moderado con latencia creciente y alta saturación indica que el servicio está cerca de su límite y necesita escalar.
Tip del Experto: No te limites a medir lo fácil, mide lo importante. Una métrica como `http_requests_total` es un buen comienzo, pero `http_request_duration_seconds` (un histograma) que te permita calcular el percentil 95 o 99 de la latencia es lo que realmente te alertará sobre problemas de rendimiento que afectan a tus usuarios más lentos antes de que se conviertan en una crisis.
Cómo Funciona en la Práctica: Instrumentando un Handler HTTP
El proceso comienza con la creación de un Registro (Registry) de Prometheus. Este registro es el contenedor central donde se registran todas tus métricas. Luego, defines y registras las métricas específicas. Para un microservicio REST, las más críticas son: un Counter para contar solicitudes totales (y por código de estado), un Histogram para medir la duración de las solicitudes, y quizás un Gauge para monitorear el número de gorutinas activas o el tamaño de una cola interna. Estas métricas se definen una vez al inicio de la aplicación.
La magia de la instrumentación ocurre en el nivel del middleware. Crearemos un middleware para Gorilla/Mux que envuelva cada handler. Este middleware capturará el momento de inicio de la solicitud, ejecutará el handler real, y luego, tras la ejecución, registrará la métrica de duración (observando el histograma) e incrementará el contador de solicitudes totales y el contador específico para el código de estado HTTP devuelto. Este patrón garantiza que todas las rutas queden automáticamente instrumentadas sin modificar su lógica de negocio. Finalmente, necesitamos que el router sirva el endpoint /metrics para que Prometheus pueda extraer los datos. El handler para este endpoint lo proporciona la biblioteca de Prometheus y sabe cómo leer el registro y formatear los datos correctamente.
Código en Acción: Middleware de Métricas y Endpoint de Salud
A continuación, un ejemplo completo y funcional de un microservicio con un endpoint de producto, instrumentado con métricas básicas pero esenciales, y un endpoint de salud extendido que incluye checks personalizados.
// main.go
package main
import (
"log"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Definición de métricas como variables globales.
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Número total de solicitudes HTTP.",
},
[]string{"method", "path", "status_code"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duración de las solicitudes HTTP en segundos.",
Buckets: prometheus.DefBuckets, // Buckets predefinidos para percentiles.
},
[]string{"method", "path"},
)
activeGoroutines = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "go_goroutines_active",
Help: "Número de gorutinas activas.",
},
)
)
// Middleware de métricas para Gorilla Mux.
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
// Usamos un ResponseWriter wrapper para capturar el status code.
rw := &responseWriter{w, http.StatusOK}
next.ServeHTTP(rw, r)
duration := time.Since(start).Seconds()
statusCode := http.StatusText(rw.statusCode)
// Registramos las métricas.
httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
httpRequestsTotal.WithLabelValues(r.Method, path, statusCode).Inc()
activeGoroutines.Set(float64(runtime.NumGoroutine())) // Actualizamos gauge
})
}
// Wrapper para capturar el status code.
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Handler de ejemplo.
func productHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Simulación de lógica de negocio y posible error.
if id == "0" {
http.Error(w, "producto no encontrado", http.StatusNotFound)
return
}
time.Sleep(10 * time.Millisecond) // Simula trabajo.
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id": "` + id + `", "name": "Producto Ejemplo"}`))
}
// Health Check con métrica de estado.
func healthHandler(w http.ResponseWriter, r *http.Request) {
// Lógica de verificación de salud (ej., conexión a BD, estado de caché).
isHealthy := true // Supongamos que todo está bien.
if isHealthy {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "healthy"}`))
} else {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status": "unhealthy"}`))
}
}
func main() {
r := mux.NewRouter()
// Ruta para métricas de Prometheus.
r.Handle("/metrics", promhttp.Handler()).Methods("GET")
// Ruta para health check.
r.HandleFunc("/health", healthHandler).Methods("GET")
// Router de API con middleware de métricas aplicado.
apiRouter := r.PathPrefix("/api/v1").Subrouter()
apiRouter.Use(metricsMiddleware)
apiRouter.HandleFunc("/products/{id:[0-9]+}", productHandler).Methods("GET")
log.Println("Servidor iniciado en :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
Este código establece un servicio con tres endpoints clave: /api/v1/products/{id} (nuestro negocio), /health (para orquestadores como Kubernetes) y /metrics (para Prometheus). El middleware captura automáticamente método, ruta, código de estado y duración para cada solicitud a la API, etiquetando las métricas de manera que puedan ser agregadas y consultadas con gran granularidad en Prometheus (por ejemplo, tasa de error solo para POST a una ruta específica).
Errores Comunes y Cómo Evitarlos
1. Cardinalidad Explosiva de Etiquetas (Labels): El error más grave. Usar etiquetas con valores de alta cardinalidad, como IDs de usuario, direcciones IP o marcas de tiempo completas, hace que Prometheus cree una nueva serie de tiempo por cada valor único, inutilizando la memoria del servidor. Solución: Usa etiquetas solo para dimensiones acotadas (método HTTP, ruta, código de estado, nombre del servicio). Nunca uses datos de usuario directamente como etiqueta.
2. No Instrumentar los Errores de Negocio: Muchos desarrolladores solo capturan errores HTTP 5xx, pero los errores de lógica (ej., "saldo insuficiente", HTTP 400) también son cruciales. Solución: Crea un contador específico para errores de dominio, por ejemplo, `business_errors_total` con una etiqueta `error_type`. Increméntalo en los puntos relevantes de tu código.
3. Olvidar el Endpoint /metrics o Protegerlo Incorrectamente: Exponer el endpoint sin restricciones puede ser un riesgo de seguridad (fuga de información interna) o saturarlo con solicitudes. Solución: Usa middleware de autenticación básica o permite el acceso solo desde las IPs de tu red de monitoreo. En Kubernetes, usa anotaciones `prometheus.io/scrape: "true"` para un descubrimiento automático y seguro.
4. Medir Solo lo Fácil, No lo Importante: Limitarse a `http_requests_total` da una visión muy superficial. Solución: Implementa siempre histogramas para latencia (`http_request_duration_seconds`) y gauges para la saturación del servicio (uso de CPU/memoria, longitud de colas internas, conexiones de base de datos activas).
5. Bloquear el Handler /metrics con Código Lento: Si la lógica para calcular una métrica personalizada es muy costosa (ej., consultar una BD), puedes ralentizar o bloquear las extracciones de Prometheus. Solución: Calcula las métricas de forma asíncrona en segundo plano (usando gorutinas y canales) y deja que el gauge solo exponga el último valor calculado. El handler de Prometheus debe ser de solo lectura y ultra rápido.
Checklist de Dominio
- He creado y registrado correctamente contadores (Counter), gauges (Gauge) e histogramas (Histogram) usando el paquete `promauto` para simplicidad.
- He implementado un middleware que envuelve mis handlers HTTP para capturar automáticamente método, ruta, código de estado y duración de la solicitud.
- He expuesto el endpoint
/metricsusando `promhttp.Handler()` y he verificado que devuelve datos en el formato de texto plano de Prometheus. - He definido etiquetas (labels) para mis métricas que tienen cardinalidad baja y controlada (ej., código de estado, nombre de ruta, método HTTP).
- He incluido métricas para las Cuatro Señales de Oro: Tráfico (requests total), Latencia (histograma de duración), Errores (contador por status 4xx/5xx) y Saturación (gauge de gorutinas o memoria).
- He configurado mi servidor Prometheus para descubrir y extraer (scrape) el endpoint
/metricsde mi servicio Go, verificando que aparezca como un target "UP". - He creado al menos una alerta básica en Prometheus basada en mis métricas (ej., tasa de error > 5% durante 2 minutos).
- He integrado el logging estructurado (usando, por ejemplo, log/slog o zerolog) de forma complementaria, registrando trazas (logs) para eventos excepcionales, mientras las métricas monitorizan el comportamiento agregado.