Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Distributed Tracing z OpenTelemetry — Szlaki Kurierskie Imperium

Imperium Rzymskie posiadało rozbudowany system cursus publicus — państwowej poczty kurierskiej. Każda depesza miała znacznik identyfikacyjny i była śledzona od nadawcy do odbiorcy, przez wszystkie stacje pośrednie. W aplikacjach mikroservisowych, tę samą rolę pełni distributed tracing — śledzenie żądań przez wiele serwisów.

Problem: zagubione żądanie

W systemie monolitycznym łatwo prześledzić przepływ żądania — wszystko jest w jednym procesie. Ale w architekturze mikroservisowej żądanie przechodzi przez wiele serwisów:

1// Żądanie przechodzi przez wiele serwisów
2const requestFlow = {
3  krok1: 'Użytkownik -> API Gateway',
4  krok2: 'API Gateway -> Auth Service',
5  krok3: 'Auth Service -> User Service (weryfikacja)',
6  krok4: 'API Gateway -> Legion Service (logika)',
7  krok5: 'Legion Service -> MongoDB (zapis)',
8  krok6: 'Legion Service -> Redis (cache)',
9  krok7: 'Legion Service -> Notification Service (powiadomienie)',
10
11  problem: 'Które żądanie jest wolne? Gdzie jest wąskie gardło?',
12  rozwiazanie: 'Distributed Tracing — śledź żądanie przez WSZYSTKIE serwisy',
13};

Czym jest Distributed Tracing?

Distributed tracing to technika śledzenia żądania przez wiele serwisów. Każde żądanie otrzymuje unikalny identyfikator (trace ID), który jest propagowany do każdego serwisu:

1// Kluczowe pojęcia
2interface TracingConcepts {
3  trace: 'Pełna podróż żądania (od klienta do odpowiedzi)';
4  span: 'Pojedyncza operacja w ramach trace (np. zapytanie DB)';
5  traceId: 'Unikalny ID całego żądania (jak numer depeszy)';
6  spanId: 'Unikalny ID pojedynczej operacji';
7  parentSpanId: 'ID operacji nadrzędnej (łączy spany w drzewo)';
8  context: 'Propagacja — przekazywanie traceId między serwisami';
9}
10
11// Przykład drzewa spanów
12const traceExample = {
13  traceId: 'abc-123-def-456',
14  rootSpan: {
15    name: 'POST /legiones',
16    duration: '250ms',
17    children: [
18      { name: 'auth.verify', duration: '15ms' },
19      {
20        name: 'legion.create',
21        duration: '180ms',
22        children: [
23          { name: 'mongodb.insert', duration: '45ms' },
24          { name: 'redis.set', duration: '5ms' },
25          { name: 'notification.send', duration: '120ms' },
26        ],
27      },
28    ],
29  },
30};

OpenTelemetry — Standard obserwacji

OpenTelemetry (OTel) to otwarty standard zbierania danych telemetrycznych (traces, metrics, logs). Jest wspierany przez CNCF i wszystkich głównych dostawców chmury:

1# Instalacja OpenTelemetry SDK dla NestJS
2yarn add @opentelemetry/sdk-node
3yarn add @opentelemetry/auto-instrumentations-node
4yarn add @opentelemetry/exporter-trace-otlp-http
5yarn add @opentelemetry/resources
6yarn add @opentelemetry/semantic-conventions

Konfiguracja OpenTelemetry w NestJS

1// tracing/tracing.ts — WAŻNE: załaduj PRZED importem NestJS!
2import { NodeSDK } from '@opentelemetry/sdk-node';
3import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
4import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
5import { Resource } from '@opentelemetry/resources';
6import {
7  ATTR_SERVICE_NAME,
8  ATTR_SERVICE_VERSION,
9} from '@opentelemetry/semantic-conventions';
10
11const sdk = new NodeSDK({
12  resource: new Resource({
13    [ATTR_SERVICE_NAME]: 'roman-imperium-api',
14    [ATTR_SERVICE_VERSION]: '1.0.0',
15  }),
16  traceExporter: new OTLPTraceExporter({
17    url: process.env.OTEL_EXPORTER_URL || 'http://jaeger:4318/v1/traces',
18  }),
19  instrumentations: [
20    getNodeAutoInstrumentations({
21      '@opentelemetry/instrumentation-http': { enabled: true },
22      '@opentelemetry/instrumentation-express': { enabled: true },
23      '@opentelemetry/instrumentation-mongoose': { enabled: true },
24    }),
25  ],
26});
27
28sdk.start();
29console.log('OpenTelemetry zainicjalizowany');
30
31// Graceful shutdown
32process.on('SIGTERM', () => {
33  sdk.shutdown().then(() => console.log('Tracing zamkniety'));
34});

