Skip to main content
BlogBackend

Distributed Tracing di NestJS dengan Auto-Instrumentation OpenTelemetry

Bagaimana saya menambahkan distributed tracing ke API NestJS dengan auto-instrumentation OpenTelemetry — setup SDK, context propagation, dan mengekspor span ke collector.

Matthews Wong

Software Engineer

Juli 20269 menit baca
Distributed Tracing di NestJS dengan Auto-Instrumentation OpenTelemetry

Foto oleh Pi.1415926535 via Wikimedia Commons (CC BY-SA 3.0)

Ringkasan Utama

Untuk menambahkan distributed tracing ke API NestJS, pasang @opentelemetry/sdk-node bersama @opentelemetry/auto-instrumentations-node, jalankan SDK di file yang dipreload sebelum Nest boot, lalu arahkan exporter OTLP ke sebuah collector. Auto-instrumentation menambal HTTP, Express, dan TypeORM secara otomatis, dan header traceparent W3C merambatkan context antar service tanpa perkabelan manual.

Satu request lambat di service NestJS mudah di-debug. Satu request lambat yang menyebar ke tiga API internal, satu query Postgres, dan satu cache Redis tidak, karena log Anda hanya menunjukkan satu hop pada satu waktu. Distributed tracing menjahit hop-hop itu menjadi satu timeline, dan OpenTelemetry kini menjadi standar netral-vendor untuk menghasilkannya. Bagian terbaiknya adalah Anda hampir tidak menulis kode tracing: auto-instrumentation yang mengerjakan bagian beratnya.

Saya menjalankan beberapa service TypeScript di VPS self-hosted, dan menyambungkan OpenTelemetry ke dalamnya memakan waktu satu sore, yang sebagian besar dihabiskan untuk mempelajari satu aturan yang dikubur dokumentasi: SDK harus start sebelum apa pun lainnya dimuat. Salah soal ini dan Anda mendapat trace kosong tanpa error sama sekali, jenis kegagalan yang paling buruk. Panduan ini membahas setup SDK, cara kerja context propagation yang sebenarnya, dan cara mengekspor span ke collector.

Paket yang benar-benar Anda butuhkan

OpenTelemetry hadir sebagai banyak paket kecil, yang awalnya menakutkan. Untuk setup NestJS khusus traces yang mengekspor lewat OTLP/HTTP, Anda butuh lima: API, Node SDK, metapaket auto-instrumentations, exporter trace OTLP, dan helper semantic-conventions. Metapaket auto-instrumentations menggabungkan instrumentation HTTP, Express, NestJS-core, TypeORM, Prisma, dan PG sehingga Anda tidak memasangnya satu per satu.

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

# As of mid-2026, auto-instrumentations-node is ~0.78.x.
# Note: instrumentation-fastify was removed from the metapackage
# in early 2026 — if you run Fastify, add @fastify/otel separately.

Aturan emas: start SDK sebelum Nest

Auto-instrumentation bekerja dengan me-monkey-patch modul library saat mereka di-require. Artinya SDK harus berjalan sebelum Node memuat Express, TypeORM, atau driver pg — jika Nest mengimpornya lebih dulu, patch menempel ke ketiadaan dan Anda mendapat trace kosong tanpa suara. Cara andal untuk menjamin urutan adalah file tracing terpisah yang dimuat dengan flag --require Node, sehingga ia dieksekusi sebelum entrypoint Anda bahkan diparsing.

// tracing.ts — loaded via --require, NOT imported by main.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';

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'orders-api',
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? '0.0.0',
  }),
  traceExporter: new OTLPTraceExporter({
    // default is http://localhost:4318/v1/traces
    url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      // fs spans are noisy and rarely useful
      '@opentelemetry/instrumentation-fs': { enabled: false },
      '@opentelemetry/instrumentation-http': {
        ignoreIncomingRequestHook: (req) => req.url === '/health',
      },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown().finally(() => process.exit(0));
});
// package.json — preload tracing before the app
{
  "scripts": {
    "start:prod": "node --require ./dist/tracing.js dist/main.js"
  }
}

Jangan mengimpor tracing.ts di bagian atas main.ts lalu menganggap itu cukup awal. Jika ESM meng-hoist import lain di atasnya, atau graf dependensi Nest menarik sebuah driver lebih dulu, urutan patch rusak. Preload --require (atau --import untuk ESM) adalah satu-satunya jaminan urutan yang bertahan melewati bundler.

Apa yang diberikan auto-instrumentation secara gratis

Begitu SDK berjalan, setiap request HTTP masuk menjadi root span, dan setiap operasi downstream bersarang di bawahnya. Anda tidak menyentuh satu controller pun. Berikut yang menyala di stack NestJS + TypeORM + Postgres tipikal:

  • Span request masuk, ditandai dengan method HTTP, route, dan status code
  • Span anak per panggilan fetch atau Axios keluar ke service lain
  • Span per query database, dengan statement SQL tercatat sebagai atribut
  • Operasi Redis, gRPC, dan message-queue jika library tersebut ada

Anda hanya menulis span manual ketika ada logika bisnis yang layak diukur — perhitungan harga, render PDF, sebuah langkah batch job. Untuk itu, ambil tracer dari API dan bungkus pekerjaan dalam active span; semua yang Anda mulai di dalamnya otomatis menjadi anak dari span request saat ini.

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

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

