Zurück zur Startseite

Beobachtbarkeit in Go: QueryTracer, Metriken und OTel

Der Artikel vergleicht Beobachtbarkeitstools in Go: QueryTracer, Metriken und OpenTelemetry. Erklärt Unterschiede in der Anwendung und typische Fehler. Bietet Code-Beispiele und Empfehlungen zur Stack-Auswahl für verschiedene Szenarien.

QueryTracer vs OpenTelemetry: Wie wählen Sie Beobachtbarkeitstools in Go aus
Advertisement 728x90

Observability in Go: Die Wahl zwischen QueryTracer, Metrics und OpenTelemetry

Beim Einbinden von PostgreSQL in Go-Anwendungen stehen Entwickler oft vor der Wahl verschiedener Observability-Tools. Metrics, QueryTracer und OpenTelemetry lösen unterschiedliche Probleme, lassen sich aber leicht verwechseln. Lassen Sie uns aufschlüsseln, wann welches zu verwenden ist und wie man gängige Fallstricke vermeidet – basierend auf einem Praxisbeispiel mit einem Decorator und Transaktionen.

Drei Ebenen der Observability: Metrics, Tracing und Kontext

Metrics sind die grundlegende Ebene der Observability. Sie beantworten die Frage „Was passiert gerade?“: Zum Beispiel ein Anstieg der Fehler nach einem Deployment oder die p99-Endpunkt-Antwortzeit springt von 50 ms auf 800 ms. Das Sammeln von Metrics ist ressourcenschonend und ermöglicht eine schnelle Anomalie-Erkennung. Metrics fehlt jedoch der Kontext: Sie zeigen, dass eine Abfrage langsamer geworden ist, erklären aber nicht, warum. Dafür braucht man Tracing.

QueryTracer ist eine Schnittstelle, die der pgx-Treiber bereitstellt, um Operationen auf Treiberebene abzufangen. Sie erzeugt separate Events für jede Transaktionsphase: BEGIN, die Abfrage selbst und COMMIT. QueryTracer ist kein Metrics-Sammeltool, sondern ein Hook, in den Sie Logik zum Erstellen von Spans, zum Protokollieren von SQL oder zum Sammeln von Metrics einfügen können. Wichtig: QueryTracer selbst sendet keine Daten – es stellt lediglich einen Mechanismus zur Generierung zur Verfügung.

Google AdInline article slot

OpenTelemetry (OTel) übernimmt das verteilte Tracing. Ein Span wird auf HTTP-Request-Handler-Ebene erstellt und über den Kontext (context.Context) den Stack hinuntergereicht: Service, Repository, Treiber. Jede Ebene fügt einen Child-Span hinzu und bildet einen Ausführungsbaum. In einem Visualisierungssystem (z. B. Jaeger) sehen Sie das volle Bild: Welche spezifische DB-Abfrage war langsam und in welchem Kontext. OTel läuft durchgehend, verwendet aber Sampling, um den Overhead zu reduzieren: Zum Beispiel werden nur 10 % der Requests getraced. Wird ein Request gesampled, wird der gesamte Span-Baum aufgezeichnet.

Decorator-Beispiel: Wo sich das Problem verbirgt

In typischen Projekten wird ein Decorator über dem Repository für das Metrics-Sammeln verwendet. Hier ein Implementierungsbeispiel:

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
}

Für Methoden, die eine einzelne SQL-Abfrage ausführen, funktioniert der Decorator präzise: Die Metric entspricht der Abfrage-Ausführungszeit. Bei Methoden mit Transaktionen sieht das anders aus. Betrachten Sie dieses Beispiel:

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
    })
}

Der Decorator misst die Zeit des gesamten Blocks – inklusive Validierung, Transaktionsstart, Abfrageausführung und Commit. Die Metric create_booking = 45ms verrät nicht, welche Phase 40 ms in Anspruch nimmt. QueryTracer zerlegt die Transaktion hingegen in drei separate Events:

  • BEGIN = 0.1ms
  • INSERT = 40ms
  • COMMIT = 2ms

