Saltar al contenido principal

Construyendo aplicaciones Ent observables con Prometheus

· 10 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

La observabilidad es una cualidad de un sistema que se refiere a qué tan bien se puede medir su estado interno desde el exterior. A medida que un programa informático evoluciona hacia un sistema de producción completo, esta cualidad se vuelve cada vez más importante. Una de las formas de hacer que un sistema de software sea más observable es exportar métricas, es decir, informar de alguna manera externamente visible una descripción cuantitativa del estado del sistema en funcionamiento. Por ejemplo, exponer un endpoint HTTP donde podamos ver cuántos errores han ocurrido desde que el proceso comenzó. En esta publicación, exploraremos cómo construir aplicaciones Ent más observables usando Prometheus.

¿Qué es Ent?

Ent es un marco de entidades simple pero poderoso para Go, que facilita la creación y mantenimiento de aplicaciones con modelos de datos complejos.

¿Qué es Prometheus?

Prometheus es un sistema de monitoreo de código abierto desarrollado por ingeniería en SoundCloud en 2012. Incluye una base de datos de series temporales integrada y muchas integraciones con sistemas de terceros. El cliente de Prometheus expone las métricas del proceso a través de un endpoint HTTP (generalmente /metrics). Este endpoint es descubierto por el scraper de Prometheus que lo consulta periódicamente (normalmente cada 30s) y escribe los datos en una base de datos de series temporales.

Prometheus es solo un ejemplo de una clase de backends de recolección de métricas. Existen muchos otros, como AWS CloudWatch, InfluxDB y otros, ampliamente utilizados en la industria. Hacia el final de esta publicación, discutiremos un posible camino para una integración unificada y basada en estándares con cualquier backend de este tipo.

Trabajando con Prometheus

Para exponer las métricas de una aplicación usando Prometheus, necesitamos crear un Collector de Prometheus. Un collector recopila un conjunto de métricas desde tu servidor.

En nuestro ejemplo, usaremos dos tipos de métricas que pueden almacenarse en un collector: Contadores e Histogramas. Los contadores son métricas acumulativas monótonamente crecientes que representan cuántas veces ha ocurrido algo, comúnmente usados para contar el número de solicitudes que un servidor ha procesado o errores que han ocurrido. Los histogramas agrupan observaciones en cubos de tamaños configurables y se usan comúnmente para representar distribuciones de latencia (ej. cuántas solicitudes respondieron en menos de 5ms, 10ms, 100ms, 1s, etc.). Además, Prometheus permite desglosar métricas mediante etiquetas. Esto es útil, por ejemplo, para contar solicitudes desglosando el contador por nombre del endpoint.

Veamos cómo crear tal collector usando el cliente oficial de Go. Para ello, usaremos un paquete del cliente llamado promauto que simplifica el proceso de creación de collectors. Un ejemplo simple de un collector que cuenta (por ejemplo, solicitudes totales o número de errores en solicitudes):

package example

import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
// List of dynamic labels
labelNames = []string{"endpoint", "error_code"}

// Create a counter collector
exampleCollector = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "endpoint_errors",
Help: "Number of errors in endpoints",
},
labelNames,
)
)

// When using you set the values of the dynamic labels and then increment the counter
func incrementError() {
exampleCollector.WithLabelValues("/create-user", "400").Inc()
}

Hooks de Ent

Los Hooks son una característica de Ent que permite agregar lógica personalizada antes y después de operaciones que modifican las entidades de datos.

Una mutación es una operación que cambia algo en la base de datos. Existen 5 tipos de mutaciones:

  1. Create (Crear).

  2. UpdateOne (Actualizar uno).

  3. Update (Actualizar).

  4. DeleteOne (Eliminar uno).

  5. Delete (Eliminar).

Los hooks son funciones que reciben un ent.Mutator y devuelven un mutador. Funcionan de manera similar al popular patrón de middleware HTTP.

package example

import (
"context"

"entgo.io/ent"
)

func exampleHook() ent.Hook {
//use this to init your hook
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Do something before mutation.
v, err := next.Mutate(ctx, m)
if err != nil {
// Do something if error after mutation.
}
// Do something after mutation.
return v, err
})
}
}

