NestJS: backend-API:er för företag · Lektion

Distribuerad tracing med OpenTelemetry

Instrumentera controllers, providers och HTTP-klienter så att korrelerade spans skapas mellan tjänster.

Lektion 3 av 413 steg

Distribuerad tracing med OpenTelemetry är en gratis lektion i NestJS: backend-API:er för företag på CoddyKit. Detta är lektion 3 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för NestJS: backend-API:er för företag, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.

Varför distribuerad tracing

I en mikrotjänstmiljö kan ett enda användaranrop passera en gateway, en ordertjänst, en betaltjänst och ett HTTP-API från tredje part. När latensen ökar kan loggar ensamma inte visa vilket steg som var långsamt.

Distribuerad tracing fogar samman dessa steg. Varje arbetsenhet blir en span; spans som länkas med ett gemensamt trace_id bildar en enda övergripande trace.

  • trace_id — samma värde i alla tjänster för ett och samma anrop
  • span_id — unikt för varje operation
  • parent_span_id — hur spans fogas samman till ett träd

OpenTelemetry (OTel) är den leverantörsoberoende standarden för att skapa och vidarebefordra dessa spans, vilket vi kopplar in i NestJS.

En spans uppbyggnad

En span är helt enkelt ett typat objekt som beskriver en operation vid en viss tidpunkt. Innan ni arbetar med NestJS är det bra att modellera vad OTel faktiskt skickar ut. Nedan visas en enkel TypeScript-skiss av de fält som SDK:t fyller i.

Observera kind: SERVER-spans representerar inkommande anrop och CLIENT-spans representerar utgående anrop. Att korrelera en CLIENT-span i tjänst A med en SERVER-span i tjänst B är precis vad kontextpropagering möjliggör.

type SpanKind = 'SERVER' | 'CLIENT' | 'INTERNAL';

interface Span {
  traceId: string;
  spanId: string;
  parentSpanId?: string;
  name: string;
  kind: SpanKind;
  startTimeMs: number;
  endTimeMs: number;
  attributes: Record<string, string | number | boolean>;
}

function durationMs(span: Span): number {
  return span.endTimeMs - span.startTimeMs;
}

const span: Span = {
  traceId: '4bf92f3577b34da6a3ce929d0e0e4736',
  spanId: '00f067aa0ba902b7',
  name: 'GET /orders/:id',
  kind: 'SERVER',
  startTimeMs: 1000,
  endTimeMs: 1042,
  attributes: { 'http.method': 'GET', 'http.route': '/orders/:id', 'http.status_code': 200 },
};

console.log(`${span.name} took ${durationMs(span)}ms`);

Installera OTel SDK

För en NestJS-tjänst behöver ni tre lager av OTel-paket:

  • @opentelemetry/sdk-node — Node-SDK:t och livscykelhanteringen
  • @opentelemetry/auto-instrumentations-node — kodfria ändringar för HTTP, Express, Nest, pg, ioredis med mera
  • En exporter, till exempel @opentelemetry/exporter-trace-otlp-http, för att skicka spans till en collector eller backend (Jaeger, Tempo, Honeycomb)

Auto-instrumentering skapar redan korrelerade SERVER- och CLIENT-spans för inkommande HTTP-anrop och utgående fetch-/axios-anrop. Manuella spans (i senare scener) lägger till verksamhetsbetydelse.

npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

Starta tracing före Nest

Den viktigaste regeln är: starta OTel SDK innan någon applikationsmodul importeras. Auto-instrumentering fungerar genom att monkey-patcha moduler som http när de require-laddas. Om Nest laddas först missar patcharna modulen.

Lägg SDK:t i en egen tracing.ts och importera den allra längst upp i main.ts, eller förladda den med node --require ./dist/tracing.js.

// tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

export const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'orders-service',
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? '0.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().finally(() => process.exit(0));
});

Koppla in det i main.ts

Eftersom ordningen för importer med sidoeffekter spelar roll måste tracing-importen vara den första satsen — ovanför Nest-factoryn och till och med ovanför AppModule. ES-modulernas hoisting respekterar fortfarande den fysiska ordningen för SDK:ts sidoeffekt från sdk.start(), så länge den här filen laddas först.

Ett säkrare alternativ som undviker all oklarhet kring hoisting är preload-flaggan node --require ./dist/tracing.js dist/main.js i startskriptet.

// main.ts
import './tracing'; // MUST be first
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

Så propageras kontext mellan tjänster

Spans blir ett enda spår endast om trace_id följer med mellan tjänsterna. OTel gör detta med W3C-standarden Trace Context, genom att injicera en HTTP-header med namnet traceparent i utgående CLIENT-spans och extrahera den i inkommande SERVER-spans.

