Panduan komprehensif OpenTelemetry yang mencakup sejarah, konsep inti, strategi instrumentasi, dan implementasi real-world dengan NestJS menggunakan ketiga pilar observability.

Dalam sistem terdistribusi, memahami apa yang terjadi di puluhan atau ratusan service ibarat mencoba menyusun puzzle di mana kepingannya tersebar di berbagai ruangan. Anda mungkin tahu API Anda lambat, tapi apakah masalahnya di database, cache, third-party service, atau network latency? Tanpa observability yang tepat, Anda debugging dalam kegelapan.
Di sinilah OpenTelemetry berperan. Ini bukan sekadar monitoring tool—ini adalah standar vendor-neutral yang menyatukan cara kita mengumpulkan, memproses, dan mengekspor data telemetry. Bayangkan seperti USB-C untuk observability: satu standar yang bekerja di mana saja, menghilangkan kebutuhan akan proprietary agent dan vendor lock-in.
Dalam deep dive ini, kita akan mengeksplorasi OpenTelemetry dari asal-usulnya hingga implementasi production, mencakup instrumentasi manual dan otomatis, konsep inti, dan aplikasi NestJS real-world yang mendemonstrasikan ketiga pilar observability: metrics, logs, dan traces.
Sebelum OpenTelemetry, landscape observability sangat terfragmentasi. Setiap vendor memiliki agent, SDK, dan format data mereka sendiri. Jika Anda menggunakan Datadog, Anda install Datadog agent. Jika beralih ke New Relic, Anda harus membongkar semuanya dan mulai dari awal. Ini menciptakan beberapa masalah:
Dua proyek muncul untuk menyelesaikan ini: OpenTracing (fokus pada distributed tracing) dan OpenCensus (fokus pada metrics dan tracing). Keduanya mendapat traksi, tapi memiliki dua standar yang bersaing menciptakan masalahnya sendiri. Komunitas terpecah, dan vendor harus mendukung keduanya.
Pada 2019, Cloud Native Computing Foundation (CNCF) menggabungkan OpenTracing dan OpenCensus menjadi OpenTelemetry. Tujuannya sederhana: membuat satu standar vendor-neutral untuk pengumpulan data telemetry. OpenTelemetry menjadi CNCF incubating project pada 2021 dan sejak itu menjadi proyek CNCF paling aktif kedua setelah Kubernetes.
Hari ini, OpenTelemetry didukung oleh setiap vendor observability besar: Datadog, New Relic, Honeycomb, Grafana, Elastic, AWS, Google Cloud, dan Azure. Ini adalah standar de facto untuk cloud-native observability.
Observability bukan sekadar monitoring. Monitoring memberi tahu Anda kapan ada yang salah; observability memberi tahu Anda mengapa. Ketiga pilar bekerja bersama untuk memberikan visibilitas lengkap:
Metrics adalah pengukuran numerik dari waktu ke waktu. Mereka menjawab pertanyaan seperti "Berapa request per detik?" atau "Berapa latency persentil ke-95?" Metrics murah untuk dikumpulkan dan disimpan, menjadikannya ideal untuk dashboard dan alert.
Contoh:
Logs adalah catatan timestamped dari event diskrit. Mereka memberikan konteks tentang apa yang terjadi pada momen tertentu. Logs mahal untuk disimpan dalam skala besar tapi sangat berharga untuk debugging masalah spesifik.
Contoh:
Traces menunjukkan perjalanan request melalui sistem terdistribusi Anda. Sebuah trace terdiri dari span, di mana setiap span mewakili unit of work. Traces menjawab pertanyaan seperti "Service mana yang menyebabkan slowdown?" dan "Apa critical path-nya?"
Contoh trace flow:
API Gateway (50ms)
└─ Auth Service (10ms)
└─ User Service (30ms)
└─ Database Query (25ms)
└─ Cache Check (5ms)Tip
Ketiga pilar paling powerful ketika dikorelasikan. Spike dalam error rate (metric) dapat diinvestigasi dengan logs (apa yang gagal) dan traces (di mana gagalnya).
Memahami OpenTelemetry memerlukan pemahaman arsitektur dan komponen kuncinya.
OpenTelemetry terdiri dari beberapa komponen:
OpenTelemetry mendefinisikan tiga tipe signal:
Setiap signal memiliki API dan SDK sendiri, tapi mereka berbagi konsep umum seperti context propagation dan resource attribute.
Context adalah cara OpenTelemetry mengorelasikan telemetry melintasi service boundary. Ketika request masuk ke sistem Anda, OpenTelemetry membuat trace context yang berisi:
Context ini dipropagasi melalui HTTP header (standar W3C Trace Context), message queue, dan mekanisme transport lainnya. Inilah yang memungkinkan distributed tracing.
Resource mendeskripsikan entitas yang menghasilkan telemetry. Resource attribute umum meliputi:
service.name: Nama aplikasi Andaservice.version: Versi aplikasideployment.environment: prod, staging, devhost.name: Hostname servercloud.provider: AWS, GCP, AzureAttribute adalah key-value pair yang dilampirkan ke span, metric, dan log. Mereka menyediakan dimensi untuk filtering dan grouping:
span.setAttribute('http.method', 'GET');
span.setAttribute('http.status_code', 200);
span.setAttribute('user.id', '12345');
span.setAttribute('db.statement', 'SELECT * FROM users');OpenTelemetry mendefinisikan semantic convention—nama attribute standar untuk skenario umum. Ini memastikan konsistensi di berbagai service dan bahasa.
Untuk HTTP request:
http.method: GET, POST, dll.http.url: URL lengkaphttp.status_code: Response statushttp.route: Route pattern seperti /users/:idUntuk database operation:
db.system: postgresql, mysql, mongodbdb.statement: SQL query atau commanddb.name: Nama databasedb.operation: SELECT, INSERT, UPDATEMenggunakan semantic convention membuat data telemetry Anda portable dan lebih mudah dianalisis.
Mengumpulkan setiap trace dalam sistem high-traffic itu mahal dan tidak perlu. Sampling memutuskan trace mana yang disimpan. OpenTelemetry mendukung beberapa strategi sampling:
Head-based sampling (keputusan di root span) sederhana tapi bisa melewatkan trace menarik. Tail-based sampling (keputusan setelah melihat seluruh trace) lebih sophisticated tapi memerlukan OpenTelemetry Collector.
OpenTelemetry menawarkan dua pendekatan instrumentasi, masing-masing dengan trade-off.
Instrumentasi otomatis menggunakan agent atau library yang menyuntikkan telemetry tanpa perubahan kode. Untuk Node.js, ini dilakukan melalui package @opentelemetry/auto-instrumentations-node.
Keuntungan:
Kekurangan:
Kapan menggunakan:
Instrumentasi manual berarti secara eksplisit membuat span, metric, dan log dalam kode Anda.
Keuntungan:
Kekurangan:
Kapan menggunakan:
Dalam production, Anda biasanya menggunakan keduanya. Instrumentasi otomatis memberikan baseline coverage, sementara instrumentasi manual menambahkan konteks bisnis dan custom metric.
// Otomatis: HTTP request di-trace secara otomatis
@Get('/users/:id')
async getUser(@Param('id') id: string) {
// Manual: Tambahkan konteks bisnis
const span = trace.getActiveSpan();
span?.setAttribute('user.id', id);
span?.setAttribute('user.tier', 'premium');
// Manual: Custom metric
this.userFetchCounter.add(1, { tier: 'premium' });
return this.userService.findOne(id);
}OpenTelemetry Collector adalah proxy vendor-agnostic yang menerima, memproses, dan mengekspor data telemetry. Bayangkan sebagai data pipeline untuk observability.
Tanpa collector, setiap aplikasi mengekspor langsung ke backend:
App 1 → Datadog
App 2 → Datadog
App 3 → DatadogIni menciptakan masalah:
Dengan collector:
App 1 ↘
App 2 → Collector → Datadog
App 3 ↗Keuntungan:
Collector memiliki tiga tipe komponen:
Receiver: Menerima data telemetry
Processor: Transform dan filter data
Exporter: Kirim data ke backend
Agent Pattern: Collector berjalan sebagai sidecar atau daemon di setiap host
App → Collector (localhost) → BackendGateway Pattern: Collector berjalan sebagai centralized service
App 1 ↘
App 2 → Collector (gateway) → Backend
App 3 ↗Hybrid Pattern: Agent collector forward ke gateway collector
App → Collector (agent) → Collector (gateway) → BackendImportant
Untuk production, gateway pattern direkomendasikan. Ini memusatkan konfigurasi, mengurangi dependency aplikasi, dan memungkinkan processing advanced seperti tail-based sampling.
Sekarang mari kita bangun aplikasi NestJS production-grade dengan instrumentasi OpenTelemetry lengkap yang mencakup metrics, logs, dan traces.
Pertama, buat project NestJS baru dan install dependency:
npm i -g @nestjs/cli
nest new otel-demo
cd otel-demoBuat file dedicated untuk setup OpenTelemetry. Ini harus diimpor sebelum kode aplikasi lainnya:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_SERVICE_VERSION,
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
} from '@opentelemetry/semantic-conventions';
const resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'nestjs-otel-demo',
[SEMRESATTRS_SERVICE_VERSION]: '1.0.0',
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || 'http://localhost:4318/v1/traces',
});
const metricExporter = new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || 'http://localhost:4318/v1/metrics',
});
const logExporter = new OTLPLogExporter({
url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || 'http://localhost:4318/v1/logs',
});
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 60000, // Ekspor setiap 60 detik
}),
logRecordProcessor: new BatchLogRecordProcessor(logExporter),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false, // Disable filesystem instrumentation (terlalu noisy)
},
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
// Abaikan health check endpoint
return req.url?.includes('/health') || false;
},
},
}),
],
});
sdk.start();
// Graceful shutdown
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('OpenTelemetry SDK shut down successfully'))
.catch((error) => console.error('Error shutting down OpenTelemetry SDK', error))
.finally(() => process.exit(0));
});
export default sdk;Import tracing sebelum yang lain:
// HARUS import pertama
import './tracing';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('Application is running on: http://localhost:3000');
}
bootstrap();Warning
Import tracing harus menjadi baris pertama di entry point Anda. Jika Anda mengimpor modul lain terlebih dahulu, mereka tidak akan diinstrumentasi secara otomatis.
Implementasi service untuk metrics, logging, dan tracing sama dengan versi English. Berikut struktur lengkapnya:
Metrics Service (src/telemetry/metrics.service.ts): Menyediakan counter, histogram, dan gauge untuk business metric.
Logger Service (src/telemetry/logger.service.ts): Structured logging yang terintegrasi dengan OpenTelemetry, otomatis menambahkan trace context.
Tracing Service (src/telemetry/tracing.service.ts): Helper untuk membuat span manual dengan error handling otomatis.
Telemetry Module (src/telemetry/telemetry.module.ts): Module global yang meng-export semua service telemetry.
Service order yang realistis menggunakan ketiga pilar:
@Injectable()
export class OrdersService {
constructor(
private readonly metrics: MetricsService,
private readonly logger: LoggerService,
private readonly tracing: TracingService,
) {}
async createOrder(userId: string, items: Array<any>) {
return this.tracing.withSpan('orders.create', async (span) => {
span.setAttribute('user.id', userId);
this.logger.log('Creating new order', { user_id: userId });
await this.validateItems(items);
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
await this.processPayment(userId, total);
// Buat order
const order = { /* ... */ };
// Record metric
this.metrics.recordOrderCreated(total, 'USD', userId);
this.logger.log('Order created successfully', { order_id: order.id });
span.addEvent('order.created', { order_id: order.id });
return order;
});
}
}@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(private readonly metrics: MetricsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
tap(() => {
this.metrics.incrementRequestCount(
request.method,
request.route?.path || request.url,
response.statusCode,
);
}),
);
}
}Buat konfigurasi collector:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 10s
send_batch_size: 1024
attributes:
actions:
- key: environment
value: production
action: insert
exporters:
logging:
loglevel: debug
prometheus:
endpoint: 0.0.0.0:8889
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, attributes]
exporters: [logging]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheus]
logs:
receivers: [otlp]
processors: [batch, attributes]
exporters: [logging]version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317"
- "4318:4318"
- "8889:8889"
networks:
- otel
jaeger:
image: jaegertracing/all-in-one:latest
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686"
- "14250:14250"
networks:
- otel
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
networks:
- otel
grafana:
image: grafana/grafana:latest
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
ports:
- "3001:3000"
volumes:
- grafana-storage:/var/lib/grafana
networks:
- otel
networks:
otel:
driver: bridge
volumes:
grafana-storage:Start observability stack:
docker-compose up -dStart aplikasi NestJS:
npm run start:devBuat beberapa test request:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{
"userId": "user-123",
"items": [
{
"productId": "prod-1",
"quantity": 2,
"price": 29.99
}
]
}'Akses observability tool:
Di Jaeger, Anda akan melihat trace yang menunjukkan complete request flow:
HTTP POST /orders (250ms)
├─ orders.create (240ms)
│ ├─ orders.validateItems (50ms)
│ └─ orders.processPayment (200ms)
└─ HTTP responseDi Prometheus, query metric:
# Request rate per endpoint
rate(http_requests_total[5m])
# Order creation rate
rate(orders_created_total[5m])
# 95th percentile order value
histogram_quantile(0.95, orders_value_bucket)// ❌ Salah - import lain duluan
import { NestFactory } from '@nestjs/core';
import './tracing';
// ✅ Benar - tracing pertama
import './tracing';
import { NestFactory } from '@nestjs/core';Jika tracing tidak diimpor pertama, instrumentasi otomatis tidak akan bekerja karena modul sudah dimuat.
// ❌ Salah - span untuk operasi trivial
async calculateTotal(items) {
return this.tracing.withSpan('calculateTotal', async () => {
return items.reduce((sum, item) => sum + item.price, 0);
});
}
// ✅ Benar - span untuk operasi meaningful
async processPayment(amount) {
return this.tracing.withSpan('processPayment', async () => {
// External API call, layak di-trace
return this.paymentGateway.charge(amount);
});
}Span memiliki overhead. Hanya buat span untuk operasi yang melintasi boundary (network, disk, external service) atau business-critical.
// ❌ Salah - nama attribute custom
span.setAttribute('method', 'GET');
span.setAttribute('url', '/api/users');
// ✅ Benar - semantic convention
span.setAttribute('http.method', 'GET');
span.setAttribute('http.url', '/api/users');Semantic convention memastikan telemetry Anda portable dan bekerja dengan dashboard dan query standar.
// ❌ Salah - span tidak pernah berakhir
const span = tracer.startSpan('operation');
await doWork();
// Lupa memanggil span.end()
// ✅ Benar - gunakan withSpan helper
await this.tracing.withSpan('operation', async (span) => {
await doWork();
// Otomatis diakhiri
});Span yang tidak diakhiri menyebabkan memory leak dan merusak trace. Selalu gunakan helper yang menjamin lifecycle management span.
// ❌ Salah - logging data sensitif
this.logger.log('User login', {
email: user.email,
password: user.password, // Jangan pernah log password!
credit_card: user.creditCard,
});
// ✅ Benar - sanitasi data sensitif
this.logger.log('User login', {
user_id: user.id,
email_domain: user.email.split('@')[1],
});Data telemetry sering disimpan untuk periode lama dan mungkin dapat diakses banyak orang. Jangan pernah log password, token, credit card, atau PII.
// ❌ Salah - sampling semuanya di production
const sdk = new NodeSDK({
// Tidak ada sampler dikonfigurasi = sample semuanya
});
// ✅ Benar - gunakan sampling yang sesuai
const sdk = new NodeSDK({
sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10%
});Sampling semuanya dalam sistem production high-traffic itu mahal dan tidak perlu. Gunakan ratio-based atau tail-based sampling.
// ❌ Salah - export synchronous memblokir request
const sdk = new NodeSDK({
spanProcessor: new SimpleSpanProcessor(exporter), // Synchronous
});
// ✅ Benar - batch export adalah async
const sdk = new NodeSDK({
spanProcessor: new BatchSpanProcessor(exporter), // Async batching
});Synchronous span processor memblokir aplikasi Anda. Selalu gunakan batch processor di production.
Definisikan resource attribute sekali saat startup:
const resource = new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'order-service',
[SEMRESATTRS_SERVICE_VERSION]: process.env.APP_VERSION,
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
[SEMRESATTRS_SERVICE_NAMESPACE]: 'ecommerce',
'team.name': 'checkout',
'region': process.env.AWS_REGION,
});Metadata ini dilampirkan ke semua telemetry, memungkinkan filtering dan grouping di seluruh service.
Exclude health check endpoint dari tracing untuk mengurangi noise:
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': {
ignoreIncomingRequestHook: (req) => {
return req.url?.includes('/health') ||
req.url?.includes('/metrics') ||
req.url?.includes('/ready');
},
},
});Selalu sertakan trace context dalam log:
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
logger.log('Processing order', {
order_id: orderId,
trace_id: spanContext?.traceId,
span_id: spanContext?.spanId,
});Ini memungkinkan jumping dari log ke trace dan sebaliknya di observability platform Anda.
// ❌ High cardinality - membuat terlalu banyak metric series
this.counter.add(1, {
user_id: userId, // Jutaan nilai unik
order_id: orderId,
});
// ✅ Low cardinality - dimensi terbatas
this.counter.add(1, {
user_tier: 'premium', // Nilai terbatas: free, premium, enterprise
region: 'us-east-1',
});High-cardinality attribute dalam metric membuat jutaan time series, membebani metric backend Anda. Gunakan high-cardinality data dalam trace dan log sebagai gantinya.
Pastikan telemetry di-flush sebelum shutdown:
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
// Flush telemetry
await sdk.shutdown();
// Close server
await app.close();
process.exit(0);
});Tanpa graceful shutdown, Anda akan kehilangan data telemetry dari beberapa detik terakhir sebelum terminasi.
Collector sendiri perlu monitoring. Export collector metric:
service:
telemetry:
metrics:
address: 0.0.0.0:8888Monitor:
otelcol_receiver_accepted_spans: Span yang diterimaotelcol_exporter_sent_spans: Span yang dieksporotelcol_processor_batch_batch_send_size: Ukuran batchotelcol_exporter_send_failed_spans: Kegagalan eksporKetika membuat HTTP call ke service lain, propagate context:
import { propagation, context } from '@opentelemetry/api';
async callExternalService(url: string) {
const headers = {};
// Inject trace context ke header
propagation.inject(context.active(), headers);
return axios.get(url, { headers });
}Ini memastikan trace melintasi service boundary.
OpenTelemetry tidak selalu pilihan yang tepat. Pertimbangkan alternatif ketika:
Untuk aplikasi single-service dengan traffic rendah, OpenTelemetry mungkin berlebihan. Logging sederhana dan basic metric mungkin cukup.
Alternatif: Gunakan logger sederhana seperti Winston atau Pino dengan basic metric dari framework Anda.
OpenTelemetry menambah overhead. Untuk sistem ultra-low-latency (sub-millisecond), bahkan instrumentasi minimal mungkin tidak dapat diterima.
Alternatif: Gunakan sampling-based profiler atau custom lightweight instrumentation.
Jika Anda terkunci dalam proprietary observability platform yang tidak mendukung OpenTelemetry, migrasi mungkin tidak worth it.
Alternatif: Tetap dengan vendor-specific agent sampai Anda bisa migrasi.
Inisialisasi OpenTelemetry SDK menambah cold start time di serverless function. Untuk function latency-critical, ini penting.
Alternatif: Gunakan vendor-specific lightweight SDK (AWS X-Ray SDK, Google Cloud Trace) atau defer initialization.
Penyimpanan data telemetry itu mahal. Jika Anda memiliki budget ketat, full observability mungkin tidak feasible.
Alternatif: Gunakan aggressive sampling, fokus pada error saja, atau gunakan open-source backend (Jaeger, Prometheus, Grafana).
OpenTelemetry memiliki overhead yang terukur:
Strategi mitigasi:
Data telemetry dapat berisi informasi sensitif:
Data telemetry mahal dalam skala besar:
Optimasi cost:
Buat observability stack Anda resilient:
OpenTelemetry merepresentasikan perubahan fundamental dalam cara kita mendekati observability. Dengan menyediakan standar vendor-neutral, ini menghilangkan lock-in dan memungkinkan portabilitas sejati dari data telemetry. Ketiga pilar—metrics, logs, dan traces—bekerja bersama untuk memberikan visibilitas lengkap ke dalam sistem terdistribusi.
Implementasi NestJS yang kita bangun mendemonstrasikan cara memanfaatkan ketiga pilar dalam aplikasi real-world. Instrumentasi otomatis memberikan baseline coverage, sementara instrumentasi manual menambahkan konteks bisnis dan custom metric. OpenTelemetry Collector bertindak sebagai pipeline terpusat untuk memproses dan routing data telemetry.
Key takeaway:
Mulai dengan instrumentasi otomatis untuk mendapatkan value langsung, kemudian secara bertahap tambahkan instrumentasi manual untuk critical path. Gunakan collector di production untuk fleksibilitas dan resilience. Yang terpenting, perlakukan observability sebagai first-class concern—instrumentasi sejak awal, instrumentasi sering, dan buat keputusan berdasarkan data.
Masa depan observability adalah open, standardized, dan vendor-neutral. OpenTelemetry adalah masa depan itu.