Observabilidad en Go: Elegir entre QueryTracer, métricas y OpenTelemetry
Al integrar PostgreSQL en aplicaciones Go, los desarrolladores a menudo se enfrentan a una elección de herramientas de observabilidad. Las métricas, QueryTracer y OpenTelemetry resuelven problemas diferentes, pero es fácil confundirlos. Vamos a desglosar cuándo usar cada una y cómo evitar errores comunes, basado en un caso real que involucra un decorador y transacciones.
Tres niveles de observabilidad: métricas, trazado y contexto
Las métricas son el nivel básico de observabilidad. Responden a la pregunta «¿qué está pasando?»: por ejemplo, un pico de errores tras un despliegue o el tiempo de respuesta p99 de un endpoint que salta de 50 ms a 800 ms. Recopilar métricas es barato en términos de recursos y permite detectar anomalías rápidamente. Sin embargo, las métricas carecen de contexto: muestran que una consulta se ralentizó, pero no explican por qué. Para eso se necesita el trazado.
QueryTracer es una interfaz proporcionada por el driver pgx para interceptar operaciones a nivel del driver. Genera eventos separados para cada etapa de la transacción: BEGIN, la consulta en sí y COMMIT. No es una herramienta de recopilación de métricas, sino un gancho donde puedes insertar lógica para crear spans, registrar SQL o recopilar métricas. Importante: QueryTracer en sí no envía datos, solo proporciona un mecanismo para generarlos.
OpenTelemetry (OTel) se encarga del trazado distribuido. Se crea un span a nivel del manejador de solicitudes HTTP y se pasa a través del contexto (context.Context) hacia abajo en la pila: servicio, repositorio, driver. Cada capa añade un span hijo, formando un árbol de ejecución. En un sistema de visualización (p. ej., Jaeger), ves el panorama completo: qué consulta específica de la BD fue lenta y en qué contexto. OTel se ejecuta de forma continua, pero usa muestreo para reducir la sobrecarga: por ejemplo, solo se rastrea el 10 % de las solicitudes. Si una solicitud se muestrea, se registra el árbol completo de spans.
Ejemplo de decorador: dónde se esconde el problema
En proyectos típicos, se usa un decorador sobre el repositorio para la recopilación de métricas. Aquí hay un ejemplo de implementación:
func WithDBMetricsValueT any (T, error)) (T, error) {
start := time.Now()
result, err := fn()
seconds := time.Since(start).Seconds()
metrics.ObserveDatabaseQueryDuration(operation, seconds)
if err != nil {
metrics.IncDatabaseErrors(operation)
return result, CheckDBError(operation, err)
}
return result, err
}
Para métodos que ejecutan una sola consulta SQL, el decorador funciona con precisión: la métrica coincide con el tiempo de ejecución de la consulta. Sin embargo, en métodos con transacciones, las cosas cambian. Considera este ejemplo:
func (r *BookingRepo) CreateBooking(ctx context.Context, b *models.Booking) (int64, error) {
const operation = "create_booking"
return WithDBMetricsValue(operation, func() (int64, error) {
if err := r.validateBooking(b); err != nil {
return 0, err
}
tx, err := r.db.BeginTxx(ctx, nil)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback() }()
var id int64
err = tx.QueryRowContext(ctx, createBookingAtomicQuery, ...).Scan(&id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, models.ErrSlotOccupied
}
return 0, err
}
if err = tx.Commit(); err != nil {
return 0, err
}
return id, nil
})
}
El decorador mide el tiempo de todo el bloque, incluyendo validación, inicio de transacción, ejecución de la consulta y commit. La métrica create_booking = 45 ms no revela qué etapa consume 40 ms. Mientras tanto, QueryTracer desglosa la transacción en tres eventos separados:
- BEGIN = 0,1 ms
- INSERT = 40 ms
- COMMIT = 2 ms
Esta granularidad es crucial para diagnosticar cuellos de botella de rendimiento.
Implementación de QueryTracer: interfaces y errores comunes
QueryTracer consta de interfaces en pgx, cada una para un tipo específico de operación. Por ejemplo, BatchTracer se usa para operaciones Batch:
type BatchTracer interface {
TraceBatchStart(ctx context.Context, conn *Conn, data TraceBatchStartData) context.Context
TraceBatchQuery(ctx context.Context, conn *Conn, data TraceBatchQueryData)
TraceBatchEnd(ctx context.Context, conn *Conn, data TraceBatchEndData)
}
La implementación requiere crear una struct que satisfaga estas interfaces:
type MyTracer struct{}
func (t *MyTracer) TraceQueryStart(ctx context.Context, conn *Conn, data TraceQueryStartData) context.Context {
// Inicio de consulta: crear un span o registrar
return ctx
}
func (t *MyTracer) TraceQueryEnd(ctx context.Context, conn *Conn, data TraceQueryEndData) {
// Fin de consulta: registrar tiempo, enviar métrica
}
// Adjuntar a la configuración
config.ConnConfig.Tracer = &MyTracer{}
Errores comunes clave:
- Si no implementas BatchTracer, las operaciones Batch pasarán desapercibidas. No hay error, pero se pierde data.
- Para integración con OTel, crea spans manualmente dentro de los métodos de QueryTracer y pasa el contexto correctamente.
- QueryTracer no reemplaza las métricas: se necesita lógica adicional para agregación.
Comparación de pila: sqlx, pgx y soluciones personalizadas
La elección de herramienta depende de la pila actual y los requisitos de detalle. Aquí hay tres opciones:
- sqlx + otelsql
- Configuración: Una línea de código.
- Ventajas: No requiere cambio de driver, soporte básico de trazado.
- Desventajas: Sin soporte para Batch o CopyFrom, sin lógica personalizada.
- Recomendación: Bueno para proyectos simples sin operaciones complejas en BD.
- pgx + otelpgx
- Configuración: Una línea de código.
- Ventajas: Soporte completo para Batch y CopyFrom, creación automática de spans.
- Desventajas: Requiere cambiar de sqlx a pgx.
- Recomendación: Lo mejor para proyectos nuevos o migraciones.
- pgx + MyTracer
- Configuración: Implementación manual de interfaces.
- Ventajas: Control total (métricas personalizadas, registro avanzado).
- Desventajas: Alta complejidad, requiere mantenimiento.
- Recomendación: Solo para necesidades específicas no cubiertas por otelpgx.
Importante: En todos los casos, el contexto (context.Context) debe pasarse a través de todas las capas de la app, desde el manejador hasta el driver. De lo contrario, los spans se convierten en raíz y pierden la relación con la solicitud original del usuario.
Lecciones clave
- Las métricas muestran el problema, pero no la causa. Úsalas para monitoreo, no para diagnósticos profundos.
- QueryTracer es un gancho de trazado, no para métricas. Desglosa las transacciones en etapas para análisis detallado.
- OpenTelemetry requiere propagación correcta del contexto. Asegúrate de que context.Context fluya por todas las capas de la app.
- Los decoradores funcionan para métricas simples, pero no para transacciones. Usa QueryTracer o OTel para diagnósticos intra-transacción.
- Elección de pila: Quédate con otelsql si usas sqlx sin operaciones complejas. Cambia a pgx + otelpgx para visibilidad completa.
— Editorial Team
Aún no hay comentarios.