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.
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:
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:
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
Noch keine Kommentare.