OpenTelemetry od zera: instrumentacja aplikacji i wysyłka trace do Jaegera lub Grafany

0
14
Rate this post

Chcesz „tylko zobaczyć trace’y” i szybko zdecydować: lepiej odpalić Jaegera czy Grafanę (Tempo) — i czy od razu stawiać OpenTelemetry Collectora? To dobra perspektywa, bo w OpenTelemetry najwięcej czasu przepala się nie na samą instalację, tylko na typowe wpadki: brak propagacji kontekstu, chaos w nazewnictwie, zły sampling, „przegadana” auto-instrumentacja i przypadkowe wrzucenie danych wrażliwych do atrybutów.

Żeby ułatwić decyzję i skrócić drogę do działającego distributed tracing, podejście będzie ostrzegawcze, ale praktyczne: konkretne objawy, skutki i szybkie poprawki. Na końcu dostaniesz checklistę wdrożeniową, którą da się odhaczyć w jeden wieczór (albo w sprint), bez budowania od razu wielkiej platformy obserwowalności.

Realne pytania, które zwykle stoją za tym tematem:

  • Jakie jest absolutne minimum, żeby trace’y zaczęły się pojawiać?
  • Kiedy wystarczy auto-instrumentation, a kiedy ręczne spany są nieuniknione?
  • Czy wysyłać dane bezpośrednio do Jaegera/Tempo, czy zawsze przez OpenTelemetry Collector?
  • Jak poznać, że propagacja kontekstu jest zepsuta (i gdzie najczęściej pęka)?
  • Jak ustawić sampling, żeby nie utopić się w kosztach i narzucie?
  • Co z nazewnictwem (service.name, span names) i atrybutami, żeby później dało się to filtrować?
  • Jak nie wrzucić PII/sekretów do trace’ów i nie narobić sobie kłopotów?
  • Jak diagnozować „brak trace’ów”, „urwane trace’y”, „dziwne czasy” i „za dużo spanów”?

Frazy pomocnicze: OpenTelemetry instrumentacja, OpenTelemetry Collector, Jaeger tracing, Grafana Tempo trace, propagacja kontekstu W3C, OTLP gRPC vs HTTP, sampling head vs tail, service.name i atrybuty, auto-instrumentation vs manual spans, urwane trace’y, PII w atrybutach, korelacja logs-traces

Nawigacja:

Decyzja startowa, która oszczędza czas: Jaeger czy Grafana (Tempo) i czy stawiać Collectora

Szybka mapa decyzji (bez akademickiej teorii)

Jeśli zależy Ci na czasie, potraktuj wybór backendu i architektury eksportu jako decyzję o koszcie zmian za miesiąc, a nie tylko o „czy dziś działa”. Da się wystartować tanio i szybko, ale część skrótów potem boli (szczególnie przy wielu usługach albo gdy dochodzi filtracja danych/PII).

Minimalne pojęcia, które muszą „kliknąć”, żeby nie utknąć na debugowaniu:

  • Trace — całe przejście jednego żądania przez system (end-to-end). To to, co chcesz zobaczyć w Jaegerze lub Tempo.
  • Span — pojedynczy krok w trace (np. obsługa endpointu, zapytanie do DB, request do innej usługi).
  • Propagacja kontekstu — przekazywanie identyfikatora trace/span między usługami (najczęściej przez nagłówki HTTP). Bez tego każdy serwis zaczyna nowy trace.
  • Resource — „tożsamość” procesu/serwisu (np. service.name, wersja, środowisko). Stabilne dane, wspólne dla wszystkich spanów.
  • Span attributes — cechy konkretnego spana (np. http.route, status, db.system). Tu łatwo o kardynalność i PII.
  • Exporter — element SDK, który wysyła telemetrykę (najczęściej OTLP) do backendu lub Collectora.
  • OpenTelemetry Collector — osobny proces/usługa pośrednia: przyjmuje OTLP i może batchować, retry’ować, filtrować, maskować, robić tail sampling i wysyłać dalej.
  • Sampling — decyzja, które trace’y zachować. Klucz do kosztów i wydajności.

Jaeger „na start” vs Tempo/Grafana „na workflow”

Jaeger jest często najszybszą drogą do „mam UI i widzę trace”. Jest lekki w użyciu, popularny w lokalnych/dev setupach, prosty do odpalenia kontenerem. Jeśli potrzebujesz głównie przeglądania trace’y i szybkiej diagnostyki, Jaeger zwykle wystarczy na start.

Grafana Tempo ma duży sens, jeśli i tak żyjesz w ekosystemie Grafany: metryki w Prometheusie/Mimir, logi w Loki, dashboardy i alerty w Grafanie. Tempo samo w sobie to „magazyn trace”, ale realna wartość rośnie, gdy łączysz widok trace z metrykami i logami w jednym miejscu (np. kliknięcie z panelu latency do trace’ów). To często oszczędza czas zespołu bardziej niż „ładniejsze UI do trace”.

KryteriumJaegerGrafana Tempo
Szybkość startu (lokal/dev)Bardzo wysokaWysoka, ale zwykle dochodzi Grafana + datasource
Workflow w GrafanieIntegracja możliwa, ale nie „native”Naturalnie „u siebie” (trace + metrics + logs)
Kiedy najczęściej wygrywaProsty tracing, mało usług, szybkie demo/POCGdy i tak używasz Grafany i chcesz korelacji
Typowy błąd na starcieWysyłka w złym protokole/porcie, chaos w nazwachZamieszanie OTLP HTTP vs gRPC, brak Collectora przy większej skali