En Ent, existen dos tipos de hooks para mutaciones: hooks de esquema y hooks en tiempo de ejecución. Los hooks de esquema se utilizan principalmente para definir lógica personalizada de mutación en un tipo de entidad específico, por ejemplo, sincronizar la creación de una entidad con otro sistema. Los hooks en tiempo de ejecución, por otro lado, se emplean para definir lógica más global que añade funcionalidades como registro de eventos, métricas, trazabilidad, etc.

Para nuestro caso de uso, definitivamente deberíamos utilizar hooks en tiempo de ejecución, porque para que sea valioso necesitamos exportar métricas sobre todas las operaciones en todos los tipos de entidades:

package example

import (
"entprom/ent"
"entprom/ent/hook"
)

func main() {
client, _ := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")

// Add a hook only on user mutations.
client.User.Use(exampleHook())

// Add a hook only on update operations.
client.Use(hook.On(exampleHook(), ent.OpUpdate|ent.OpUpdateOne))
}

Exportando métricas de Prometheus para una aplicación Ent

Con todas las introducciones completadas, vayamos al grano y mostremos cómo usar Prometheus junto con hooks de Ent para crear una aplicación observable. Nuestro objetivo con este ejemplo es exportar estas métricas mediante un hook:

Metric NameDescription
ent_operation_totalNumber of ent mutation operations
ent_operation_errorNumber of failed ent mutation operations
ent_operation_duration_secondsTime in seconds per operation

Cada una de estas métricas se desglosará mediante etiquetas en dos dimensiones:

  • mutation_type: Tipo de entidad que se está mutando (User, BlogPost, Account, etc.).

  • mutation_op: Operación que se está realizando (Create, Delete, etc.).

Comencemos definiendo nuestros recolectores:

//Ent dynamic dimensions
const (
mutationType = "mutation_type"
mutationOp = "mutation_op"
)

var entLabels = []string{mutationType, mutationOp}

// Create a collector for total operations counter
func initOpsProcessedTotal() *prometheus.CounterVec {
return promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ent_operation_total",
Help: "Number of ent mutation operations",
},
entLabels,
)
}

// Create a collector for error counter
func initOpsProcessedError() *prometheus.CounterVec {
return promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "ent_operation_error",
Help: "Number of failed ent mutation operations",
},
entLabels,
)
}

// Create a collector for duration histogram collector
func initOpsDuration() *prometheus.HistogramVec {
return promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "ent_operation_duration_seconds",
Help: "Time in seconds per operation",
},
entLabels,
)
}

A continuación, definamos nuestro nuevo hook:

// Hook init collectors, count total at beginning error on mutation error and duration also after.
func Hook() ent.Hook {
opsProcessedTotal := initOpsProcessedTotal()
opsProcessedError := initOpsProcessedError()
opsDuration := initOpsDuration()
return func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
// Before mutation, start measuring time.
start := time.Now()
// Extract dynamic labels from mutation.
labels := prometheus.Labels{mutationType: m.Type(), mutationOp: m.Op().String()}
// Increment total ops counter.
opsProcessedTotal.With(labels).Inc()
// Execute mutation.
v, err := next.Mutate(ctx, m)
if err != nil {
// In case of error increment error counter.
opsProcessedError.With(labels).Inc()
}
// Stop time measure.
duration := time.Since(start)
// Record duration in seconds.
opsDuration.With(labels).Observe(duration.Seconds())
return v, err
})
}
}

Conectando el recolector de Prometheus a nuestro servicio

Después de definir nuestro hook, veamos cómo conectarlo a nuestra aplicación y cómo usar Prometheus para servir un endpoint que exponga las métricas de nuestros recolectores:

package main

