NestJS Enterprise Backend APIs · Pelajaran

Pelacakan Terdistribusi dengan OpenTelemetry

Instrumen pengontrol, penyedia, dan klien HTTP untuk memancarkan span berkorelasi di seluruh layanan.

Pelajaran 3 dari 413 langkah

Pelacakan Terdistribusi dengan OpenTelemetry adalah pelajaran NestJS Enterprise Backend APIs gratis di CoddyKit. Ini adalah pelajaran 3 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar NestJS Enterprise Backend APIs, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus NestJS Enterprise Backend APIs mencakup 4 pelajaran total.

Bagian dari pelajaran ini belum diterjemahkan dan ditampilkan dalam bahasa Inggris.

Why Distributed Tracing

In a microservice fleet a single user request might touch a gateway, an orders service, a payments service, and a third-party HTTP API. When latency spikes, logs alone cannot tell you which hop was slow.

Distributed tracing stitches these hops together. Each unit of work becomes a span; spans linked by a shared trace_id form one end-to-end trace.

  • trace_id — the same value across every service in one request
  • span_id — unique per operation
  • parent_span_id — how spans nest into a tree

OpenTelemetry (OTel) is the vendor-neutral standard for producing and propagating these spans, which we'll wire into NestJS.

The Anatomy of a Span

A span is just a typed object describing one operation in time. Before touching NestJS, it helps to model what OTel actually emits. Below is a plain TypeScript sketch of the fields the SDK populates.