Direct export vs OpenTelemetry Collector: kiedy skrót jest OK

Są dwa sensowne warianty startu:

  • Najszybciej i najtaniej: aplikacja (SDK/agent) → exporter OTLP → backend (Jaeger/Tempo). Mniej elementów do utrzymania, mniej YAML-i. Ograniczenia: trudniej zmienić backend bez zmian w aplikacjach, trudniej robić tail sampling, filtrację atrybutów i redakcję danych.
  • Rozsądny standard: aplikacja → OTel Collector → backend. Minimalnie ustawiasz receiver OTLP + batch + exporter. Zyskujesz miejsce na retry, buforowanie, transformacje i bezbolesne przełączanie docelowego storage.

Collector od razu ma największy zwrot, gdy:

  • masz więcej niż 2–3 usługi i nie chcesz dotykać konfiguracji w każdej, gdy zmienisz backend lub protokół,
  • potrzebujesz tail sampling (zachowuj tylko trace’y z błędami / wolne),
  • musisz maskować/usuwać atrybuty (PII, tokeny, parametry),
  • zależy Ci na stabilnym eksporcie (batch + retry + kolejka), bo backend bywa chwilowo niedostępny.

Co sprawdzić przed decyzją (krótko, ale konkretnie)

  • Ile usług i ile ścieżek krytycznych? Im więcej, tym bardziej Collector się opłaca.
  • Czy masz API gateway / reverse proxy? To częste miejsce, gdzie giną nagłówki trace.
  • Czy masz async / message queue? Wtedy sama auto-instrumentacja HTTP nie da E2E.
  • Jakie środowiska chcesz objąć: dev (100% trace), prod (sampling)?
  • Czy w payloadach/URL-ach mogą być dane wrażliwe? Jeśli tak, planuj filtrację atrybutów i zasady higieny.
  • Czy masz wymagania dot. retencji i kosztów przechowywania? To wpływa na sampling i na wybór backendu.

Błąd 1 — „Chcę tylko zobaczyć trace”, ale nie ustalam minimalnego celu i scope’u instrumentacji

Objawy, które wyglądają jak „działa”, ale nic nie daje

Trace’y pojawiają się w Jaegerze albo Grafanie, tylko że po 30 minutach klikania nadal nie umiesz odpowiedzieć na proste pytania: który endpoint jest najwolniejszy, gdzie dokładnie leży bottleneck, czy problem jest w DB, czy w zewnętrznym API. W UI widzisz setki podobnych wpisów, a każdy serwis opisany inaczej.

Typowe symptomy braku scope’u:

  • instrumentujesz wszystko naraz, więc masz za dużo szumu i trudno znaleźć sensowny przykład,
  • service.name jest losowy lub różny między środowiskami,
  • nie ma spójnego nazewnictwa spanów dla endpointów (np. raz „GET”, raz pełny URL, raz nazwa kontrolera),
  • trace’y z healthchecków, metrics endpointów i botów dominują w danych.

Skutek: szybkie zniechęcenie i „tracing jest nieczytelny”

To nie jest problem Jaegera ani Tempo — tylko brak decyzji, po co instrumentujesz. Gdy nie ma celu, każdy kolejny span „może się przydać”, a potem płacisz za przechowywanie i tracisz czas na filtrowanie. W małym zespole to zabija temat, bo nie widać szybkiego zwrotu.

Interfejs programu do edycji audio i wideo z wykresami fal dźwięku
Źródło: Pexels | Autor: Pixabay

Drugi koszt to porównywalność: jeśli raz span nazywa się „GET /orders/123”, a innym razem „GET /orders/456”, to agregacja po nazwie nie działa, a UI zasypuje Cię unikatami.

Lepsze rozwiązanie: minimalny scope na start (wysoki efekt, niski wysiłek)

Na start wybierz 1–2 krytyczne ścieżki i instrumentuj tylko to, co pomoże Ci je zdiagnozować:

  • HTTP server (wejście requestu do serwisu),
  • HTTP client (wyjście do innych usług / zewnętrznych API),
  • DB (czas zapytania i błędy),
  • (opcjonalnie) cache, jeśli realnie wpływa na latency.

Resztę (kolejki, batch, mniej ważne endpointy) dołóż, gdy już masz pierwsze sensowne trace’y i wiesz, czego brakuje.

Minimalny standard danych, który ustawia porządek od pierwszego dnia

Najtańszy „kontrakt” w zespole, który oszczędza godziny w UI, to ustalenie kilku pól i zasad:

  • service.name: stała nazwa serwisu, bez środowiska (np. orders-api, nie orders-api-prod).
  • deployment.environment: osobno (np. dev/stage/prod).
  • service.version: wersja releasu/commit SHA (cokolwiek stabilnego).
  • Nazwa spana HTTP server: metoda + route (np. GET /orders/:id), nie pełny URL z ID.

To jest mało pracy, a robi różnicę między „ładny wykres” a „narzędzie diagnostyczne”.

Dwie ścieżki uruchomienia: szybka i standardowa

Wariant „najszybciej i najtaniej”: bez Collectora (na start)

Ten wariant ma sens, gdy chcesz zrobić POC albo instrumentujesz 1–2 usługi i nie potrzebujesz tail sampling ani filtracji atrybutów.

  • Włącz SDK/auto-instrumentation w aplikacji.
  • Ustaw exporter OTLP i endpoint backendu.
  • Ustaw service.name, environment i sampling (np. 100% w dev).

