# Obserwowalność w Go: wybór między QueryTracer, metrykami i OpenTelemetry
Podczas integrowania PostgreSQL z aplikacjami Go programiści często stają przed wyborem narzędzi do obserwowalności. Metryki, QueryTracer i OpenTelemetry rozwiązują różne zadania, ale łatwo je pomylić. Rozbieramy, kiedy używać każdego, i jak unikać typowych błędów, opierając się na rzeczywistym przypadku z dekoratorem i transakcjami.
Trzy poziomy obserwowalności: metryki, tracing i kontekst
Metryki to podstawowy poziom obserwowalności. Odpowiadają na pytanie «co się dzieje»: na przykład wzrost liczby błędów po wdrożeniu lub zwiększenie p99 czasu odpowiedzi endpointu z 50 ms do 800 ms. Zbieranie metryk jest tanie pod względem zasobów i pozwala szybko wykryć anomalię. Jednak metryki nie dają kontekstu: pokazują, że zapytanie spowolniło, ale nie wyjaśniają dlaczego. Aby odpowiedzieć na to pytanie, niezbędny jest tracing.
QueryTracer to interfejs dostarczany przez sterownik pgx do przechwycenia operacji na poziomie sterownika. Generuje oddzielne zdarzenia dla każdego etapu transakcji: BEGIN, samego zapytania i COMMIT. To nie narzędzie do zbierania metryk, lecz punkt wstawienia (hook), w który możesz włożyć logikę do tworzenia spanów, logowania SQL lub zbierania metryk. Ważne, by zrozumieć: sam QueryTracer nie wysyła danych — jedynie dostarcza mechanizm do ich generowania.
OpenTelemetry (OTel) rozwiązuje zadanie rozproszonego tracingu. Span jest tworzony na poziomie obsługującego zapytanie HTTP i przekazywany przez kontekst (context.Context) w głąb stosu: serwis, repozytorium, sterownik. Każdy poziom dodaje span potomny, tworząc drzewo wykonania. W systemie wizualizacji (np. Jaeger) widzisz pełny obraz: które konkretnie zapytanie do bazy danych powodowało opóźnienia i w jakim kontekście. OTel działa stale, ale dla zmniejszenia obciążenia stosuje próbkowanie: np. śledzi tylko 10% zapytań. Jeśli zapytanie trafi do próbki, zapisywane jest pełne drzewo spanów.
Przykład z dekoratorem: gdzie czai się problem
W typowych projektach do zbierania metryk stosuje się dekorator nad repozytorium. Rozważmy przykład implementacji:
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
}
Dla metod wykonujących jedno zapytanie SQL dekorator działa precyzyjnie: metryka odpowiada czasowi wykonania zapytania. Jednak w metodach z transakcjami sytuacja się zmienia. Weźmy przykład:
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
})
}
Dekorator mierzy czas całego bloku, włączając walidację, rozpoczęcie transakcji, wykonanie zapytania i commit. Metryka create_booking = 45ms nie ujawnia, który etap zajmuje 40 ms. Tymczasem QueryTracer rozbija transakcję na trzy oddzielne zdarzenia:
- BEGIN = 0.1ms
- INSERT = 40ms
- COMMIT = 2ms
Ta szczegółowość jest kluczowa do diagnozowania wąskich gardeł wydajnościowych.
Implementacja QueryTracer: interfejsy i pułapki
QueryTracer to zestaw interfejsów w pgx, każdy dla innego typu operacji. Na przykład dla operacji Batch używany jest interfejs BatchTracer:
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)
}
Implementacja wymaga stworzenia struktury spełniającej te interfejsy:
type MyTracer struct{}
func (t *MyTracer) TraceQueryStart(ctx context.Context, conn *Conn, data TraceQueryStartData) context.Context {
// Początek zapytania: tworzymy span lub logujemy
return ctx
}
func (t *MyTracer) TraceQueryEnd(ctx context.Context, conn *Conn, data TraceQueryEndData) {
// Koniec zapytania: zapisujemy czas, wysyłamy metrykę
}
// Podłączenie do konfiguracji
config.ConnConfig.Tracer = &MyTracer{}
Kluczowe pułapki:
- Jeśli nie zaimplementujesz BatchTracer, operacje Batch pozostaną niezauważone. Błędu nie będzie, ale dane zostaną utracone.
- Do pracy z OTel trzeba ręcznie tworzyć spany wewnątrz metod QueryTracer i poprawnie przekazywać kontekst.
- QueryTracer nie zastępuje metryk: do agregacji danych will be required dodatkowa logika.
Porównanie stosów: sqlx, pgx i rozwiązania niestandardowe
Wybór narzędzia zależy od aktualnego stosu i wymagań co do szczegółowości. Rozważmy trzy warianty:
- sqlx + otelsql
- Podłączenie: jedna linia kodu.
- Zalety: nie wymaga zmiany sterownika, obsługa podstawowego tracingu.
- Ograniczenia: brak obsługi Batch i CopyFrom, brak możliwości niestandardowej logiki.
- Rekomendacja: nadaje się do prostych projektów bez skomplikowanych operacji na bazie danych.
- pgx + otelpgx
- Podłączenie: jedna linia kodu.
- Zalety: pełna obsługa Batch i CopyFrom, automatyczne tworzenie spanów.
- Ograniczenia: wymaga przejścia z sqlx na pgx.
- Rekomendacja: optymalny wybór dla nowych projektów lub migracji istniejących.
- pgx + MyTracer
- Podłączenie: ręczna implementacja interfejsów.
- Zalety: pełna kontrola nad logiką (niestandardowe metryki, rozszerzone logowanie).
- Ograniczenia: wysoka złożoność, konieczność utrzymania.
- Rekomendacja: tylko przy specyficznych wymaganiach nieobsługiwanych przez otelpgx.
Ważne: we wszystkich przypadkach kontekst (context.Context) musi być przekazywany przez wszystkie warstwy aplikacji — od handlera do sterownika. Bez tego spany będą korzeniowe i stracą powiązanie z pierwotnym zapytaniem użytkownika.
Co ważne
- Metryki pokazują problem, ale nie przyczynę. Warto je używać do monitoringu, ale nie do diagnozowania wąskich gardeł.
- QueryTracer to hook dla tracingu, nie metryk. Rozbija transakcje na etapy, co niezbędne do szczegółowej analizy.
- OpenTelemetry wymaga poprawnego przekazywania kontekstu. Upewnij się, że context.Context jest przekazywany przez wszystkie warstwy aplikacji.
- Dekorator nadaje się do prostych metryk, ale nie do transakcji. Do diagnozowania wewnątrz transakcji używaj QueryTracer lub OTel.
- Wybór stosu: jeśli projekt już używa sqlx i nie wymaga skomplikowanych operacji — zostań przy otelsql. Dla pełnego obrazu przechodź na pgx z otelpgx.
— Editorial Team
Brak komentarzy.