Note kind: SERVER spans represent inbound requests, CLIENT spans represent outbound calls. Correlating a CLIENT span in service A with a SERVER span in service B is exactly what context propagation buys you.

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`);

Installing the OTel SDK

For a NestJS service you need three layers of OTel packages:

  • @opentelemetry/sdk-node — the Node SDK and lifecycle
  • @opentelemetry/auto-instrumentations-node — zero-code patches for HTTP, Express, Nest, pg, ioredis, etc.
  • An exporter such as @opentelemetry/exporter-trace-otlp-http to ship spans to a collector or backend (Jaeger, Tempo, Honeycomb)

Auto-instrumentation alone already produces correlated SERVER and CLIENT spans for incoming HTTP and outgoing fetch/axios calls. Manual spans (later scenes) layer business meaning on top.

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

Bootstrapping Tracing Before Nest

The single most important rule: start the OTel SDK before any application module is imported. Auto-instrumentation works by monkey-patching modules like http at require time. If Nest loads first, the patches miss it.

Put the SDK in its own tracing.ts and import it at the very top of main.ts, or preload it with 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));
});

Wiring It Into main.ts

Because side-effect import order matters, the tracing import must be the first statement — above the Nest factory and even above AppModule. ES module hoisting will still respect physical order for the SDK's sdk.start() side effect as long as this file is first.

A safer alternative that avoids any hoisting ambiguity is the node --require ./dist/tracing.js dist/main.js preload flag in your start script.

// 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();

How Context Propagates Across Services

Spans become a single trace only if the trace_id travels between services. OTel does this with the W3C Trace Context standard, injecting a traceparent HTTP header on outbound CLIENT spans and extracting it on inbound SERVER spans.

The header looks like:

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

With auto-instrumentation this is automatic for HTTP. The key requirement is that the active context flows through your async code so the outbound call knows which trace it belongs to.

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);

Manual Spans in a Provider

Auto-instrumentation gives you HTTP-level spans, but business logic ("reserve inventory", "charge card") deserves its own spans. Grab a Tracer and wrap the operation with startActiveSpan so child spans nest correctly under the current SERVER span.

startActiveSpan sets the span as active for the duration of its callback, meaning any nested span or outbound HTTP call automatically becomes its child.

// 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 };
  }
}

Enriching the Controller's Active Span

A NestJS controller handler already runs inside the auto-generated SERVER span for the request. Rather than create a new one, fetch the active span and decorate it with high-cardinality business attributes (tenant id, user id, order id) so traces become searchable.

Use trace.getActiveSpan() — it returns the SERVER span the request handler is executing within. Adding attributes here keeps everything on one node of the trace instead of fragmenting it.

// 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);
  }
}

Tracing Outbound HTTP Clients

When the orders service calls the payments service over HTTP, you want a CLIENT span that propagates the traceparent header so payments continues the same trace.

If you use the auto-instrumented http module (Node fetch, axios, Nest's HttpService), propagation is automatic — provided the call happens inside the active context. Calling out within a startActiveSpan callback guarantees the outbound CLIENT span nests under your business span.

// 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 and Cost Control

At enterprise scale, recording 100% of traces is expensive. OTel uses samplers to decide which traces to keep, and the decision propagates via the sampled flag in traceparent so a trace is recorded (or dropped) consistently across all services.

  • ParentBasedSampler — respect the upstream service's decision (the standard default)
  • TraceIdRatioBasedSampler — keep a fixed fraction, e.g. 10%
  • Tail sampling — done in the Collector: keep all error/slow traces, sample the rest

Configure the root sampler with OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG env vars or the SDK sampler option.

// 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

Correlating Logs with Traces

The final win is linking your structured logs to traces. Pull trace_id and span_id from the active span context and inject them into every log line. In Jaeger/Tempo you can then jump from a slow span straight to its logs and back.

Use trace.getActiveSpan()?.spanContext() to read the current ids and feed them into your Pino/Winston logger as fields.

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));

Quick Check

Your NestJS orders service emits SERVER spans, but the outbound calls to the payments service show up as separate, disconnected traces with a brand-new trace_id. Auto-instrumentation for both HTTP and Nest is installed. What is the most likely root cause?

Recap

You instrumented a NestJS service end-to-end with OpenTelemetry:

  • Bootstrap first — start the NodeSDK before AppModule (or use node --require) so auto-instrumentation patches HTTP/Nest in time.
  • Auto + manual spans — auto-instrumentation gives SERVER/CLIENT HTTP spans; tracer.startActiveSpan adds business spans that nest correctly.
  • Enrich the active span — trace.getActiveSpan() in controllers to attach tenant/user/order attributes.
  • Propagation — the W3C traceparent header carries trace_id + sampling decision; outbound calls must run inside the active context to stay correlated.
  • Sampling & logs — ParentBased + ratio (or tail sampling in the Collector) controls cost; inject trace_id/span_id into logs to bridge traces and logging.

The cardinal rule: a trace stays whole only when the active context flows through every async hop.

Gratis untuk memulai

Belajar TypeScript dengan tutor AI — gratis

Tulis dan jalankan kode asli di browser kamu, dapatkan bantuan instan dari tutor AI 24/7, dan lanjutkan di mana kamu tinggalkan di web atau aplikasi.

Kursus
20
Pelajaran
76

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Pelacakan Terdistribusi dengan OpenTelemetry” gratis?

Ya — teks lengkap “Pelacakan Terdistribusi dengan OpenTelemetry” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus NestJS Enterprise Backend APIs, upgrade ke CoddyKit PRO. Kursus NestJS Enterprise Backend APIs mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Pelacakan Terdistribusi dengan OpenTelemetry”?

Instrumen pengontrol, penyedia, dan klien HTTP untuk memancarkan span berkorelasi di seluruh layanan. Kamu berlatih NestJS Enterprise Backend APIs dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai NestJS Enterprise Backend APIs?

Tidak diperlukan pengalaman sebelumnya. NestJS Enterprise Backend APIs di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 3 dari 4.

Berapa lama pelajaran “Pelacakan Terdistribusi dengan OpenTelemetry” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran NestJS Enterprise Backend APIs ini?

Ya. Setiap pelajaran NestJS Enterprise Backend APIs menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Batas Waktu, Percobaan Ulang, dan Bulkhead dengan Interceptor
  2. Pemutus Sirkuit untuk Kegagalan Layanan Hilir
  3. Pelacakan Terdistribusi dengan OpenTelemetry
  4. Menentukan SLO dan Anggaran Kesalahan
← Kembali ke NestJS Enterprise Backend APIs