Pułapka: jeśli za tydzień dojdziesz do wniosku, że trzeba jednak Collectora, zmieniasz konfigurację w każdej usłudze (albo budujesz warstwę konfiguracji, co też kosztuje).

Wariant „rozsądny standard”: minimalny Collector od razu

Najprostszy Collector to receiver OTLP + processor batch + exporter do Jaegera/Tempo. Bez Kubernetesowych cudów, bez dziesiątek procesorów.

receivers:
  otlp:
    protocols:
      grpc:
      http:

processors:
  batch:

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

To jest „mało”, ale już daje retry/batching i jeden punkt, w którym później dołożysz filtrowanie lub tail sampling bez zmiany aplikacji.

Błąd 2 — Brak lub zepsuta propagacja kontekstu: każdy serwis zaczyna nowy trace

Jak to rozpoznać w Jaegerze/Tempo w 30 sekund

Najprostszy test: otwórz trace, który powinien przejść przez kilka usług. Jeśli widzisz tylko jedną usługę albo w środku flow pojawiają się „nowe korzenie”, propagacja leży.

Objawy w UI:

  • po wejściu w kolejną usługę widzisz inny trace_id,
  • spany z downstreamu nie mają parent span z upstreamu,
  • połączenia wyglądają jak kilka oddzielnych trace’y, a nie jedno drzewo.

Skutek: tracing „per serwis”, zero E2E

Taki tracing bywa gorszy niż brak tracingu, bo tworzy fałszywe poczucie, że widzisz całość. W incydencie kończy się to ręcznym sklejaniem logów i zgadywaniem, co było pierwsze. Jeśli Twoim celem jest „gdzie ginie czas w całym flow”, bez propagacji tracisz sens wdrożenia.

Najczęstsze przyczyny (i dlaczego „czasem działa”)

Najbardziej zdradliwe są przypadki, gdy w jednym miejscu propagacja działa, a w drugim znika. Wtedy trace bywa „prawie” kompletny, tylko brakuje jednego hopa — i nagle nie da się ustalić, czy to sieć, retry, czy downstream. Źródeł jest kilka, ale zwykle sprowadza się do nagłówków albo do braku poprawnego „aktywnego” kontekstu w kodzie.

Typowe winowajcy: gateway/ingress, który nie przepuszcza nagłówków (albo je normalizuje), bibliotekowy klient HTTP bez instrumentacji, ręcznie budowane requesty (np. wklejony curl w kodzie), oraz async (kolejka, taski, joby), gdzie kontekst nie przenosi się sam. Do tego dochodzi miks standardów: część usług wysyła traceparent (W3C), a część nadal polega na nagłówkach Jaegera/B3 — i bez konfiguracji propagatorów każdy „rozumie” co innego.

Checklist „naprawiam w 15 minut”

Zamiast strzelać, przejdź krótki test od brzegu do brzegu: na wejściu do systemu sprawdź, czy pojawia się nagłówek traceparent, a potem czy jest przekazywany dalej na każdym hopie. To jest nudne, ale tanie i skuteczne.

  • Na brzegu (gateway/proxy): upewnij się, że nagłówki trace nie są usuwane. Jeśli masz allow-listę nagłówków, dopisz co najmniej traceparent i tracestate (a jeśli używasz B3 — również x-b3-*).
  • W serwisie A (HTTP server): sprawdź, czy instrumentacja w ogóle czyta nagłówki i tworzy span jako dziecko, a nie nowy root. W SDK kluczowa jest konfiguracja propagatorów (najbezpieczniej: W3C Trace Context).
  • Między serwisami (HTTP client): zweryfikuj, czy klient dokleja nagłówki. Jeśli robisz request „ręcznie” i omijasz instrumentowany client, trace się urwie.
  • Async/kolejki: to osobna ścieżka — trzeba jawnie przekazać kontekst w message attributes/headers i odtworzyć go po stronie konsumenta. Bez tego każdy job wygląda jak nowy trace, nawet jeśli startował od requestu użytkownika.

Dwa krótkie przykłady z praktyki

Klasyk: Nginx/Ingress ma ustawione proxy_set_header dla kilku pól, a reszta leci domyślnie. Po zmianie konfiguracji bezpieczeństwa nagłówki trace wypadają z allow-listy i cały system zaczyna „robić nowe trace”. Na wykresach wygląda to jak nagły spadek średnich czasów (bo trace’y są krótsze), a realnie po prostu zniknęła połowa zależności.

Drugi częsty przypadek: serwis robi wywołania do downstreamu przez niestandardowy wrapper (albo własny pool/thread), który nie niesie kontekstu. Auto-instrumentacja łapie tylko standardowy klient, a wrapper omija hooki. Efekt: serwerowy span jest, klienckiego nie ma, downstream zaczyna od zera. Naprawa bywa prosta: ustandaryzować klienta HTTP albo dodać manualną instrumentację tylko w tym jednym miejscu, zamiast „ręcznie” opisywać cały system.

Jeśli po wdrożeniu widzisz trace’y, ale nie potrafisz ich skleić w jedną historię, najpierw ogarnij propagację i podstawowy kontrakt danych (service.name, route naming). Dopiero potem dokładaj kolejne spany i bajery — w przeciwnym razie doklejasz kolejne klocki do konstrukcji bez łączników.