Diese Granularität ist entscheidend, um Performance-Engpässe zu diagnostizieren.

QueryTracer implementieren: Schnittstellen und Fallstricke

QueryTracer besteht aus Schnittstellen in pgx, jeweils für einen bestimmten Operationstyp. BatchTracer wird beispielsweise für Batch-Operationen verwendet:

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)
}

Zur Implementierung erstellen Sie eine Struct, die diese Schnittstellen erfüllt:

type MyTracer struct{}

func (t *MyTracer) TraceQueryStart(ctx context.Context, conn *Conn, data TraceQueryStartData) context.Context {
    // Query-Start: Span erstellen oder loggen
    return ctx
}

func (t *MyTracer) TraceQueryEnd(ctx context.Context, conn *Conn, data TraceQueryEndData) {
    // Query-Ende: Zeit aufzeichnen, Metric senden
}

// An Config anhängen
config.ConnConfig.Tracer = &MyTracer{}

Wichtige Fallstricke:

  • Implementieren Sie BatchTracer nicht, gehen Batch-Operationen unbemerkt vorüber. Es tritt kein Fehler auf, aber Daten gehen verloren.
  • Für die OTel-Integration Spans manuell in den QueryTracer-Methoden erstellen und den Kontext korrekt weiterleiten.
  • QueryTracer ersetzt keine Metrics: Zusätzliche Logik für die Aggregation ist erforderlich.

Stack-Vergleich: sqlx, pgx und eigene Lösungen

Die Wahl des Tools hängt vom aktuellen Stack und den Detailanforderungen ab. Hier drei Optionen:

  • sqlx + otelsql

- Setup: Eine Codezeile.

- Vorteile: Kein Treiberwechsel nötig, grundlegende Tracing-Unterstützung.

- Nachteile: Kein Batch- oder CopyFrom-Support, keine benutzerdefinierte Logik.

- Empfehlung: Gut für einfache Projekte ohne komplexe DB-Operationen.

  • pgx + otelpgx

- Setup: Eine Codezeile.

- Vorteile: Voller Batch- und CopyFrom-Support, automatisches Span-Erstellen.

- Nachteile: Wechsel von sqlx zu pgx erforderlich.

- Empfehlung: Ideal für neue Projekte oder Migrationen.

  • pgx + MyTracer

- Setup: Manuelle Schnittstellenimplementierung.

- Vorteile: Volle Kontrolle (benutzerdefinierte Metrics, erweitertes Logging).

- Nachteile: Hohe Komplexität, Wartungsaufwand.

- Empfehlung: Nur bei spezifischen Anforderungen, die otelpgx nicht abdeckt.

Wichtig: In allen Fällen muss der Kontext (context.Context) durch alle App-Schichten – vom Handler bis zum Treiber – weitergegeben werden. Andernfalls werden Spans zur Root-Ebene und verlieren den Bezug zur ursprünglichen User-Request.

Wichtige Erkenntnisse

  • Metrics zeigen das Problem, aber nicht die Ursache. Nutzen Sie sie für Monitoring, nicht für tiefe Diagnosen.
  • QueryTracer ist ein Tracing-Hook, kein Metrics-Tool. Es zerlegt Transaktionen in Phasen für detaillierte Analysen.
  • OpenTelemetry erfordert korrekte Kontext-Weitergabe. Stellen Sie sicher, dass context.Context durch alle App-Schichten fließt.
  • Decorator eignen sich für einfache Metrics, nicht für Transaktionen. Für Intra-Transaktions-Diagnosen QueryTracer oder OTel verwenden.
  • Stack-Wahl: Bei sqlx ohne komplexe Operationen otelsql beibehalten. Zu pgx + otelpgx wechseln für volle Sichtbarkeit.

— Editorial Team

Advertisement 728x90

Weiterlesen