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
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”.
| Kryterium | Jaeger | Grafana Tempo |
|---|---|---|
| Szybkość startu (lokal/dev) | Bardzo wysoka | Wysoka, ale zwykle dochodzi Grafana + datasource |
| Workflow w Grafanie | Integracja możliwa, ale nie „native” | Naturalnie „u siebie” (trace + metrics + logs) |
| Kiedy najczęściej wygrywa | Prosty tracing, mało usług, szybkie demo/POC | Gdy i tak używasz Grafany i chcesz korelacji |
| Typowy błąd na starcie | Wysyłka w złym protokole/porcie, chaos w nazwach | Zamieszanie 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.

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
traceparentitracestate(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, razuser_id, razenduser.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), nieGET /orders/123. - HTTP client: nazwa celu + metoda:
GET inventory-service /items/:idalbo 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.

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) i4318(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

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.bodyjako 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ę.

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
traceparenti ewentualniebaggage). - 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.