Błąd 3 — Złe nazewnictwo i atrybuty: „wszystko jest, nic nie da się znaleźć”

Jak to rozpoznać bez grzebania w kodzie

Jeśli po tygodniu masz w backendzie „dużo danych”, a mimo to nie da się sensownie filtrować i porównywać, to zwykle problemem nie jest brak spanów, tylko brak wspólnego języka.

  • Lista usług w UI jest „poszatkowana” (to samo API ma kilka wariantów nazwy).
  • Wyszukiwanie po endpointach nie działa, bo span name jest unikatowy dla prawie każdego requestu.
  • Filtry po atrybutach są martwe: raz jest userId, raz user_id, raz enduser.id, a czasem w ogóle brak.
  • Grafana/Tempo pokazuje tysiące unikatowych nazw spanów, a Jaeger ma śmietnik w „Operation”.

Skutek: UI staje się kosztem, a nie narzędziem

Największy problem nie jest estetyczny. Bez spójnych nazw nie zrobisz szybkiej odpowiedzi na pytania typu: „czy GET /orders/:id dziś jest wolniejsze niż wczoraj?” albo „w których usługach najczęściej kończy się timeoutem?”. Zamiast diagnostyki masz ręczne przeklikiwanie się przez przypadkowe trace’e.

Co ustawić zamiast: minimum, które daje porządek

Nie trzeba tworzyć korporacyjnego standardu na 40 stron. Wystarczy kilka twardych reguł i jeden krótki słownik pól.

1) Nazwy serwisów i środowisk

  • service.name jest stałe i krótkie: orders-api, payments-worker, frontend.
  • deployment.environment trzyma środowisko: dev, stage, prod.
  • service.version daje możliwość porównania wdrożeń (SHA, tag, numer buildu).

To rozwiązuje 80% chaosu w listach serwisów i w filtrach, a koszt jest prawie zerowy.

2) Nazwy spanów: stabilne, nie „unikatowe”

Jeśli span name zawiera ID, query string, pełny URL albo dynamiczne fragmenty — przygotuj się na tysiące operacji, których nie da się agregować.

  • HTTP server: GET /orders/:id (route), nie GET /orders/123.
  • HTTP client: nazwa celu + metoda: GET inventory-service /items/:id albo chociaż host + route.
  • DB: zostaw to, co daje biblioteka, ale pilnuj, by nie logować całych zapytań z danymi wrażliwymi.

W wielu frameworkach route jest dostępny dopiero po dopasowaniu routingu. Gdy auto-instrumentacja nie potrafi jej złapać, dostajesz nazwy oparte o URL. Wtedy lepszy efekt daje mały patch (np. middleware/hook ustawiający span name na podstawie route) niż dokładanie kolejnych spanów „żeby było widać”.

3) Atrybuty: mniej, ale spójnie

Na start nie próbuj opisywać wszystkiego. Wybierz kilka pól, które naprawdę pomogą filtrować trace’e w incydencie i w codziennej diagnostyce.

  • http.route (lub ekwiwalent) – krytyczne do agregacji.
  • http.request.method, http.response.status_code – do odsiawania 5xx i porównań.
  • error.type / exception.type – jeśli biblioteka wystawia, nie walcz z tym.
  • enduser.id tylko jeśli masz to legalnie i bezpiecznie (często lepiej: zhashowany identyfikator albo stable pseudonim).

Pułapka kosztowa: atrybuty jako „magazyn danych”

Najłatwiej przepalić budżet na trace’ach przez dwa błędy: wrzucanie dużych payloadów (body, nagłówki, odpowiedzi) oraz doklejanie „wszystkiego” jako tagi. Tempo/Jaeger to przechowają — do czasu, aż zacznie boleć storage, indeksowanie i limity.

Jeśli musisz dodać kontekst diagnostyczny, trzymaj się zasady:

  • atrybuty mają być krótkie (ID, typ, status),
  • większy tekst tylko jako event i tylko tam, gdzie realnie pomaga,
  • dane wrażliwe: lepiej nie wysyłać wcale niż „może się przyda”.

Błąd 4 — Ekstremum instrumentacji: „auto zrobi wszystko” albo „manual wszędzie”

Objawy, że poszedłeś w złą stronę

  • Auto-only: widzisz spany HTTP/DB, ale brakuje Ci odpowiedzi na pytanie „co w środku zajęło czas?”. Wszystko jest jednym dużym blokiem.
  • Manual-everywhere: trace jest ogromny, a w krytycznym momencie i tak ciężko znaleźć to, co istotne. Do tego rośnie narzut i ilość danych.
  • Developerzy zaczynają omijać tracing, bo „to spowalnia” albo „za dużo roboty”.

Skutek: albo ślepa plama, albo wysoki rachunek

Auto-instrumentacja daje świetny zwrot, ale ma granice: nie rozumie Twojej logiki biznesowej. Manual instrumentacja daje precyzję, ale szybko robi się droga, jeśli próbujesz opisać wszystko. Najlepszy kompromis to potraktować auto jako szkielet, a manual jako kilka punktów kontrolnych.

Praktyczny kompromis: gdzie manual daje największy efekt