Headern ser ut så här:

  • traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
  • Format: version-traceId-parentSpanId-flags

Med automatisk instrumentering sker detta automatiskt för HTTP. Det viktiga kravet är att den aktiva kontexten följer med genom den asynkrona koden, så att det utgående anropet vet vilket spår det tillhör.

function parseTraceparent(header: string) {
  const [version, traceId, parentId, flags] = header.split('-');
  return { version, traceId, parentId, sampled: (parseInt(flags, 16) & 1) === 1 };
}

const h = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const ctx = parseTraceparent(h);
console.log(ctx.traceId);
console.log('sampled:', ctx.sampled);

Manuella spans i en provider

Automatisk instrumentering ger er spans på HTTP-nivå, men affärslogik ("reservera lager", "debitera kort") bör ha egna spans. Hämta en Tracer och omslut operationen med startActiveSpan så att underordnade spans kapslas korrekt under den aktuella SERVER-spannen.

startActiveSpan gör spannen aktiv under hela callbackens körning. Det innebär att alla kapslade spans eller utgående HTTP-anrop automatiskt blir dess underordnade spans.

// orders.service.ts
import { Injectable } from '@nestjs/common';
import { trace, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('orders-service');

@Injectable()
export class OrdersService {
  async reserveInventory(orderId: string, sku: string, qty: number) {
    return tracer.startActiveSpan('reserveInventory', async (span) => {
      span.setAttribute('order.id', orderId);
      span.setAttribute('inventory.sku', sku);
      span.setAttribute('inventory.qty', qty);
      try {
        const result = await this.doReserve(sku, qty);
        span.setStatus({ code: SpanStatusCode.OK });
        return result;
      } catch (err) {
        span.recordException(err as Error);
        span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });
        throw err;
      } finally {
        span.end();
      }
    });
  }

  private async doReserve(sku: string, qty: number) {
    return { sku, qty, reserved: true };
  }
}

Berika controllerns aktiva span

En NestJS-controllerhanterare körs redan i den automatiskt skapade SERVER-spannen för begäran. I stället för att skapa en ny span hämtar ni den aktiva spannen och berikar den med affärsattribut med hög kardinalitet (tenant-id, användar-id, order-id), så att spåren blir sökbara.

Använd trace.getActiveSpan() — den returnerar SERVER-spannen som begärans hanterare kör i. När ni lägger till attribut här hålls allt på samma nod i spåret i stället för att det splittras upp.

// orders.controller.ts
import { Controller, Post, Body, Headers } from '@nestjs/common';
import { trace } from '@opentelemetry/api';
import { OrdersService } from './orders.service';

@Controller('orders')
export class OrdersController {
  constructor(private readonly orders: OrdersService) {}

  @Post()
  async create(@Body() dto: { sku: string; qty: number }, @Headers('x-tenant-id') tenantId: string) {
    const span = trace.getActiveSpan();
    span?.setAttribute('tenant.id', tenantId);
    span?.setAttribute('order.sku', dto.sku);
    span?.addEvent('order.create.received');

    return this.orders.reserveInventory(crypto.randomUUID(), dto.sku, dto.qty);
  }
}

Spårning av utgående HTTP-klienter

När orders-tjänsten anropar betalningstjänsten via HTTP vill ni ha en CLIENT-span som vidarebefordrar headern traceparent, så att betalningstjänsten fortsätter på samma spår.

Om ni använder den automatiskt instrumenterade http-modulen (Node fetch, axios, Nest:s HttpService) sker kontextöverföringen automatiskt — förutsatt att anropet sker i den aktiva kontexten. Om ni gör anropet i en callback till startActiveSpan säkerställs att den utgående CLIENT-spannen kapslas under er affärsspan.

// payments.client.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { trace, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('orders-service');

@Injectable()
export class PaymentsClient {
  constructor(private readonly http: HttpService) {}

  async charge(orderId: string, amount: number) {
    return tracer.startActiveSpan('payments.charge', async (span) => {
      span.setAttribute('order.id', orderId);
      span.setAttribute('payment.amount', amount);
      try {
        // traceparent header is injected automatically by http instrumentation
        const res = await firstValueFrom(
          this.http.post('http://payments-svc/charges', { orderId, amount }),
        );
        span.setStatus({ code: SpanStatusCode.OK });
        return res.data;
      } catch (err) {
        span.recordException(err as Error);
        span.setStatus({ code: SpanStatusCode.ERROR });
        throw err;
      } finally {
        span.end();
      }
    });
  }
}

