Distributed Tracing di NestJS dengan Auto-Instrumentation OpenTelemetry

Foto oleh Pi.1415926535 via Wikimedia Commons (CC BY-SA 3.0)
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.
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.
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.
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.
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.

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.
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.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.
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:
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();
}
});
}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().
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]| Pengaturan | Nilai / default |
|---|---|
| Endpoint traces OTLP/HTTP | http://localhost:4318/v1/traces |
| Endpoint OTLP/gRPC | http://localhost:4317 |
| Override endpoint via env | OTEL_EXPORTER_OTLP_ENDPOINT |
| Nama service via env | OTEL_SERVICE_NAME |
| Propagator default | W3C 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.