Ręczne spany opłacają się najbardziej tam, gdzie bez nich widać tylko „czarną skrzynkę”. Kilka typowych miejsc:

  • Kluczowe decyzje biznesowe: np. „wybór metody dostawy”, „walidacja koszyka”, „autoryzacja promocji”. Jedna nazwa spana, kilka atrybutów (np. typ reguły, wynik).
  • Integracje przez wrappery: własne klienty HTTP/SDK, które omijają auto-instrumentację. Dodaj span tylko na granicy wrappera.
  • Retry/circuit breaker: jeśli biblioteka nie raportuje prób, dołóż eventy (liczba prób, powód retry) zamiast budować drzewo 50 spanów.
  • Kolejki i asynchroniczność: osobne spany „publish” i „consume” + przeniesienie kontekstu w nagłówkach wiadomości.

Minimalny wzorzec manual spana (bez „przekombinowania”)

Manual span ma być krótki i konkretny: obejmij fragment, który chcesz mierzyć, dodaj 1–3 atrybuty, oznacz błędy. Resztę zostaw auto-instrumentacji.

// pseudokod: idea jest ważniejsza niż język
span = tracer.startSpan("pricing.calculate")
span.setAttribute("pricing.mode", mode)
try {
  result = calculatePricing(input)
  return result
} catch (e) {
  span.recordException(e)
  span.setStatus(ERROR)
  throw e
} finally {
  span.end()
}

Mały trik oszczędzający czas: zacznij od jednego „marker spana”

Jeśli masz długi request i nie wiesz, gdzie ginie czas, dodaj jeden span w połowie (albo dwa: „before X”, „after X”). To jest tańsze niż instrumentować wszystko, a często od razu pokazuje, czy problem jest w integracji, walidacji, czy w bazie.

Zbliżenie na cyfrowy interfejs z futurystycznymi wykresami danych w ciemności
Źródło: Pexels | Autor: Egor Komarov

Błąd 5 — Sampling ustawiony „na czuja”: albo nic nie widać, albo toniesz w danych

Jak rozpoznać, że sampling szkodzi

  • W dev/stage niby instrumentacja działa, ale w Jaegerze/Tempo pojawiają się trace’y „raz na jakiś czas”.
  • W prod liczba trace’ów eksploduje i zaczynasz przycinać na ślepo, bo storage/ingest nie wyrabia.
  • Masz głównie trace’e szybkich requestów, a wolne i błędne dziwnie „znikają” (klasyka przy nieprzemyślanym head samplingu).

Skutek: tracisz to, co najcenniejsze

Tracing ma sens wtedy, gdy łapie przypadki graniczne: wolne, błędne, z retry, z timeoutami. Zły sampling potrafi zostawić Ci piękne, typowe ścieżki, a usunąć to, co pomaga w incydencie.

Ustawienia startowe, które zwykle „dowiozą”

Na start lepiej mieć proste zasady niż perfekcyjny model.

  • dev: 100% (żeby nie mylić problemów instrumentacji z samplingiem).
  • stage: 20–50% (zależnie od ruchu; celem jest test integracji i propagacji).
  • prod: zacznij od niskiego poziomu (np. 1–10%) i dołóż reguły „zawsze bierz błędy”.

Jeśli backend i aplikacje wspierają tylko prosty sampling „na wejściu” (head), to reguła „zawsze bierz 5xx” może wymagać Collectora i tail samplingu. Wtedy sensowne jest postawienie Collectora wcześniej niż później, bo zmiana samplingu w każdej usłudze bywa irytująca.

Najtańsze kryteria tail samplingu (kiedy Collector ma największy sens)

Tail sampling jest droższy obliczeniowo, ale daje to, czego head sampling nie umie: decyzję po zobaczeniu całego trace’a. Nie musisz zaczynać od skomplikowanych reguł. Dwa proste kryteria często wystarczają:

  • status_code >= 500 albo status spanów = error → próbkuj wysoko,
  • latency powyżej progu (np. > 1s dla kluczowych endpointów) → próbkuj wysoko.

Resztę ruchu zostaw na niskim poziomie. To utrzymuje koszty i jednocześnie zachowuje materiał diagnostyczny tam, gdzie boli.

Błąd 6 — Wysyłasz trace’y, ale backend ich „nie rozumie”: OTLP/protokół/porty

Objawy: cisza w UI albo losowe braki

  • Aplikacja loguje, że „exporter działa”, ale w Jaegerze/Tempo pusto.
  • Trace’y pojawiają się tylko z części usług (inne „milczą”).
  • Widzisz błędy typu timeout/connection refused na 4317/4318, albo HTTP 404 na endpointach OTLP.

Skutek: tracisz czas na debug „sieciowy”, który nie ma nic wspólnego z tracingiem

To jest jeden z najbardziej frustrujących etapów, bo objawy są podobne do problemów z samplingiem albo instrumentacją. A to bywa po prostu zły port lub zły protokół.

Najkrótsza ścieżka do diagnozy: trzy pytania

  • Czy wysyłasz OTLP gRPC czy OTLP HTTP? Standardowe porty to odpowiednio 4317 (gRPC) i 4318 (HTTP). Zły wybór wygląda jak „nie działa nic”.
  • Czy backend/Collector faktycznie nasłuchuje na tym protokole? W Collectorze musisz mieć w receiverze włączony właściwy protocols.
  • Czy po drodze nie stoi proxy, które nie lubi gRPC? Jeśli masz load balancer/ingress, który nie wspiera gRPC dobrze, przełączenie na OTLP/HTTP potrafi „magicznie” rozwiązać problem.

Minimalne, bezpieczne ustawienie eksportu (wariant budżetowy)

