Śledzenie OpenTelemetry w Go dla pełnej widoczności API
Śledzenie OpenTelemetry w Go wyjaśnione z praktycznymi krokami, jak skorelować trace'y, metryki i logi w żądaniach HTTP, zadaniach w tle i wywołaniach zewnętrznych.

Co oznacza end-to-end tracing dla API w Go
Trace to oś czasu jednego żądania w twoim systemie. Zaczyna się, gdy przychodzi wywołanie API, i kończy się, gdy wysyłasz odpowiedź.
W trace'ie są spans. Span to pojedynczy krok z pomiarem czasu, jak „parsowanie żądania”, „wykonaj SQL” albo „wywołanie dostawcy płatności”. Spany mogą też zawierać przydatne szczegóły, takie jak kod statusu HTTP, bezpieczny identyfikator użytkownika czy ile wierszy zwróciło zapytanie.
„End-to-end” oznacza, że trace nie kończy się w pierwszym handlerze. Podąża za żądaniem przez miejsca, w których zazwyczaj kryją się problemy: middleware, zapytania do bazy, wywołania cache, zadania w tle, zewnętrzne API (płatności, e-mail, mapy) i inne wewnętrzne serwisy.
Śledzenie jest najbardziej wartościowe, gdy problemy pojawiają się sporadycznie. Jeśli jedno na 200 żądań jest wolne, logi często wyglądają identycznie dla szybkich i wolnych przypadków. Trace pokazuje różnicę: jedno żądanie spędziło 800 ms czekając na zewnętrzne wywołanie, było dwukrotnie retryowane, a potem uruchomiło zadanie follow-up.
Trudno też łączyć logi między usługami. Możesz mieć linię logu w API, inną w workerze i nic pomiędzy. Dzięki śledzeniu te zdarzenia mają ten sam trace_id, więc możesz śledzić łańcuch bez zgadywania.
Traces, metryki i logi: jak się uzupełniają
Traces, metryki i logi odpowiadają na różne pytania.
Traces pokazują, co wydarzyło się dla jednego rzeczywistego żądania. Mówią, gdzie poszło najbardziej czasu: handler, zapytania do bazy, próby w cache czy wywołania zewnętrzne.
Metryki pokazują trend. Są najlepsze do alertów, bo są stabilne i tanie do agregowania: percentyle opóźnień, częstość żądań, częstość błędów, głębokość kolejki czy saturacja.
Logi to „dlaczego” w czystym tekście: walidacje, nieoczekiwane dane, przypadki brzegowe i decyzje kodu.
Prawdziwy zysk to korelacja. Gdy ten sam trace_id pojawia się w spanach i w ustrukturyzowanych logach, możesz przejść z wpisu błędu do dokładnego trace'a i od razu zobaczyć, która zależność spowolniła lub który krok zawiódł.
Prosty model myślowy
Używaj każdego sygnału do tego, do czego jest najlepszy:
- Metryki mówią, że coś jest nie tak.
- Traces pokazują, gdzie poszedł czas dla jednego żądania.
- Logi wyjaśniają, co twój kod postanowił i dlaczego.
Przykład: endpoint POST /checkout zaczyna mieć timouty. Metryki pokazują skok p95. Trace pokazuje, że większość czasu to wywołanie do dostawcy płatności. Powiązany wpis w logu wewnątrz tego spanu pokazuje ponownie próby z powodu 502, co sugeruje ustawienia backoffu lub incydent po stronie upstream.
Zanim dodasz kod: nazewnictwo, sampling i co śledzić
Trochę planowania na początku sprawia, że trace'y będą później łatwe do wyszukania. Bez tego nadal będziesz zbierać dane, ale podstawowe pytania stają się trudne: „Czy to staging czy prod?” „Która usługa rozpoczęła problem?”
Zacznij od spójnej identyfikacji. Wybierz jasne service.name dla każdego API w Go (np. checkout-api) i jedno pole środowiska, np. deployment.environment=dev|staging|prod. Trzymaj to stabilne. Jeśli nazwy zmienią się w trakcie tygodnia, wykresy i wyszukiwania będą wyglądać jak różne systemy.
Następnie zdecyduj o samplingu. Śledzenie każdego żądania świetnie sprawdza się w development, ale w produkcji może być zbyt kosztowne. Częsty wzorzec to próbkowanie niewielkiego odsetka normalnego ruchu i zachowywanie trace'ów dla błędów oraz wolnych żądań. Jeśli znasz już wysokoruchowe endpointy (health checki, polling), śledź je rzadziej lub wcale.
Na koniec uzgodnij, co będziesz tagować na spanach, a czego nigdy nie zbierać. Trzymaj krótką allowlistę atrybutów, które pomagają łączyć zdarzenia między usługami, i napisz proste zasady prywatności.
Dobre tagi zwykle obejmują stabilne ID i poglądowe informacje o żądaniu (szablon trasy, metoda, kod statusu). Unikaj wrażliwych danych całkowicie: hasła, dane płatności, pełne maile, tokeny auth i surowe ciała żądań. Jeśli musisz dołączyć wartości związane z użytkownikiem, zhashuj lub zredaguj je przed dodaniem.
Krok po kroku: dodaj OpenTelemetry do API HTTP w Go
Skonfigurujesz tracer provider raz przy starcie aplikacji. To decyduje, gdzie trafiają spany i jakie atrybuty zasobu są dołączane do każdego spanu.
1) Zainicjalizuj OpenTelemetry
Upewnij się, że ustawiasz service.name. Bez niego trace'y z różnych usług mogą się mieszać i wykresy staną się trudne do odczytania.
// main.go (startup)
exp, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())
res, _ := resource.New(context.Background(),
resource.WithAttributes(
semconv.ServiceName("checkout-api"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
To jest fundament śledzenia OpenTelemetry w Go. Następnie potrzebujesz spanu dla każdego przychodzącego żądania.
2) Dodaj middleware HTTP i przechwytuj kluczowe pola
Użyj middleware HTTP, który automatycznie zaczyna span i rejestruje kod statusu oraz czas trwania. Nadaj spanowi nazwę używając szablonu trasy (np. /users/:id), a nie surowego URL-a, bo inaczej będziesz mieć tysiące unikalnych ścieżek.
Celuj w czystą bazę: jeden span serwera na żądanie, nazwy spanów oparte na trasie, zarejestrowany status HTTP, błędy handlera odzwierciedlone jako error w spanie i czas trwania widoczny w przeglądarce trace'ów.
3) Uczyń błędy oczywistymi
Gdy coś pójdzie źle, zwróć błąd i oznacz bieżący span jako nieudany. Dzięki temu trace wyróżni się, nawet zanim spojrzysz na logi.
W handlerach możesz zrobić:
span := trace.SpanFromContext(r.Context())
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
4) Zweryfikuj identyfikatory trace lokalnie
Uruchom API i wywołaj endpoint. Zaloguj trace ID z kontekstu żądania raz, aby potwierdzić, że zmienia się dla każdego żądania. Jeśli jest zawsze pusta, twoje middleware nie używa tego samego kontekstu, który otrzymuje handler.
Przenoś kontekst przez DB i wywołania zewnętrzne
Widoczność end-to-end kończy się w momencie, gdy porzucisz context.Context. Przychodzący kontekst żądania powinien być nitką, którą przekazujesz do każdego wywołania DB, HTTP i helperów. Jeśli zastąpisz go context.Background() lub zapomnisz go przekazać, twój trace rozpadnie się na oddzielne, niespowinowacone prace.
Dla wychodzącego HTTP użyj instrumentowanego transportu, żeby każde Do(req) stało się child spanem bieżącego żądania. Przekazuj nagłówki W3C trace na wyjściu, żeby downstream mógł dołączyć swoje spany do tego samego trace'a.
Zapytania do bazy wymagają tego samego traktowania. Użyj instrumentowanego drivera lub owiń wywołania spanami wokół QueryContext i ExecContext. Rejestruj tylko bezpieczne szczegóły. Chcesz znaleźć wolne zapytania bez wycieku danych.
Przydatne, niskoryzykowne atrybuty to nazwa operacji (np. SELECT user_by_id), nazwa tabeli lub modelu, liczba wierszy (tylko liczba), czas trwania, liczba ponowień i ogólny typ błędu (timeout, canceled, constraint).
Timeouty są częścią historii, nie tylko porażkami. Ustawiaj je przy context.WithTimeout dla DB i wywołań zewnętrznych i pozwól, by anulowania się przebijały. Gdy wywołanie zostanie anulowane, oznacz span jako error i dodaj krótki powód typu deadline_exceeded.
Śledzenie zadań w tle i kolejek
Prace w tle to miejsce, gdzie trace'y często się urywają. Żądanie HTTP kończy się, a worker odbiera wiadomość później na innej maszynie bez współdzielonego kontekstu. Jeśli nic nie zrobisz, masz dwie historie: trace API i trace joba, który wygląda, jakby zaczął się znikąd.
Rozwiązanie jest proste: gdy enqueujesz zadanie, przechwyć bieżący kontekst trace i zapisz go w metadanych zadania (payload, nagłówki lub atrybuty, w zależności od kolejki). Gdy worker startuje, wyciągnij ten kontekst i rozpocznij nowy span jako child oryginalnego żądania.
Propaguj kontekst bezpiecznie
Kopiuj tylko kontekst trace, nie dane użytkownika.
- Wstrzykuj tylko identyfikatory trace i flagi samplingu (w stylu W3C
traceparent). - Trzymaj to oddzielnie od pól biznesowych (np. dedykowane pole
otellubtrace). - Traktuj to jako nieufne wejście przy odczycie (waliduj format, obsługuj brakujące dane).
- Unikaj umieszczania tokenów, maili czy ciał żądań w metadanych zadania.
Spany do dodania (bez zalewania trace'ów szumem)
Czytelne trace'y zwykle mają kilka znaczących spanów, a nie dziesiątki drobnych. Twórz spany wokół granic i „punktów oczekiwania”. Dobrym punktem startowym jest span enqueue w handlerze API i job.run w workerze.
Dodaj niewiele kontekstu: numer próby, nazwa kolejki, typ zadania i rozmiar payloadu (bez treści). Jeśli występują retry, rejestruj je jako oddzielne spany lub zdarzenia, żeby widzieć opóźnienia backoffu.
Zadania cykliczne też potrzebują rodzica. Jeśli nie ma przychodzącego żądania, utwórz nowy root span dla każdego uruchomienia i oznacz go nazwą harmonogramu.
Korelacja logów ze śledzeniem (i bezpieczeństwo logów)
Trace mówią, gdzie poszedł czas. Logi mówią, co się wydarzyło i dlaczego. Najprostszy sposób połączenia ich to dodanie trace_id i span_id do każdego wpisu logu jako pól strukturalnych.
W Go pobierz aktywny span z context.Context i wzbogacaj logger raz na żądanie (lub zadanie). Wtedy każdy wpis logu wskazuje na konkretny trace.
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
logger := baseLogger.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
logger.Info("charge_started", "order_id", orderID)
To wystarczy, żeby przeskoczyć z wpisu logu do dokładnego spanu, który był wykonywany w tym momencie. Pokazuje też brak kontekstu: trace_id będzie pusty.
Utrzymuj logi użyteczne bez wycieku PII
Logi często żyją dłużej i podróżują dalej niż trace'y, więc bądź bardziej restrykcyjny. Preferuj stabilne identyfikatory i wyniki: user_id, order_id, payment_provider, status i error_code. Jeśli musisz logować dane wejściowe od użytkownika, najpierw je zredaguj i ogranicz długość.
Ułatw grupowanie błędów
Używaj spójnych nazw zdarzeń i typów błędów, żeby móc je zliczać i wyszukiwać. Jeśli sformułowanie zmienia się za każdym razem, ten sam problem wygląda jak wiele różnych.
Dodaj metryki, które naprawdę pomagają znaleźć problemy
Metryki są twoim systemem wczesnego ostrzegania. W środowisku z już używanym OpenTelemetry dla Go, metryki powinny odpowiadać: jak często, jak źle i od kiedy.
Zacznij od małego zestawu, który działa dla prawie każdego API: liczba żądań, liczba błędów (według klasy statusu), percentyle opóźnień (p50, p95, p99), zapytania w locie (in-flight) oraz opóźnienia zależności dla DB i kluczowych wywołań zewnętrznych.
Aby metryki pasowały do trace'ów, używaj tych samych szablonów tras i nazw. Jeśli twoje spany używają /users/{id}, twoje metryki też powinny. Wtedy gdy wykres pokaże „p95 dla /checkout wzrosło”, możesz przejść od razu do trace'ów przefiltrowanych do tej trasy.
Uważaj na etykiety (atrybuty). Jedna zła etykieta może wybuchnąć koszty i zniszczyć pulpit. Szablon trasy, metoda, klasa statusu i nazwa serwisu są zwykle bezpieczne. ID użytkownika, maile, pełne URL-e i surowe komunikaty błędów raczej nie.
Dodaj kilka niestandardowych metryk dla krytycznych zdarzeń biznesowych (np. checkout started/completed, błędy płatności wg kodu wynikowego, sukces vs retry joba). Trzymaj zestaw mały i usuwaj to, czego nie używasz.
Eksportowanie telemetrii i bezpieczne wdrażanie
Eksportowanie to moment, gdy OpenTelemetry staje się praktyczne. Twoja usługa musi wysyłać spany, metryki i logi gdzieś niezawodnie, bez spowalniania żądań.
Dla lokalnego developmentu trzymaj to prosto. Eksporter konsolowy (lub OTLP do lokalnego collectora) pozwala szybko zobaczyć trace'y i zweryfikować nazwy spanów oraz atrybuty. W produkcji preferuj OTLP do agenta lub OpenTelemetry Collector blisko usługi. Daje to jedno miejsce do obsługi retry, routingu i filtrowania.
Batchowanie ma znaczenie. Wysyłaj telemetrykę partiami w krótkich interwałach, z ostrymi timeoutami, aby zapętlona sieć nie blokowała aplikacji. Telemetryka nie powinna być na ścieżce krytycznej. Jeśli eksporter nie nadąża, powinien odrzucać dane zamiast budować zużycie pamięci.
Sampling utrzymuje koszty pod kontrolą. Zacznij od head-based sampling (np. 1–10% żądań), potem dodaj proste reguły: zawsze sample błędy i zawsze sample wolne żądania powyżej progu. Jeśli masz high-volume joby w tle, sampleuj je rzadziej.
Wdrażaj stopniowo: dev z 100% samplingiem, staging z realistycznym ruchem i niższym samplingiem, potem produkcja z konserwatywnym samplingiem i alertami na błędy eksportera.
Typowe błędy, które psują end-to-end widoczność
Widoczność end-to-end najczęściej zawodzi z prostych powodów: dane są, ale nie łączą się.
Problemy, które łamią rozproszone śledzenie w Go, to zwykle:
- Porzucanie kontekstu między warstwami. Handler tworzy span, ale wywołanie DB, klient HTTP lub goroutine używa
context.Background()zamiast kontekstu żądania. - Zwracanie błędów bez oznaczenia spanów. Jeśli nie zarejestrujesz błędu i nie ustawisz statusu spanu, trace będzie wyglądał „na zielono”, mimo że użytkownicy widzą 500.
- Instrumentowanie wszystkiego. Jeśli każdy helper stanie się spanem, trace'y zmienią się w szum i będą droższe.
- Dodawanie atrybutów o wysokiej kardynalności. Pełne URL-e z ID, maile, surowe wartości SQL, ciała żądań czy surowe komunikaty błędów mogą stworzyć miliony unikalnych wartości.
- Ocenianie wydajności po średnich. Incydenty pojawiają się w percentylach (p95/p99) i w wskaźniku błędów, nie w średnim czasie.
Szybki test zdrowia to wybrać jedno rzeczywiste żądanie i śledzić je przez granice. Jeśli nie widzisz jednego trace_id płynącego przez żądanie przychodzące, zapytanie do DB, wywołanie zewnętrzne i worker asynchroniczny, nie masz jeszcze end-to-end widoczności.
Praktyczna lista kontrolna „gotowe”
Jesteś blisko, gdy możesz z raportu użytkownika dotrzeć do dokładnego żądania, a potem śledzić je przez każdy hop.
- Wybierz jedną linię logu API i znajdź dokładny trace po
trace_id. Potwierdź, że głębsze logi z tego samego żądania (DB, klient HTTP, worker) mają ten sam kontekst trace. - Otwórz trace i sprawdź zagnieżdżenie: span serwera HTTP na górze, z child spanami dla zapytań DB i zewnętrznych API. Płaska lista często oznacza utratę kontekstu.
- Wywołaj zadanie w tle z żądania API (np. wysłanie potwierdzenia e-mail) i potwierdź, że span worker łączy się z żądaniem.
- Sprawdź metryki podstawowe: liczba żądań, wskaźnik błędów i percentyle opóźnień. Potwierdź, że możesz filtrować po trasie lub operacji.
- Przeskanuj atrybuty i logi pod kątem bezpieczeństwa: brak haseł, tokenów, pełnych numerów kart czy surowych danych osobowych.
Prosty test rzeczywistości to zasymulować wolny checkout, gdzie dostawca płatności się opóźnia. Powinieneś zobaczyć jeden trace z wyraźnie oznaczonym spaniem zewnętrznym oraz pik metryczny p95 dla trasy checkout.
Jeśli generujesz backendy w Go (np. z AppMaster), warto uczynić tę listę kontrolną elementem rutyny wydania, aby nowe endpointy i workery pozostały śledzalne w miarę rozwoju aplikacji. AppMaster (appmaster.io) generuje prawdziwe serwisy Go, więc możesz ustandaryzować jedno ustawienie OpenTelemetry i przenosić je między usługami i zadaniami w tle.
Przykład: debugowanie wolnego checkoutu między usługami
Wiadomość od klienta: „Checkout czasem się zawiesza.” Nie możesz tego odtworzyć na żądanie — dokładnie wtedy przydaje się śledzenie OpenTelemetry w Go.
Zacznij od metryk, żeby zrozumieć kształt problemu. Sprawdź częstość żądań, wskaźnik błędów i p95 lub p99 dla endpointu checkout. Jeśli spowolnienie występuje w krótkich skokach i tylko dla części żądań, zwykle wskazuje to na zależność, kolejkę lub zachowanie retry, a nie CPU.
Następnie otwórz wolny trace z tego okna czasowego. Często wystarczy jeden trace. Zdrowy checkout może trwać 300–600 ms end-to-end. Zły może trwać 8–12 sekund, z większością czasu w jednym spanie.
Typowy wzorzec: handler API jest szybki, praca z DB jest w porządku, ale span dostawcy płatności pokazuje ponowienia z backoffem, a wywołanie downstream czeka za lockiem lub kolejką. Odpowiedź może nadal zwrócić 200, więc alerty oparte tylko na błędach nigdy się nie włączą.
Powiązane logi mówią dokładną ścieżkę w prostym języku: „retrying Stripe charge: timeout”, potem „db tx aborted: serialization failure”, potem „retry checkout flow”. To jasny sygnał, że kilka drobnych problemów łączy się w złe doświadczenie użytkownika.
Gdy znajdziesz wąskie gardło, spójność utrzymuje czytelność na dłuższą metę. Ustandaryzuj nazwy spanów, atrybuty (bezpieczny hash user_id, order_id, nazwa zależności) i reguły samplingu między usługami, tak aby wszyscy czytali trace'y tak samo.


