Observabilitas Lokal untuk Microservices: Debugging Sistem Terdistribusi di Mesin Dev Anda
1. Pendahuluan
Pernahkah Anda merasa frustrasi saat mencoba mencari tahu kenapa aplikasi microservices Anda bermasalah di lingkungan pengembangan lokal? Request masuk ke Service A, lalu entah kenapa Service C gagal, tapi Anda tidak tahu persis apa yang terjadi di antara keduanya. Ini adalah skenario umum yang dialami banyak developer ketika beralih dari aplikasi monolitik ke arsitektur microservices.
Debugging microservices secara lokal bisa menjadi mimpi buruk. Anda tidak lagi berurusan dengan satu proses yang bisa Anda pasangi debugger dengan mudah. Sebaliknya, Anda memiliki banyak service yang berkomunikasi melalui jaringan, seringkali secara asinkron. Kesalahan bisa terjadi di mana saja, dan melacak “perjalanan” sebuah request melalui beberapa service untuk menemukan akar masalahnya terasa seperti mencari jarum dalam tumpukan jerami.
Di sinilah observabilitas lokal berperan penting. Observabilitas bukan hanya untuk produksi; ia adalah alat yang sangat ampuh untuk meningkatkan pengalaman developer (DX) Anda. Artikel ini akan membahas bagaimana kita bisa menerapkan konsep Distributed Tracing menggunakan OpenTelemetry dan Jaeger di lingkungan pengembangan lokal untuk memecahkan misteri debugging microservices. Tujuannya adalah agar Anda bisa melihat dengan jelas apa yang terjadi pada setiap request, di setiap service, langsung dari mesin dev Anda.
2. Kenapa Debugging Lokal Microservices Itu Sulit?
Mari kita pahami dulu mengapa debugging microservices secara lokal jauh lebih kompleks dibanding monolit.
❌ Monolit:
- Satu codebase, satu proses.
- Debugging relatif mudah: pasang breakpoint, jalankan, dan ikuti alur eksekusi kode.
- Semua state dan data berada dalam satu memori address space.
⚠️ Microservices:
- Banyak Proses: Setiap service adalah proses terpisah, seringkali berjalan dalam kontainer Docker yang berbeda.
- Komunikasi Jaringan: Service berkomunikasi melalui HTTP, gRPC, atau message queues. Ini memperkenalkan latensi, kegagalan jaringan, dan masalah serialisasi/deserialisasi.
- Asynchronous: Seringkali request tidak langsung dijawab. Ada event yang dipublikasikan, antrean pesan, dan proses background yang membuat alur eksekusi sulit diikuti.
- “Black Box” Problem: Anda tahu request masuk ke Service A dan hasilnya error di Service C, tapi apa yang terjadi di Service B? Bagaimana data ditransformasi? Apa yang menyebabkan Service C gagal? Tanpa visibilitas, setiap service menjadi kotak hitam.
- Isolasi: Setiap service memiliki lognya sendiri. Menggabungkan dan menganalisis log dari banyak service secara bersamaan untuk satu request sangatlah merepotkan.
Intinya, kita kehilangan kemampuan untuk melihat “gambaran besar” dari satu request di seluruh sistem. Kita butuh sebuah peta, dan peta itu adalah Distributed Tracing.
3. Memperkenalkan Distributed Tracing untuk Lingkungan Lokal
Distributed Tracing adalah teknik observabilitas yang memungkinkan kita melacak perjalanan sebuah request atau transaksi saat melewati berbagai service dalam sistem terdistribusi. Bayangkan seperti kartu identitas yang melekat pada setiap request, mengikuti ke mana pun ia pergi.
📌 Konsep Utama Distributed Tracing:
- Trace: Representasi lengkap dari perjalanan sebuah request melalui sistem. Ini adalah “peta” keseluruhan.
- Span: Sebuah unit kerja tunggal dalam sebuah trace. Setiap span mewakili operasi yang dilakukan oleh sebuah service (misalnya, menerima request API, query database, memanggil service lain). Setiap span memiliki nama, durasi, dan metadata (seperti atribut dan event).
- Trace ID: ID unik yang mengidentifikasi seluruh trace. Semua span dalam trace yang sama akan berbagi Trace ID ini.
- Span ID: ID unik untuk setiap span.
- Parent Span ID: ID dari span yang memicu span saat ini. Ini membentuk hierarki span, menunjukkan hubungan “siapa memanggil siapa”.
OpenTelemetry adalah proyek open-source yang menyediakan standar dan tooling untuk menginstrumentasi, menghasilkan, mengumpulkan, dan mengekspor data telemetri (logs, metrics, dan traces) dari aplikasi Anda. Ini adalah vendor-agnostic, artinya Anda bisa menggunakan OpenTelemetry untuk mengirim data ke berbagai backend observabilitas (Jaeger, Prometheus, Grafana Tempo, dsb.).
Untuk observabilitas lokal, kita akan menggunakan:
- OpenTelemetry SDK/Libraries: Untuk menginstrumentasi kode aplikasi kita agar menghasilkan span.
- OpenTelemetry Collector: Sebuah proxy yang menerima, memproses, dan mengekspor data telemetri. Ini bertindak sebagai perantara antara aplikasi kita dan backend tracing.
- Jaeger: Sebuah sistem tracing terdistribusi end-to-end yang memungkinkan kita memantau dan memecahkan masalah arsitektur microservices. Jaeger menyediakan UI yang bagus untuk memvisualisasikan trace.
4. Setup Lingkungan Lokal dengan OpenTelemetry Collector dan Jaeger
Cara termudah untuk menjalankan Jaeger dan OpenTelemetry Collector secara lokal adalah dengan Docker Compose.
Pertama, buat file docker-compose.yaml di root direktori proyek Anda:
# docker-compose.yaml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver (untuk OpenTelemetry Collector)
- "4318:4318" # OTLP HTTP receiver (untuk OpenTelemetry Collector)
- "14268:14268" # Jaeger HTTP receiver (untuk langsung dari aplikasi jika tidak pakai collector)
environment:
- COLLECTOR_OTLP_ENABLED=true
networks:
- microservice-net
otel-collector:
image: otel/opentelemetry-collector:latest
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
depends_on:
- jaeger
networks:
- microservice-net
networks:
microservice-net:
driver: bridge
Kemudian, buat file konfigurasi untuk OpenTelemetry Collector (otel-collector-config.yaml):
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
http:
exporters:
jaeger:
endpoint: jaeger:14250 # Kirim data ke Jaeger Agent/Collector
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger]
✅ Penjelasan:
jaegerservice menjalankan semua komponen Jaeger (UI, Collector, Agent) dalam satu kontainer, memudahkan setup lokal.otel-collectorservice adalah OpenTelemetry Collector yang akan menerima data dari aplikasi kita via OTLP (OpenTelemetry Protocol) dan meneruskannya ke Jaeger. Ini adalah praktik terbaik karena collector bisa melakukan batching, buffering, dan preprocessing data sebelum dikirim ke backend.- Kita menggunakan network
microservice-netagar service-service ini bisa berkomunikasi satu sama lain dan dengan aplikasi microservices kita.
Untuk menjalankannya, cukup buka terminal di direktori yang sama dan ketik:
docker compose up -d
Sekarang, Anda bisa mengakses Jaeger UI di http://localhost:16686.
5. Instrumentasi Aplikasi Anda
Sekarang, mari kita instrumentasi dua service sederhana: service-a (Node.js) yang memanggil service-b (Node.js).
5.1. Service B (Node.js - Express)
Buat folder service-b.
cd service-b
npm init -y
npm install express @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-grpc
Buat file tracer.js untuk konfigurasi OpenTelemetry:
// service-b/tracer.js
const process = require('process');
const opentelemetry = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const exporterOptions = {
url: 'http://otel-collector:4317', // Kirim ke OpenTelemetry Collector
};
const traceExporter = new OTLPTraceExporter(exporterOptions);
const sdk = new opentelemetry.NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
Buat file index.js untuk aplikasi Express:
// service-b/index.js
require('./tracer'); // Pastikan tracer di-load pertama
const express = require('express');
const app = express();
const port = 3001;
app.get('/data', (req, res) => {
console.log('Service B received request');
res.json({ message: 'Data from Service B' });
});
app.listen(port, () => {
console.log(`Service B listening on port ${port}`);
});
5.2. Service A (Node.js - Express)
Buat folder service-a.
cd service-a
npm init -y
npm install express axios @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-grpc
Buat file tracer.js (sama seperti service-b, hanya URL collector yang penting):
// service-a/tracer.js
// ... (isi sama persis dengan service-b/tracer.js)
const process = require('process');
const opentelemetry = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const exporterOptions = {
url: 'http://otel-collector:4317', // Kirim ke OpenTelemetry Collector
};
const traceExporter = new OTLPTraceExporter(exporterOptions);
const sdk = new opentelemetry.NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown()
.then(() => console.log('Tracing terminated'))
.catch((error) => console.log('Error terminating tracing', error))
.finally(() => process.exit(0));
});
Buat file index.js untuk aplikasi Express:
// service-a/index.js
require('./tracer'); // Pastikan tracer di-load pertama
const express = require('express');
const axios = require('axios');
const app = express();
const port = 3000;
app.get('/call-b', async (req, res) => {
console.log('Service A received request, calling Service B');
try {
const response = await axios.get('http://service-b:3001/data'); // Panggil service-b
res.json({ message: 'Call to Service B successful', data: response.data });
} catch (error) {
console.error('Error calling Service B:', error.message);
res.status(500).json({ message: 'Error calling Service B', error: error.message });
}
});
app.listen(port, () => {
console.log(`Service A listening on port ${port}`);
});
5.3. Menjalankan Aplikasi dengan Docker Compose
Untuk menjalankan service-a dan service-b bersama Jaeger dan OTel Collector, tambahkan service-service ini ke docker-compose.yaml yang sudah ada:
# docker-compose.yaml (lanjutan)
version: '3.8'
services:
# ... (jaeger dan otel-collector services seperti di atas)
service-a:
build: ./service-a
ports:
- "3000:3000"
networks:
- microservice-net
environment:
- OTEL_SERVICE_NAME=service-a # Nama service untuk tracing
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # Penting: Gunakan nama service otel-collector
depends_on:
- otel-collector
- service-b
service-b:
build: ./service-b
ports:
- "3001:3001"
networks:
- microservice-net
environment:
- OTEL_SERVICE_NAME=service-b # Nama service untuk tracing
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # Penting: Gunakan nama service otel-collector
depends_on:
- otel-collector
networks:
microservice-net:
driver: bridge
Buat Dockerfile sederhana di masing-masing folder service-a dan service-b:
# service-a/Dockerfile & service-b/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]
Sekarang, bangun dan jalankan semua service:
docker compose up --build -d
Pastikan semua kontainer berjalan dengan docker compose ps.
6. Membaca Trace di Jaeger UI
Sekarang, buka browser Anda dan akses http://localhost:3000/call-b. Ini akan memicu service-a untuk memanggil service-b.
Setelah itu, buka Jaeger UI di http://localhost:16686.
🎯 Langkah-langkah di Jaeger UI:
- Pilih Service: Di dropdown “Service”, pilih
service-a. - Cari Trace: Klik tombol “Find Traces”. Anda akan melihat daftar trace yang baru saja dibuat.
- Analisis Trace: Klik salah satu trace untuk melihat detailnya.
Anda akan melihat visualisasi hierarkis dari request Anda:
- Sebuah span utama untuk
service-ayang menerima request/call-b. - Di bawahnya, sebuah span yang mewakili panggilan HTTP dari
service-akeservice-b. - Dan di bawahnya lagi, sebuah span untuk
service-byang menerima request/data.
💡 Apa yang bisa Anda lihat:
- Durasi Setiap Operasi: Berapa lama waktu yang dibutuhkan
service-auntuk memproses request, berapa lama panggilan keservice-bberlangsung, dan berapa lamaservice-bmemprosesnya. - Error: Jika ada error (misalnya,
service-bgagal), span yang terkait akan ditandai merah, dan Anda bisa melihat detail error di atribut span. - Atribut (Metadata): Setiap span memiliki atribut yang memberikan konteks tambahan, seperti URL, metode HTTP, status code, ID pengguna, dll. Ini sangat membantu untuk memahami kondisi saat operasi terjadi.
- Context Propagation: Anda bisa melihat bagaimana Trace ID dan Span ID diwariskan dari
service-akeservice-b, memastikan semua operasi terkait dalam satu trace yang koheren.
Dengan ini, jika service-b mengalami masalah, Anda akan langsung melihat span yang gagal di Jaeger UI, lengkap dengan durasi dan potensi pesan error, tanpa harus menebak-nebak atau memeriksa log di banyak tempat.
7. Tips dan Best Practices untuk Observabilitas Lokal
- Nama Service yang Jelas: Selalu berikan nama service yang deskriptif (
OTEL_SERVICE_NAME) agar mudah diidentifikasi di Jaeger UI. - Konfigurasi via Environment Variables: Gunakan environment variables untuk mengatur endpoint OpenTelemetry Collector (
OTEL_EXPORTER_OTLP_ENDPOINT). Ini membuat konfigurasi lebih fleksibel tanpa mengubah kode. - Auto-instrumentation: Untuk framework umum (Express, Axios, database drivers), gunakan library
auto-instrumentationOpenTelemetry. Ini akan otomatis membuat span untuk operasi umum tanpa Anda harus menulis kode tracing manual. - Instrumentasi Manual (Jika Perlu): Untuk logika bisnis yang spesifik atau operasi yang tidak dicakup oleh auto-instrumentation, Anda bisa membuat span secara manual menggunakan
opentelemetry/api.const { trace } = require('@opentelemetry/api'); const tracer = trace.getTracer('my-app-tracer'); app.get('/custom-logic', (req, res) => { const parentSpan = trace.getSpan(req); // Ambil span yang ada dari request const customSpan = tracer.startSpan('custom-business-logic', { parent: parentSpan }); try { // Lakukan logika bisnis kompleks customSpan.setAttribute('user.id', '123'); customSpan.addEvent('step_1_completed'); // ... res.send('Custom logic executed'); } finally { customSpan.end(); } }); - Integrasikan Trace ID ke Log: Tambahkan Trace ID dan Span ID ke setiap log aplikasi Anda. Ini akan sangat membantu saat Anda ingin menghubungkan log spesifik dengan trace di Jaeger.
- Perhatikan Context Propagation: Pastikan Trace ID dan Span ID diteruskan antar service, biasanya melalui HTTP headers (W3C Trace Context adalah standar). Auto-instrumentation biasanya menangani ini, tetapi penting untuk memahaminya.
Kesimpulan
Debugging microservices secara lokal tidak harus menjadi pengalaman yang menyakitkan. Dengan Distributed Tracing yang didukung oleh OpenTelemetry dan divisualisasikan oleh Jaeger, Anda mendapatkan visibilitas yang belum pernah ada sebelumnya ke dalam sistem terdistribusi Anda. Ini mengubah “black box” menjadi “glass box”, memungkinkan Anda untuk dengan cepat mengidentifikasi bottleneck, melacak error, dan memahami aliran data antar service.
Mulai sekarang, jadikan observabilitas lokal sebagai bagian integral dari alur kerja pengembangan Anda. Investasi awal dalam setup ini akan sangat menghemat waktu dan mengurangi frustrasi Anda dalam jangka panjang, meningkatkan produktivitas dan kepuasan Anda sebagai developer.
🔗 Baca Juga
- Mengupas Tuntas Distributed Tracing dengan OpenTelemetry: Melacak Perjalanan Request di Sistem Terdistribusi
- Mengoptimalkan Pesan: Panduan Observabilitas dan Debugging untuk Aplikasi Berbasis Message Queue
- Bagaimana Melakukan Logging yang Efektif di Aplikasi Web Modern: Panduan Praktis untuk Observability
- Strategi Penanganan Error Komprehensif: Dari Frontend, Backend, hingga Integrasi Eksternal