Jeśli nie chcesz tracić dnia na walkę z gRPC przez proxy, prosty wariant to OTLP/HTTP do Collectora, a dopiero Collector gada dalej z Tempo/Jaegerem w sposób dla Ciebie przezroczysty.

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp]

Checklista szybkiej kontroli jakości (po uruchomieniu pierwszych trace’ów)

  • Jedno żądanie = jeden trace przechodzący przez wszystkie usługi (brak „nowych korzeni” po drodze).
  • service.name jest stałe, a deployment.environment oddziela środowiska.
  • Nazwy spanów HTTP są stabilne (route), bez ID i query string.
  • Nie wysyłasz payloadów ani wrażliwych danych jako atrybuty.
  • Sampling w dev jest 100%, a w prod masz rozsądny limit i plan na „błędy/wolne zawsze”.
  • Masz co najmniej jeden punkt (Collector lub konfiguracja w app), gdzie łatwo zmienisz eksport i sampling bez dotykania każdej usługi.

Błąd 7 — Brak Collectora jako „punktu wspólnego”: każda usługa konfigurowana inaczej

Tablet z wykresami analitycznymi na biurku obok telefonu i kawy
Źródło: Pexels | Autor: AS Photography

Objawy: działa, ale tylko do pierwszej zmiany

  • Jedna usługa wysyła do Jaegera, druga do Tempo, trzecia przez OTLP/HTTP, czwarta gRPC — a Ty nie pamiętasz, czemu.
  • Zmiana endpointu eksportu wymaga merge requestów w kilku repo, a rollout trwa dłużej niż sam problem.
  • Masz różne zasady samplingu w zależności od języka/frameworka (bo „tak było najprościej”).
  • W incydencie ktoś dodał debug logi do OTel SDK w jednej usłudze, ale reszta nadal „milczy”.

Skutek: rośnie koszt utrzymania, a spójność znika

Bez Collectora „płacisz” za każdą decyzję konfiguracyjną wiele razy. Na początku to kusi, bo direct export do backendu działa w 15 minut. Potem dochodzą: rotacje endpointów, różne protokoły, zmiana back-endu, potrzeba filtrowania PII, tail sampling, retry/buffering. I nagle okazuje się, że obserwowalność jest rozproszona jak kod legacy.

Co zrobić lepiej: Collector jako minimalny „hub”, nie jako projekt na sprint

Budżetowy wariant to jeden Collector (albo per namespace), który przyjmuje OTLP i wysyła dalej. Nawet jeśli na start nie używasz tail samplingu, to zyskujesz:

  • jeden endpoint do ustawienia w aplikacjach,
  • centralne retry/batching,
  • możliwość dodania filtrowania atrybutów bez releasu usług,
  • łatwiejszą migrację Jaeger ↔ Tempo.

Minimalna konfiguracja Collectora, która „nie boli”

To jest wariant startowy: OTLP in + batch + eksport do Jaegera/Tempo. Bez egzotyki, bez dodatkowych procesorów.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    send_batch_size: 512
    timeout: 5s

exporters:
  # wybierz jeden docelowy backend (przykład: Tempo OTLP gRPC)
  otlp/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/tempo]

Jeśli używasz Jaegera w nowszych wersjach, najczęściej i tak skończysz na OTLP. Gdy Jaeger jest starszy i „chce” jaeger thrift/grpc, to Collector może robić za tłumacza protokołów — i to jest kolejny argument, żeby go mieć.

Błąd 8 — Brak korelacji trace ↔ logi: w incydencie i tak wracasz do grep

Objawy: trace są ładne, ale nie pomagają w debugowaniu

  • Widzisz wolny span, ale nie wiesz, dlaczego — bo szczegóły są tylko w logach, a nie umiesz ich znaleźć dla tego requestu.
  • Masz błąd 500, ale w logach jest tysiąc wpisów z tego samego czasu i brak wspólnego identyfikatora.
  • Developerzy mówią: „tracing fajny, ale i tak muszę szukać po request-id”.

Skutek: tracing jest „osobnym światem”, a nie narzędziem do akcji

Bez korelacji, trace kończy się na „tu bolało”, a logi zaczynają od „nie wiem, który wpis”. Najtańszy upgrade to dołożyć do logów dwa pola: trace_id i span_id. Nie trzeba przepisywać całego logowania ani wdrażać nowego stacku.

Najprostsze podejście: wstrzyknij trace_id/span_id do kontekstu loggera

Mechanizm zależy od języka, ale cel jest identyczny: gdy jest aktywny span, logger ma automatycznie dopisać identyfikatory. Dwie praktyczne zasady:

  • Nie loguj pełnego kontekstu OTel jako JSON „bo może się przyda” — kończy się szumem i kosztami.
  • Jeśli masz już request_id, zostaw go, ale niech trace_id będzie pierwszoplanowy (bo działa end-to-end).

W Grafanie (Loki + Tempo) to zwykle od razu daje kliknięcie „view trace” z logów lub „view logs” z trace — pod warunkiem, że pola są ustandaryzowane. W Jaegerze korelację najczęściej robisz przez wyszukiwanie w logach po trace_id (albo przez integrację narzędziową, jeśli ją masz).

Błąd 9 — Zbierasz za dużo szczegółów i wpuszczasz PII: tracing staje się ryzykiem

Objawy: „debug jest super”, a potem przychodzi audyt

  • W atrybutach spanów lądują e-maile, numery telefonów, tokeny, pełne adresy, payloady z formularzy.
  • Ktoś dodał http.request.body jako atrybut „tylko na chwilę” i zostało.
  • Trace’y są dostępne szerzej niż baza danych, bo „to tylko observability”.

