La monitorización es un pilar fundamental en la operación de cualquier servicio en producción, y Prometheus se ha consolidado como una herramienta líder en la recolección de métricas. Este artículo detalla cómo integrar Prometheus en proyectos Golang para obtener visibilidad sobre el rendimiento y el estado de sus aplicaciones.
Adición de la Biblioteca Cliente de Prometheus
Para empezar, incorpore la biblioteca oficial de cliente de Prometheus para Go en su proyecto. Esto se realiza fácilmente utilizando go get:
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
Definición de Métricas
Prometheus ofrece varios tipos de métricas para diferentes escenarios de monitorización:
Counter: Un valor numérico que solo puede aumentar o reiniciarse a cero. Ideal para contar eventos, como el número total de solicitudes o errores.Gauge: Un valor numérico que puede subir y bajar libremente. Se utiliza para medir valores instantáneos, como el uso de memoria o el número de goroutines activas.Histogram: Registra la distribución de valores observados (por ejemplo, latencias de solicitudes) en cubos configurables. También proporciona sumas y recuentos de todos los valores.Summary: Similar a un Histogram, pero calcula cuantiles en el lado del cliente sobre una ventana de tiempo deslizante. Es útil para latencias y tamaños de respuesta.
Aquí definimos un contador simple para rastrear el número total de solicitudes procesadas por nuestra API:
var (
totalPeticionesProcesadas = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "api_web_peticiones_totales",
Help: "Número acumulado de solicitudes HTTP manejadas por el servicio web.",
},
)
)
Registro de Métricas
Las métricas deben ser registradas en un registro de Prometheus para que puedan ser descubiertas y raspadas. Es una práctica común registrar las métricas globales en la función init() de su paquete:
func init() {
prometheus.MustRegister(totalPeticionesProcesadas)
}
Exposición del Endpoint de Métricas
Prometheus necesita un endpoint HTTP desde donde recolectar los datos de las métricas. Convencionalmente, este endpoint es /metrics. El paquete promhttp proporciona un manejador HTTP conveniente para este propósito:
import (
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func iniciarServidorMeticas(puerto string) {
http.Handle("/metrics", promhttp.Handler())
err := http.ListenAndServe(":"+puerto, nil)
if err != nil {
// Considere usar un logger para registrar el error
// log.Printf("Error al iniciar el servidor de métricas en el puerto %s: %v", puerto, err)
}
}
Actualización de Valores de Métricas
Integre la lógica para actualizar las métricas directamente en sus funciones de negocio. Para un contador, esto implica llamar al método Inc():
import "net/http"
func manejadorPrincipal(w http.ResponseWriter, r *http.Request) {
totalPeticionesProcesadas.Inc() // Incrementa el contador cada vez que se llama este manejador
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("¡Operación exitosa!"))
}
Uso de Etiquetas para Dimensiones
Las etiquetas son poderosas para añadir dimensionalidad a sus métricas, permitiendo consultas y análisis más detallados. Por ejemplo, puede contar las solicitudes por método HTTP o por código de estado:
var (
peticionesPorMetodo = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "api_web_peticiones_por_metodo",
Help: "Solicitudes HTTP procesadas, desglosadas por método (GET, POST, etc.).",
},
[]string{"metodo_http"}, // Etiqueta para el método
)
)
func init() {
prometheus.MustRegister(totalPeticionesProcesadas, peticionesPorMetodo)
}
func manejadorConEtiquetas(w http.ResponseWriter, r *http.Request) {
// ... lógica de procesamiento ...
peticionesPorMetodo.WithLabelValues(r.Method).Inc()
// ...
}
Registros Personalizados (Avnazado)
Para escenarios donde necesite aislar métricas o evitar conflictos con bibliotecas de terceros que también exponen métricas, puede crear un registro de Prometheus personalizado en lugar de usar el registro predeterminado global:
func configurarRegistroAislado() {
registroPrivado := prometheus.NewRegistry()
// Definir y registrar una métrica específica en este registro
memoriaAplicacion := prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "app_uso_memoria_bytes",
Help: "Uso actual de memoria de la aplicación en bytes.",
},
)
registroPrivado.MustRegister(memoriaAplicacion)
// Exponer este registro específico en un endpoint diferente
http.Handle("/private-metrics", promhttp.HandlerFor(registroPrivado, promhttp.HandlerOpts{}))
// Nota: Deberías gestionar el ciclo de vida del servidor HTTP adecuadamente
}
Recomendaciones para Producción
- Intervalo de Raspado: Configure un intervalo de raspado (scrape interval) apropiado en Prometheus (15-30 segundos es un buen punto de partida) para balancear la granularidad de los datos y la carga del sistema.
- Prefijos de Nombres: Utilice prefijos distintivos para sus métricas (ej.
nombre_servicio_metrica) para mantener la claridad y evitar colisiones en un ecosistema de microservicios. - Monitorización de Sistema y Go: Además de las métricas de negocio, recolecte métricas del sistema operativo (CPU, memoria, disco) y del runtime de Go (goroutines, GC) para tener una vista completa del estado de la aplicación.
- Visualización con Grafana: Integre sus métricas de Prometheus con Grafana para crear paneles de control interactivos y alertas que faciliten el análisis y la resolución de problemas.
Ejemplo Completo de Aplicación Go con Prometheus
Este ejemplo integra varios tipos de métricas en una aplicación web Go simple, mostrando cómo definir, registrar y actualizar contadores, vectores de contadores e histogramas:
package main
import (
"fmt"
"net/http"
"strconv" // Para convertir int a string para etiquetas
"time" // Para simular un procesamiento
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Definición de métricas globales para nuestra aplicación
var (
// Contador para el total de solicitudes al endpoint principal
numSolicitudesAPI = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "go_api_solicitudes_totales",
Help: "Número total de solicitudes HTTP recibidas por la API.",
},
)
// Contador vectorizado para solicitudes por código de estado HTTP
solicitudesPorCodigo = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "go_api_solicitudes_por_estado_http",
Help: "Solicitudes a la API desglosadas por código de estado HTTP.",
},
[]string{"status_code"}, // Etiqueta para el código de estado
)
// Histograma para la duración de las solicitudes HTTP
latenciaSolicitudes = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "go_api_latencia_solicitudes_segundos",
Help: "Distribución de la latencia de las solicitudes HTTP en segundos.",
Buckets: prometheus.DefBuckets, // Cubos predeterminados de Prometheus
},
)
)
// La función init se ejecuta una vez al inicio del programa
func init() {
// Registra todas las métricas en el registro predeterminado de Prometheus
prometheus.MustRegister(numSolicitudesAPI)
prometheus.MustRegister(solicitudesPorCodigo)
prometheus.MustRegister(latenciaSolicitudes)
}
func main() {
// Manejador para el endpoint principal ("/")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Inicia un temporizador para medir la duración de la solicitud
timer := prometheus.NewTimer(latenciaSolicitudes)
defer timer.ObserveDuration() // Detiene el temporizador y observa la duración al finalizar
numSolicitudesAPI.Inc() // Incrementa el contador de solicitudes totales
// Simula algún trabajo que toma tiempo
time.Sleep(time.Duration(50+r.ContentLength%200) * time.Millisecond)
// Envía una respuesta al cliente
statusCode := http.StatusOK // Asumimos éxito por defecto
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(statusCode)
fmt.Fprintf(w, "¡Servicio Go en marcha! Ruta solicitada: %s", r.URL.Path)
// Incrementa el contador por código de estado
solicitudesPorCodigo.WithLabelValues(strconv.Itoa(statusCode)).Inc()
})
// Expone el endpoint de métricas de Prometheus en "/metrics"
http.Handle("/metrics", promhttp.Handler())
// Inicia el servidor HTTP en el puerto 8080
fmt.Println("Aplicación Go de ejemplo con Prometheus escuchando en :8080")
fmt.Println("Acceda a http://localhost:8080 para probar la API.")
fmt.Println("Métricas de Prometheus disponibles en http://localhost:8080/metrics")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Printf("Error fatal al iniciar el servidor HTTP: %s\n", err)
}
}