import (
"context"
"log"
"net/http"

"entprom"
"entprom/ent"

_ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

func createClient() *ent.Client {
c, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
ctx := context.Background()
// Run the auto migration tool.
if err := c.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
return c
}

func handler(client *ent.Client) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
// Run operations.
_, err := client.User.Create().SetName("a8m").Save(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
}

func main() {
// Create Ent client and migrate
client := createClient()
// Use the hook
client.Use(entprom.Hook())
// Simple handler to run actions on our DB.
http.HandleFunc("/", handler(client))
// This endpoint sends metrics to the prometheus to collect
http.Handle("/metrics", promhttp.Handler())
log.Println("server starting on port 8080")
// Run the server
log.Fatal(http.ListenAndServe(":8080", nil))
}

Tras acceder varias veces a / en nuestro servidor (usando curl o un navegador), visita /metrics. Allí verás la salida del cliente de Prometheus:

# HELP ent_operation_duration_seconds Time in seconds per operation
# TYPE ent_operation_duration_seconds histogram
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.005"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.01"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.025"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.05"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.1"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.25"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="0.5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="1"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="2.5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="5"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="10"} 2
ent_operation_duration_seconds_bucket{mutation_op="OpCreate",mutation_type="User",le="+Inf"} 2
ent_operation_duration_seconds_sum{mutation_op="OpCreate",mutation_type="User"} 0.000265669
ent_operation_duration_seconds_count{mutation_op="OpCreate",mutation_type="User"} 2
# HELP ent_operation_error Number of failed ent mutation operations
# TYPE ent_operation_error counter
ent_operation_error{mutation_op="OpCreate",mutation_type="User"} 1
# HELP ent_operation_total Number of ent mutation operations
# TYPE ent_operation_total counter
ent_operation_total{mutation_op="OpCreate",mutation_type="User"} 2

En la parte superior, podemos ver el histograma calculado, que cuenta el número de operaciones en cada "cubo". Después, vemos el número total de operaciones y la cantidad de errores. Cada métrica incluye su descripción, visible al consultar con el panel de Prometheus.

El cliente de Prometheus es solo un componente de su arquitectura. Para ejecutar un sistema completo que incluya un recolector que sondee tu endpoint, un Prometheus que almacene tus métricas y pueda responder consultas, y una interfaz simple para interactuar, recomiendo leer la documentación oficial o usar el docker-compose.yaml en este repositorio de ejemplo.

Trabajo futuro en observabilidad para Ent

Como mencionamos antes, existe una abundancia de backends para recolección de métricas disponibles hoy, siendo Prometheus solo uno de muchos proyectos exitosos. Aunque estas soluciones difieren en múltiples dimensiones (autohospedadas vs SaaS, diferentes motores de almacenamiento con lenguajes de consulta distintos, etc.), desde la perspectiva del cliente que reporta métricas son virtualmente idénticas.

En casos como este, los principios de buena ingeniería de software sugieren que el backend concreto debería abstraerse del cliente mediante una interfaz. Esta interfaz podría ser implementada por diferentes backends, permitiendo a las aplicaciones cliente cambiar fácilmente entre implementaciones. Cambios similares están ocurriendo en nuestra industria en años recientes. Consideremos, por ejemplo, la Open Container Initiative o la Service Mesh Interface: ambas son iniciativas que buscan definir una interfaz estándar para un espacio problemático. En el ámbito de la observabilidad, está ocurriendo exactamente la misma convergencia con OpenCensus y OpenTracing fusionándose actualmente en OpenTelemetry.

Por muy atractivo que sea publicar una extensión de Ent + Prometheus similar a la presentada en este post, somos firmes creyentes de que la observabilidad debe resolverse con un enfoque basado en estándares. Invitamos a todos a unirse a la discusión sobre cuál es la forma correcta de implementar esto para Ent.

Conclusión

Comenzamos esta publicación presentando Prometheus, una popular solución de monitoreo de código abierto. Luego revisamos los "Hooks", una característica de Ent que permite agregar lógica personalizada antes y después de operaciones que modifican entidades de datos. Mostramos cómo integrar ambos para crear aplicaciones observables con Ent. Finalmente, discutimos el futuro de la observabilidad en Ent e invitamos a todos a unirse a la discusión para darle forma.

¿Tienes preguntas? ¿Necesitas ayuda para empezar? Únete a nuestro servidor de Discord o canal de Slack.

[Para más noticias y actualizaciones de Ent:]