Sampling och kostnadskontroll

I företagsskala är det dyrt att registrera 100 % av alla spår. OTel använder samplers för att avgöra vilka spår som ska behållas, och beslutet vidarebefordras via flaggan sampled i traceparent, så att ett spår registreras (eller tas bort) konsekvent i alla tjänster.

  • ParentBasedSampler — respekterar uppströms-tjänstens beslut (standardinställningen)
  • TraceIdRatioBasedSampler — behåller en fast andel, till exempel 10 %
  • Tail sampling — utförs i Collector: behåll alla felaktiga/långsamma spår och sampla resten

Konfigurera root-samplern med miljövariablerna OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG eller SDK-alternativet sampler.

// tracing.ts (excerpt)
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

const sampler = new ParentBasedSampler({
  // when this service starts a trace, keep 10%
  root: new TraceIdRatioBasedSampler(0.1),
});

// pass `sampler` into the NodeSDK({ ... }) options

Korrelera loggar med spår

Det sista stora steget är att länka era strukturerade loggar till spår. Hämta trace_id och span_id från den aktiva spankontexten och lägg in dem i varje loggrad. I Jaeger/Tempo kan ni sedan gå direkt från en långsam span till dess loggar och tillbaka.

Använd trace.getActiveSpan()?.spanContext() för att läsa de aktuella id:na och skicka in dem som fält till er Pino-/Winston-loggning.

import { trace } from '@opentelemetry/api';

function traceFields(): Record<string, string> {
  const ctx = trace.getActiveSpan()?.spanContext();
  if (!ctx) return {};
  return { trace_id: ctx.traceId, span_id: ctx.spanId };
}

// simulate a log line enriched with correlation ids
const entry = {
  level: 'info',
  msg: 'order created',
  ...{ trace_id: '4bf92f3577b34da6a3ce929d0e0e4736', span_id: '00f067aa0ba902b7' },
};
console.log(JSON.stringify(entry));

Snabb kontroll

Er NestJS-orders-tjänst genererar SERVER-spans, men de utgående anropen till betalningstjänsten visas som separata, frånkopplade spår med ett helt nytt trace_id. Automatisk instrumentering för både HTTP och Nest är installerad. Vad är den mest sannolika grundorsaken?

Sammanfattning

Ni har instrumenterat en NestJS-tjänst från början till slut med OpenTelemetry:

  • Starta först — starta NodeSDK före AppModule (eller använd node --require) så att den automatiska instrumenteringen hinner instrumentera HTTP/Nest.
  • Automatiska + manuella spans — automatisk instrumentering ger SERVER-/CLIENT-spans för HTTP; tracer.startActiveSpan lägger till affärsspans som kapslas korrekt.
  • Berika den aktiva spannen — använd trace.getActiveSpan() i controllers för att lägga till attribut för tenant/användare/order.
  • Kontextöverföring — W3C-headern traceparent innehåller trace_id + samplingsbeslutet; utgående anrop måste köras i den aktiva kontexten för att fortsätta vara korrelerade.
  • Sampling och loggar — ParentBased tillsammans med ratio (eller tail sampling i Collector) styr kostnaden; injicera trace_id/span_id i loggarna för att koppla samman spårning och loggning.

Grundregeln är: ett spår förblir sammanhängande endast när den aktiva kontexten följer med genom varje asynkratiskt steg.

Gratis att börja

Lär dig TypeScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
20
Lektioner
76

Vanliga frågor

Är lektionen ”Distribuerad tracing med OpenTelemetry” gratis?

Ja – hela texten till ”Distribuerad tracing med OpenTelemetry” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i NestJS: backend-API:er för företag, kan Ni uppgradera till CoddyKit PRO. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.

Vad lär jag mig i ”Distribuerad tracing med OpenTelemetry”?

Instrumentera controllers, providers och HTTP-klienter så att korrelerade spans skapas mellan tjänster. Ni övar på NestJS: backend-API:er för företag med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig NestJS: backend-API:er för företag?

Du behöver inga förkunskaper. Utbildningen i NestJS: backend-API:er för företag på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 3 av 4.

Hur lång tid tar lektionen ”Distribuerad tracing med OpenTelemetry”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här NestJS: backend-API:er för företag-lektionen?

Ja. Varje NestJS: backend-API:er för företag-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Tidsgränser, omförsök och bulkheads med interceptors
  2. Circuit breakers för fel i nedströms tjänster
  3. Distribuerad tracing med OpenTelemetry
  4. Definiera SLO:er och error budgets
← Tillbaka till NestJS: backend-API:er för företag