Ładowanie przed aplikacją

1// main.ts
2import './tracing/tracing';  // MUSI być pierwszym importem!
3import { NestFactory } from '@nestjs/core';
4import { AppModule } from './app.module';
5
6async function bootstrap() {
7  const app = await NestFactory.create(AppModule);
8  await app.listen(4000);
9}
10bootstrap();

Ręczne tworzenie spanów

Auto-instrumentacja obsługuje HTTP i bazę danych, ale możemy tworzyć własne spany dla logiki biznesowej:

1import { Injectable } from '@nestjs/common';
2import { trace, SpanStatusCode } from '@opentelemetry/api';
3
4@Injectable()
5export class LegionService {
6  private tracer = trace.getTracer('legion-service');
7
8  async recruitLegionary(dto: CreateLegionaryDto) {
9    return this.tracer.startActiveSpan('legion.recruit', async (span) => {
10      try {
11        span.setAttribute('legionary.name', dto.name);
12        span.setAttribute('legionary.rank', dto.rank);
13
14        // Każda operacja to osobny span
15        const legionary = await this.tracer.startActiveSpan(
16          'mongodb.create',
17          async (dbSpan) => {
18            const result = await this.legionRepository.create(dto);
19            dbSpan.setAttribute('db.collection', 'legionaries');
20            dbSpan.end();
21            return result;
22          },
23        );
24
25        span.setStatus({ code: SpanStatusCode.OK });
26        return legionary;
27      } catch (error) {
28        span.setStatus({
29          code: SpanStatusCode.ERROR,
30          message: error.message,
31        });
32        span.recordException(error);
33        throw error;
34      } finally {
35        span.end();
36      }
37    });
38  }
39}

Context Propagation — Przekazywanie kontekstu

Trace ID jest automatycznie przekazywany między serwisami przez nagłówki HTTP:

1// Nagłówki propagacji (W3C Trace Context)
2const traceHeaders = {
3  'traceparent': '00-abc123def456-span789-01',
4  // version-traceId-parentSpanId-flags
5};
6
7// Auto-instrumentacja automatycznie dodaje te nagłówki
8// Nie musisz tego robić ręcznie!

Jaeger — Wizualizacja tras

Jaeger to popularne narzędzie do wizualizacji distributed traces:

1# docker-compose.yml — Jaeger
2services:
3  jaeger:
4    image: jaegertracing/all-in-one:latest
5    environment:
6      - COLLECTOR_OTLP_ENABLED=true
7    ports:
8      - "16686:16686"    # Jaeger UI
9      - "4318:4318"      # OTLP HTTP

Korelacja logów z trasami

Łączenie logów z trace ID pozwala na pełną diagnostykę:

1// Dodanie traceId do logów
2import { context, trace } from '@opentelemetry/api';
3
4function getTraceId(): string {
5  const span = trace.getSpan(context.active());
6  return span?.spanContext().traceId || 'no-trace';
7}
8
9// W loggerze
10this.logger.log({
11  message: 'Legionista zrekrutowany',
12  traceId: getTraceId(),
13  legionaryId: 'leg_42',
14});
15
16// Teraz w Jaeger widzisz trace, a w logach — szczegóły

Distributed tracing to system cursus publicus naszego Imperium — każde żądanie jest śledzone od początku do końca, przez wszystkie serwisy. Dzięki OpenTelemetry i Jaeger widzimy dokładnie, gdzie żądanie spędza czas i gdzie powstają problemy.

Vai a CodeWorlds