Skutek: dane wrażliwe rozchodzą się szybciej niż myślisz

Tracing jest świetny, ale bywa kopiowany do wielu miejsc: storage, backupy, eksporty, środowiska testowe. Każdy niepotrzebny kawałek PII zwiększa ryzyko i utrudnia retencję.

Laptop z dashboardem analitycznym do monitorowania danych w czasie rzeczywistym
Źródło: Pexels | Autor: Atlantic Ambience

Tańszy i bezpieczniejszy wzorzec: identyfikatory zamiast treści

Zamiast przechowywać dane osobowe, trzymaj w atrybutach krótkie, stabilne identyfikatory:

  • user.id jako wewnętrzne ID (nie e-mail),
  • order.id, cart.id, tenant.id,
  • error.type / validation.rule zamiast pełnych opisów wejścia.

Jeśli koniecznie potrzebujesz „kawałka” danych do diagnozy, rozważ hash (z solą po stronie aplikacji) albo event z krótką etykietą. A pełne treści zostaw w systemie, który jest do tego przeznaczony (np. bezpieczne logi z kontrolą dostępu i retencją).

Collector jako bezpiecznik: filtruj zanim dane wyjdą dalej

Praktyczny kompromis: nawet jeśli nie kontrolujesz jeszcze wszystkich bibliotek w aplikacjach, możesz odcinać ryzyko w Collectorze. Przykład: usunięcie wybranych atrybutów, które „czasem ktoś dorzuca”.

processors:
  attributes/sanitize:
    actions:
      - key: http.request.header.authorization
        action: delete
      - key: http.request.body
        action: delete
      - key: user.email
        action: delete

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/sanitize, batch]
      exporters: [otlp/tempo]

To nie zastąpi higieny w kodzie, ale potrafi uratować dzień, kiedy zespół dopiero uczy się „co wolno w spanie”.

Błąd 10 — Ignorujesz asynchroniczność: trace’y „urwane” na kolejkach i jobach

Objawy: front widzi wolno, backend „nie ma nic”

  • HTTP span kończy się po publish do kolejki, a dalej już nie ma nic — mimo że praca trwa.
  • Consumer tworzy nowy trace zamiast dołączyć do istniejącego.
  • Masz spany, ale bez relacji parent-child pomiędzy usługami, które realnie są połączone wiadomością.

Skutek: end-to-end znika dokładnie tam, gdzie najczęściej boli

Kolejki i joby to typowe miejsce, gdzie latencja „paruje” z perspektywy HTTP. Jeśli kontekstu nie przeniesiesz w nagłówkach wiadomości, tracing pokaże tylko pół historii.

Minimalny wzorzec: dwa spany i propagacja w message headers

  • Po stronie producenta: span messaging.publish (często auto-instrumentacja to ma, ale nie zawsze).
  • W wiadomości: wstrzyknięcie kontekstu (W3C traceparent i ewentualnie baggage).
  • Po stronie konsumenta: ekstrakcja kontekstu i span messaging.consume jako child tego kontekstu.
// pseudokod: producent
ctx = currentContext()
propagator.inject(ctx, message.headers)
queue.publish(message)

// pseudokod: konsument
parentCtx = propagator.extract(message.headers)
span = tracer.startSpan("messaging.consume", parentCtx)
try { handle(message) } finally { span.end() }

Jeśli masz tylko czas na jedną poprawkę: dopilnuj propagacji traceparent. Bez tego nawet najlepsze spany manualne nie skleią historii.

Co sprawdzić przed wyborem Jaeger vs Grafana Tempo (żeby nie przepłacić migracją)

Decyzja jest mniej o „UI”, a bardziej o tym, jak chcesz to utrzymywać i integrować:

  • Czy chcesz jeden interfejs dla metryk/logów/trace? Jeśli i tak siedzisz w Grafanie (Prometheus/Loki), Tempo zwykle daje najkrótszą ścieżkę do korelacji.
  • Czy potrzebujesz prostoty „klikam i działa” w dev? Jaeger bywa najszybszy do lokalnego odpalenia i ręcznego grzebania w trace.
  • Jak wygląda retencja i koszty storage? Tempo często idzie w obiektowy storage; Jaeger zależy od wybranego backendu (Elastic/Cassandra/Badger itd.).
  • Czy masz już OTLP wszędzie? Jeśli nie, Collector jako warstwa zgodności ułatwia zmianę backendu bez dotykania aplikacji.

Jeśli nie masz twardych wymagań: zacznij od tego, co już masz w firmie. Najdroższe jest „jeszcze jedno narzędzie do utrzymania”, nie sam wybór trace UI.

Najczęściej zadawane pytania (FAQ)

Co jest potrzebne minimalnie, żeby zobaczyć trace’y w Jaegerze albo Grafanie Tempo?

Minimum to działająca instrumentacja w aplikacji (auto lub ręczna) i exporter OTLP wysyłający dane do backendu. Kluczowe są dwie rzeczy: poprawny endpoint/port oraz sensowny service.name, żeby dało się w ogóle odróżnić usługi.

Jeśli cel jest „zobaczyć cokolwiek dzisiaj”, najprościej iść w wariant: aplikacja → OTLP exporter → Jaeger/Tempo. Gdy trace’y się pojawią, dopiero wtedy warto dokładać Collectora i porządkować resztę.

