Volver al inicio

Observabilidad en Go: QueryTracer, métricas y OTel

El artículo compara herramientas de observabilidad en Go: QueryTracer, métricas y OpenTelemetry. Explica diferencias en la aplicación y errores típicos. Proporciona ejemplos de código y recomendaciones para la selección de stacks en diferentes escenarios.

QueryTracer vs OpenTelemetry: cómo elegir herramientas de observabilidad en Go
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
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:

Google AdInline article slot
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

Advertisement 728x90

Leer después