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]| 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.