Jaeger czy Grafana Tempo — co wybrać na start?

Jeśli chcesz szybko odpalić UI i klikać trace’y bez budowania całego stacku, Jaeger zwykle wygrywa czasem startu. To częsty wybór do lokalnego dev/POC i prostych systemów.

Tempo ma więcej sensu, gdy i tak żyjesz w Grafanie i chcesz płynnej korelacji: metryki (Prometheus/Mimir), logi (Loki) i trace’y w jednym miejscu. W praktyce to oszczędza czas w diagnostyce bardziej niż sama zmiana „przeglądarki trace”.

Czy muszę stawiać OpenTelemetry Collectora, czy mogę wysyłać trace’y bezpośrednio?

Możesz wysyłać bezpośrednio i często to jest najlepszy „budżetowy” start: mniej komponentów, mniej konfiguracji, mniej miejsc do zepsucia. Minus pojawia się później: zmiana backendu albo protokołu oznacza grzebanie w konfiguracji każdej aplikacji.

Collector opłaca się szybko, gdy masz kilka usług i chcesz stabilności oraz kontroli: batch/retry, buforowanie, tail sampling, filtrację atrybutów (np. PII) i prostsze przełączanie docelowego storage.

Dlaczego mam „urwane trace’y” albo każdy serwis pokazuje osobny trace?

Najczęściej to problem z propagacją kontekstu: identyfikatory trace/span nie przechodzą między usługami. Efekt wygląda tak, jakby request „resetował się” na granicy serwisu, gatewaya albo przy wywołaniach async.

Typowe miejsca, gdzie to pęka: API gateway/reverse proxy (nagłówki są ucinane lub nieprzepuszczane), niestandardowe klienty HTTP, kolejki/async (sama auto-instrumentacja HTTP nie obejmuje E2E). Wtedy trzeba dopilnować propagacji (np. W3C Trace Context) i/lub dodać ręczne spany w punktach, gdzie kontekst znika.

OTLP gRPC czy OTLP HTTP — który protokół wybrać i czemu „nie działa”?

Najczęstszy problem to mismatch: aplikacja wysyła OTLP HTTP na port gRPC (albo odwrotnie). Objaw: brak trace’y w backendzie, a w logach komunikaty o błędnym protokole albo połączeniu.

Jeśli nie masz silnych powodów, wybierz jeden standard w całym środowisku (np. OTLP gRPC) i trzymaj się go konsekwentnie. Gdy w grę wchodzi proxy lub ograniczenia sieciowe, OTLP HTTP bywa prostszy do przepchnięcia, ale wymaga pilnowania endpointów.

Auto-instrumentation wystarczy, czy muszę pisać ręczne spany?

Auto-instrumentation daje szybki efekt, ale potrafi narobić szumu: dużo spanów o niskiej wartości i mało informacji o tym, co naprawdę jest „krytycznym krokiem” w logice biznesowej. Ręczne spany są nieuniknione, gdy chcesz widzieć domenowe etapy (np. „walidacja koszyka”, „kalkulacja rabatów”) albo gdy masz async/queue i musisz skleić kontekst end-to-end.

Pragmatyczny kompromis: auto-instrumentation na start + kilka ręcznych spanów w 1–2 krytycznych ścieżkach. To zwykle daje najlepszy stosunek efektu do wysiłku.

Jak ustawić sampling, żeby nie utopić się w kosztach i narzucie?

W dev często ma sens 100% trace (łatwiej debugować), ale w prod trzeba ciąć, bo inaczej szybko rośnie koszt i narzut. Najprostszy jest head sampling (decyzja na początku trace) — tani i łatwy, ale może uciąć te trace’y, które akurat były „ciekawe”.

Jeśli chcesz zachowywać tylko wolne trace’y albo te z błędami, potrzebujesz zwykle tail sampling, a to najwygodniej robić w Collectorze. Rozsądny kolejny krok: zacząć od prostego head sampling w aplikacjach, a gdy pojawi się potrzeba selekcji „tylko problemy”, przenieść sampling do Collectora.

Jak nie wrzucić PII albo sekretów do trace’ów (atrybuty, URL-e, nagłówki)?

Największe ryzyko jest w atrybutach spanów: pełne URL-e z parametrami, payloady, nagłówki typu Authorization, identyfikatory użytkownika wprost. To potem ląduje w storage i bywa trudne do wyczyszczenia.

Najtańsza higiena na start: nie zapisuj request/response body w atrybutach i unikaj dokładania atrybutów „na szybko”. Jeśli musisz coś logować do trace, preferuj wartości zredukowane (np. http.route zamiast pełnego URL) i wstaw filtrację/redakcję w Collectorze, gdy trace’y wychodzą poza środowisko developerskie.

Poprzedni artykułOptymalizacja Windows 11 bez zbędnych programów i usług
Andrzej Kowalczyk
Andrzej Kowalczyk pisze o sieciach, 5G i infrastrukturze, łącząc teorię z doświadczeniem wdrożeniowym. Interesują go parametry, które naprawdę wpływają na jakość usług: przepustowość, opóźnienia, stabilność i odporność na zakłócenia. W artykułach korzysta z pomiarów, analiz konfiguracji oraz dokumentacji standardów, a złożone tematy tłumaczy na przykładach z życia. Podkreśla znaczenie planowania, segmentacji i monitoringu, a przy rekomendacjach jasno wskazuje założenia oraz ograniczenia środowiska.