async function settleInvoice(id: string) {
  return tracer.startActiveSpan('settle-invoice', async (span) => {
    try {
      span.setAttribute('invoice.id', id);
      const result = await doWork(id); // nested DB spans attach here
      return result;
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}

Bagaimana context propagation benar-benar melintasi jaringan

Sebuah trace tetap terhubung antar service karena satu header HTTP. Ketika service Anda memanggil service lain, instrumentation HTTP menyuntikkan header traceparent yang membawa trace ID dan span ID saat ini; instrumentation service penerima membacanya dan menjadikan root span-nya sebagai anak dari milik Anda. Ini adalah standar W3C Trace Context, dan itu propagator default OpenTelemetry, sehingga dua service NestJS ber-OTel tersambung tanpa konfigurasi.

Anda hanya mengganti propagator ketika berkomunikasi dengan sistem yang berbicara dialek berbeda — banyak service Zipkin dan Java lama memancarkan header B3 alih-alih. Dalam kasus itu, daftarkan composite propagator yang membaca sekaligus menulis W3C dan B3, sehingga context bertahan tidak peduli sisi mana yang memulai panggilan.

import { CompositePropagator, W3CTraceContextPropagator } from '@opentelemetry/core';
import { B3Propagator, B3InjectEncoding } from '@opentelemetry/propagator-b3';

const sdk = new NodeSDK({
  textMapPropagator: new CompositePropagator({
    propagators: [
      new W3CTraceContextPropagator(),
      new B3Propagator({ injectEncoding: B3InjectEncoding.MULTI_HEADER }),
    ],
  }),
  // ...resource, traceExporter, instrumentations
});

Propagation hanya membawa context ke hop berikutnya jika kode async Anda sendiri mempertahankannya. AsyncLocalStorage milik Node melakukan ini untuk Anda dalam satu proses, tetapi jika Anda mendorong pekerjaan ke setInterval mentah atau queue yang tak ditambal, span context bisa hilang. Utamakan library yang sudah ditambal auto-instrumentation, atau bungkus serah-terima dalam context.with().

Mengekspor ke collector, bukan langsung ke backend

Anda bisa mengarahkan exporter OTLP langsung ke vendor, tetapi pola yang menua dengan baik adalah mengekspor ke OpenTelemetry Collector yang berjalan sebagai sidecar atau service bersama, lalu biarkan collector menyebarkannya ke backend Anda. Ini melepaskan aplikasi Anda dari vendor spesifik mana pun, memungkinkan batching dan sampling terpusat, dan berarti perubahan backend adalah edit konfigurasi collector, bukan redeploy setiap service. Exporter default ke OTLP/HTTP di port 4318; exporter gRPC memakai 4317.

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  debug:
    verbosity: detailed
  # otlphttp/jaeger, otlp/tempo, etc. go here

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
PengaturanNilai / default
Endpoint traces OTLP/HTTPhttp://localhost:4318/v1/traces
Endpoint OTLP/gRPChttp://localhost:4317
Override endpoint via envOTEL_EXPORTER_OTLP_ENDPOINT
Nama service via envOTEL_SERVICE_NAME
Propagator defaultW3C Trace Context (traceparent)

Dengan collector menerima span, jalankan satu request ke API Anda dan amati debug exporter mencetak trace yang terhubung: root span HTTP, controller, method service, dan query SQL, semuanya di bawah satu trace ID. Dari sana, menukar target ekspor collector ke Jaeger, Tempo, atau vendor terkelola adalah perubahan konfigurasi yang Anda lakukan sekali — aplikasi tidak pernah perlu tahu.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Mengapa trace OpenTelemetry saya kosong di NestJS?

Hampir selalu karena SDK start setelah Nest sudah memuat library yang ingin Anda trace. Auto-instrumentation menambal modul saat di-require, jadi ia harus berjalan lebih dulu. Muat file tracing Anda dengan flag --require Node (atau --import untuk ESM) alih-alih mengimpornya di dalam main.ts, dan patch akan menempel sebelum Express, TypeORM, atau driver pg dimuat.

Apakah saya perlu menulis kode span di setiap controller?

Tidak. Auto-instrumentation membuat root span untuk setiap request HTTP masuk dan menyarangkan query database, panggilan HTTP keluar, serta operasi Redis di bawahnya secara otomatis. Anda hanya menulis span manual untuk logika bisnis yang secara spesifik ingin diukur, seperti perhitungan harga atau langkah batch, memakai tracer.startActiveSpan.

Bagaimana trace context berpindah antara dua service NestJS?

Melalui header HTTP traceparent W3C. Instrumentation HTTP menyuntikkannya pada panggilan keluar dan membacanya pada panggilan masuk, sehingga service penerima menjadikan root span-nya anak dari span pemanggil. W3C Trace Context adalah propagator default OpenTelemetry, jadi dua service ber-instrumentasi tersambung tanpa konfigurasi.

Sebaiknya ekspor trace ke collector atau langsung ke backend?

Ekspor ke OpenTelemetry Collector di produksi. Itu melepaskan aplikasi dari vendor spesifik mana pun, memungkinkan batching dan sampling terpusat, serta mengubah pergantian backend menjadi edit konfigurasi collector alih-alih redeploy setiap service. Arahkan exporter OTLP ke port 4318 collector untuk HTTP atau 4317 untuk gRPC.

Apa beda exporter OTLP HTTP dan gRPC?

Keduanya mengirim data OTLP yang sama lewat transport berbeda. Exporter HTTP default ke port 4318 di path /v1/traces dan lebih mudah dirutekan lewat proxy dan firewall. Exporter gRPC memakai port 4317 dan bisa lebih efisien pada volume tinggi. Keduanya bekerja dengan collector standar; pilih HTTP kecuali ada alasan untuk tidak.