Metryki mówią, że dziewięćdziesiąty piąty percentyl czasu odpowiedzi wzrósł z 200 ms do dwóch sekund. Wiesz że jest wolno - ale nie gdzie. Żądanie przechodzi przez bramę uwierzytelniającą, serwis legionów, bazę i jeszcze zewnętrzne API płatności. Które z tych ogniw zabiera te dwie sekundy?
Rzym miał na to spis stacji. Kurier wiozący depeszę z Galii do Rzymu meldował się w każdej mansio, a na końcu z tych meldunków dało się odczytać, gdzie stracił dzień. Distributed tracing robi dokładnie to samo z żądaniem.
Dwa pojęcia, które trzeba rozdzielić od razu.
Trace to cała droga jednego żądania - od wejścia do wyjścia, przez wszystkie usługi. Span to pojedyncza operacja w ramach trace - jedno zapytanie do bazy, jedno wywołanie zewnętrznego API, jedna metoda serwisu.
Span nie jest więc identyfikatorem całego żądania (tym jest trace ID), nie jest formatem eksportu danych ani narzędziem do wizualizacji. To jeden odcinek drogi, z własnym czasem rozpoczęcia i zakończenia.
Spany układają się w drzewo. Dla żądania tworzącego legion wygląda ono tak, od korzenia w głąb:
POST /legiones - root span, całe żądanie HTTP.auth.verify - weryfikacja tokena.legion.create - logika biznesowa.mongodb.insert - zapis do bazy.Zagnieżdżenie jest tu informacją samą w sobie: skoro
mongodb.insert leży wewnątrz legion.create, to jego czas wlicza się w czas rodzica. Gdy root span trwa dwie sekundy, a mongodb.insert półtorej - wiesz już, gdzie szukać.OpenTelemetry uruchamiamy przed aplikacją, w osobnym pliku ładowanym najwcześniej:
1const sdk = new NodeSDK({
2 resource: new Resource({
3 [SemanticResourceAttributes.SERVICE_NAME]: 'legion-api',
4 }),
5 traceExporter: new OTLPTraceExporter({
6 url: 'http://jaeger:4318/v1/traces',
7 }),
8 instrumentations: [getNodeAutoInstrumentations()],
9});
10
11sdk.start();
nadaje nazwę usłudze - to po niej rozpoznasz swoje spany wśród spanów z pozostałych serwisów. resource
wskazuje, dokąd wysyłać dane; tutaj do Jaegera, narzędzia, które rysuje z nich oś czasu.traceExporter
to najciekawszy element. instrumentations
getNodeAutoInstrumentations() włącza automatyczne śledzenie popularnych bibliotek - HTTP, Express, MongoDB, Redis. Nie dopisujesz ani linii w kodzie serwisów, a spany dla zapytań do bazy powstają same.Kolejność ma znaczenie: SDK musi wystartować, zanim zaimportujesz Express czy sterownik bazy. Automatyczna instrumentacja działa przez podmianę tych bibliotek w locie, a nie podmieni czegoś, co już zostało załadowane.
Auto-instrumentacja pokrywa infrastrukturę, ale nie wie nic o Twojej dziedzinie. Żeby zobaczyć w drzewie
legion.create, tworzysz span sam:1@Injectable()
2export class LegionService {
3 private tracer = trace.getTracer('legion-service');
4
5 async create(dto: CreateLegionDto) {
6 return this.tracer.startActiveSpan('legion.create', async (span) => {
7 try {
8 span.setAttribute('legion.name', dto.name);
9 span.setAttribute('legion.rank', dto.rank);
10
11 const legion = await this.repo.save(dto);
12
13 span.setStatus({ code: SpanStatusCode.OK });
14 return legion;
15 } catch (error) {
16 span.recordException(error);
17 span.setStatus({ code: SpanStatusCode.ERROR });
18 throw error;
19 } finally {
20 span.end();
21 }
22 });
23 }
24}Tracer tworzysz raz, jako pole klasy - kolejność zapisu to
, private tracer
, =
.trace.getTracer('legion-service')
W środku dzieje się pięć rzeczy.
startActiveSpan otwiera span i czyni go aktywnym, dzięki czemu wszystko, co powstanie w środku - także spany z auto-instrumentacji - zagnieździ się pod nim automatycznie. setAttribute dokłada dane wyszukiwalne później w Jaegerze; to tutaj wpisujesz identyfikatory, po których będziesz szukać konkretnego przypadku. setStatus oznacza wynik, recordException zapisuje wyjątek wraz ze śladem stosu.Najważniejsze jest
w bloku span.end()
. Span bez zakończenia nie trafi do eksportu wcale - i to jest ta sama symetria, którą znasz z połączeń i haków testowych: co otworzyłeś, musisz zamknąć, także wtedy gdy poleciał wyjątek.finally
Zostało pytanie, jak spany z różnych usług trafiają do jednego drzewa. Odpowiada za to context propagation - automatyczne przekazywanie trace ID między serwisami przez nagłówki HTTP.
Gdy serwis A woła serwis B, instrumentacja dokłada do żądania nagłówek
traceparent z identyfikatorem trace'u i bieżącego spanu. Serwis B odczytuje go i tworzy swoje spany jako dzieci tamtego. Nikt niczego nie kopiuje ręcznie, nie synchronizuje baz ani nie przenosi plików konfiguracyjnych.Stąd bierze się cała wartość tracingu w systemie rozproszonym: jedno drzewo obejmuje wszystkie usługi, więc dwie sekundy da się przypisać konkretnemu ogniwu, nawet gdy leży ono w cudzym serwisie.
Kurierzy meldują się na każdej stacji:
POST /legiones → auth.verify → legion.create → mongodb.insert,NodeSDK konfigurują trzy rzeczy: resource z nazwą usługi, traceExporter z adresem odbiorcy, instrumentations,getNodeAutoInstrumentations() śledzi popularne biblioteki bez zmian w kodzie - ale SDK musi wystartować przed ich zaimportowaniem,private tracer = trace.getTracer('nazwa'),startActiveSpan zagnieżdża wszystko, co powstanie w środku; setAttribute dokłada dane do wyszukiwania, setStatus i recordException opisują wynik,span.end() należy do finally - span bez zakończenia nie trafi do eksportu,W następnej lekcji zbierzesz cały ten moduł w jeden projekt wdrożeniowy. A na razie zapamiętaj: metryka pokazuje wykres, trace pokazuje trasę - i dopiero na niej widać, przy której stacji kurier stracił dzień.