Di monolith, satu permintaan yang melambat bisa dikejar lewat satu stack trace dan satu berkas log. Di microservices, permintaan yang sama terfragmentasi: log svc-order berada di mesin A, log svc-pembayaran di mesin B, dan tidak ada satupun berkas yang memuat cerita utuh. Observability adalah properti sistem yang bisa diinterogasi dari luar — tanpa menambah cetak debug baru dan men-deploy ulang. Alatnya tiga pilar: log, metrik, dan trace; pilar ketiga — distributed tracing — adalah perekat yang menjahit ketiganya lintas proses. Chapter ini membangun mesinnya langsung: konteks trace lewat ThreadLocal, span yang membentuk pohon, dan log terstruktur yang membawa identitas permintaan.
Tiga Pilar Observability #
| Pilar | Pertanyaan yang dijawab | Bentuk khas | Granularitas |
|---|---|---|---|
| Log | “apa yang terjadi tepatnya?” | baris kejadian per kejadian | tinggi, mahal disimpan semua |
| Metrik | “berapa banyak dan seberapa sering?” | angka teragregasi per rentang waktu | rendah, murah |
| Trace | “permintaan ini lewat mana dan lambat di mana?” | pohon span per permintaan | per permintaan |
Ketiganya saling menguatkan: metrik tingkat_error yang menanjak mengarahkan kita ke rentang waktu; trace pada rentang itu menunjukkan service mana yang menyerap waktu; log dengan traceId yang sama membuktikan apa yang terjadi di dalamnya — log teks polos tanpa identitas permintaan tidak bisa diinterogasi seperti ini.
Trace Context dan Propagasi #
Satu trace merekam seluruh perjalanan satu permintaan; di dalamnya, setiap potongan pekerjaan dicatat sebagai span — dengan nama, layanan pelaku, dan hubungan ortu-anak. Kuncinya ada pada satu nilai kecil yang harus menyeberangi batas proses: trace context. Program berikut memodelkan propagasi itu dengan ThreadLocal dan ID beku:
import java.util.ArrayList;
import java.util.List;
/** Distributed tracing mini: trace context via ThreadLocal dan jejak span lintas service. */
public class JejakTerdistribusi {
/** Konteks yang mengalir bersama eksekusi: id trace, id span, id span ortu. */
record KonteksTrace(String traceId, String spanId, String spanIdOrtu) {}
/** ThreadLocal membawa konteks sehingga seluruh method ikut tanpa parameter tambahan. */
static final ThreadLocal<KonteksTrace> KONTEKS = new ThreadLocal<>();
/** Satu span tercatat: potongan pekerjaan dalam satu trace. */
record Span(String traceId, String spanId, String spanIdOrtu, String layanan, String nama) {}
static final List<Span> SPAN = new ArrayList<>();
/** Log terstruktur: setiap baris otomatis membawa trace id dan span id. */
static void log(String pesan) {
KonteksTrace k = KONTEKS.get();
System.out.println(" [trace=" + k.traceId() + " span=" + k.spanId() + "] " + pesan);
}
/** Buka satu span: id baru, catat, jalankan isi, lalu pulihkan konteks pemanggil. */
static void span(String layanan, String nama, Runnable isi) {
KonteksTrace ortu = KONTEKS.get();
String traceId = (ortu == null) ? "t-4f21" : ortu.traceId();
String spanBaru = "s-" + (SPAN.size() + 1);
String ortuId = (ortu == null) ? null : ortu.spanId();
KONTEKS.set(new KonteksTrace(traceId, spanBaru, ortuId));
SPAN.add(new Span(traceId, spanBaru, ortuId, layanan, nama));
log(layanan + " mulai span \"" + nama + "\"");
isi.run();
log(layanan + " selesai span \"" + nama + "\"");
KONTEKS.set(ortu); // kembalikan konteks pemanggil
}
/** Panggilan lintas service: pada sistem nyata konteks menumpang header HTTP. */
static void panggilLayanan(String layanan, String namaSpan, Runnable isi) {
span(layanan, namaSpan, isi);
}
public static void main(String[] args) {
System.out.println("== Log terstruktur selama satu permintaan berjalan ==");
span("svc-order", "POST /api/pesanan", () -> {
span("svc-order", "validasi pesanan", () -> log("isi keranjang diperiksa"));
panggilLayanan("svc-pembayaran", "POST /tagih", () -> {
span("svc-pembayaran", "tagih kartu", () -> log("permintaan ke gateway"));
});
panggilLayanan("svc-notifikasi", "kirim email", () -> log("email konfirmasi"));
});
System.out.println();
System.out.println("== Jejak lengkap: seluruh span milik satu traceId ==");
System.out.println("traceId = " + SPAN.get(0).traceId() + ", jumlah span = " + SPAN.size());
for (Span s : SPAN) {
String ortu = (s.spanIdOrtu() == null) ? "-" : s.spanIdOrtu();
System.out.println(" " + s.spanId() + " (ortu " + String.format("%-4s", ortu) + ") "
+ String.format("%-16s", s.layanan()) + s.nama());
}
}
}Output yang Dihasilkan:
== Log terstruktur selama satu permintaan berjalan ==
[trace=t-4f21 span=s-1] svc-order mulai span "POST /api/pesanan"
[trace=t-4f21 span=s-2] svc-order mulai span "validasi pesanan"
[trace=t-4f21 span=s-2] isi keranjang diperiksa
[trace=t-4f21 span=s-2] svc-order selesai span "validasi pesanan"
[trace=t-4f21 span=s-3] svc-pembayaran mulai span "POST /tagih"
[trace=t-4f21 span=s-4] svc-pembayaran mulai span "tagih kartu"
[trace=t-4f21 span=s-4] permintaan ke gateway
[trace=t-4f21 span=s-4] svc-pembayaran selesai span "tagih kartu"
[trace=t-4f21 span=s-3] svc-pembayaran selesai span "POST /tagih"
[trace=t-4f21 span=s-5] svc-notifikasi mulai span "kirim email"
[trace=t-4f21 span=s-5] email konfirmasi
[trace=t-4f21 span=s-5] svc-notifikasi selesai span "kirim email"
[trace=t-4f21 span=s-1] svc-order selesai span "POST /api/pesanan"
== Jejak lengkap: seluruh span milik satu traceId ==
traceId = t-4f21, jumlah span = 5
s-1 (ortu - ) svc-order POST /api/pesanan
s-2 (ortu s-1 ) svc-order validasi pesanan
s-3 (ortu s-1 ) svc-pembayaran POST /tagih
s-4 (ortu s-3 ) svc-pembayaran tagih kartu
s-5 (ortu s-1 ) svc-notifikasi kirim emailMari kita bedah mesinnya:
ThreadLocal<KonteksTrace> KONTEKS: konteks trace tersimpan per thread sehingga seluruh method yang dieksekusi thread tersebut otomatis “tahu” trace yang sedang berjalan — tanpa parameter tambahan di setiap method. Mekanisme yang sama dipakai SLF4J lewat MDC (Mapped Diagnostic Context) untuk menstempel log.span(...): membuka span baru — menurunkantraceIddari ortu (atau menciptakant-4f21bila ini span akar), membuatspanIdberikutnya, mencatat hubungan ortu, menjalankan isi, lalu memulihkan konteks ortu (KONTEKS.set(ortu)). Pemulihan inilah yang membuat span bertingkat rapi: begituvalidasi pesananselesai, konteks kembali kePOST /api/pesanan.panggilLayanan(...): penanda batas proses. Pada sistem nyata, di sinilah konteks menumpang header HTTP — standar W3C Trace Context menuliskannya sebagaitraceparent: 00-{traceId}-{spanId}-01— dan service tetangga membaca header itu untuk melanjutkan trace yang sama, bukan memulai trace baru.log(...): log terstruktur. Setiap baris otomatis membawa[trace=... span=...]dariKONTEKS— pada bagian keluaran pertama, kelima belas baris log dari tiga service yang berbeda ternyata satu keluarga.- Tabel span di bagian kedua: hasil akhir yang dikirim ke backend tracing (Zipkin, Jaeger, atau vendor SaaS). Pohonnya terbaca dari kolom ortu:
s-2,s-3,s-5anak langsung daris-1;s-4anak daris-3— grafik yang menampilkan pemanggilan sekaligus penyusunan waktu.
Log Terstruktur dan Korelasi #
Baris log [trace=t-4f21 span=s-1] pada program kita adalah bentuk paling sederhana dari log terstruktur. Produksi melangkah lebih jauh: log ditulis sebagai JSON ({"ts":"...","level":"INFO","service":"svc-order","traceId":"t-4f21","msg":"..."}) dan dikumpulkan ke satu pusat (pattern log aggregation) — Elasticsearch, Loki, atau layanan sejenis — sehingga satu kueri traceId = t-4f21 mengembalikan seluruh baris dari seluruh service dalam satu urutan. Pustaka logging Java standarnya SLF4J sebagai antarmuka dan Logback sebagai implementasi; MDC (MDC.put("traceId", ...)) berdiri di atas ThreadLocal persis seperti KONTEKS milik kita.
Korelasi log menjadi kewajiban begitu sistem melewati dua-tiga service. Tanpa traceId di setiap baris, investigasi insiden berubah menjadi dugaan tanpa bukti yang saling dilempar antar tim; dengan traceId, seluruh cerita satu permintaan ditarik dalam satu kueri.
Membaca Jejak Saat Insiden #
Nilai observability terasa saat sistem sehat tapi pengguna mengeluh lambat. Trace menjawab dengan struktur: span yang menyerap mayoritas waktu adalah tersangka utama; span ortu yang selesai cepat sementara anak-anaknya menumpuk menandakan antrean di tengah; gap antara span ortu dan anak pertama menandakan waktu yang hilang di jaringan atau antrean broker. Dikombinasikan dengan metrik per service dan health check dari chapter Service Discovery, pohon span memberi peta lengkap: apa yang terjadi, di mana, seberapa sering.
Satu chapter tersisa di section ini — dan isinya jujur: seluruh peralatan yang kita bangun (registry, broker, tracing, saga) adalah harga yang harus dibayar per batas service. Chapter penutup merangkum daftar harganya dan kerangka memutuskan kapan pembayarannya masuk akal.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
JejakTerdistribusi.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
/** Distributed tracing mini: trace context via ThreadLocal dan jejak span lintas service. */
public class JejakTerdistribusi {
/** Konteks yang mengalir bersama eksekusi: id trace, id span, id span ortu. */
record KonteksTrace(String traceId, String spanId, String spanIdOrtu) {}
/** ThreadLocal membawa konteks sehingga seluruh method ikut tanpa parameter tambahan. */
static final ThreadLocal<KonteksTrace> KONTEKS = new ThreadLocal<>();
/** Satu span tercatat: potongan pekerjaan dalam satu trace. */
record Span(String traceId, String spanId, String spanIdOrtu, String layanan, String nama) {}
static final List<Span> SPAN = new ArrayList<>();
/** Log terstruktur: setiap baris otomatis membawa trace id dan span id. */
static void log(String pesan) {
KonteksTrace k = KONTEKS.get();
System.out.println(" [trace=" + k.traceId() + " span=" + k.spanId() + "] " + pesan);
}
/** Buka satu span: id baru, catat, jalankan isi, lalu pulihkan konteks pemanggil. */
static void span(String layanan, String nama, Runnable isi) {
KonteksTrace ortu = KONTEKS.get();
String traceId = (ortu == null) ? "t-4f21" : ortu.traceId();
String spanBaru = "s-" + (SPAN.size() + 1);
String ortuId = (ortu == null) ? null : ortu.spanId();
KONTEKS.set(new KonteksTrace(traceId, spanBaru, ortuId));
SPAN.add(new Span(traceId, spanBaru, ortuId, layanan, nama));
log(layanan + " mulai span \"" + nama + "\"");
isi.run();
log(layanan + " selesai span \"" + nama + "\"");
KONTEKS.set(ortu); // kembalikan konteks pemanggil
}
/** Panggilan lintas service: pada sistem nyata konteks menumpang header HTTP. */
static void panggilLayanan(String layanan, String namaSpan, Runnable isi) {
span(layanan, namaSpan, isi);
}
public static void main(String[] args) {
System.out.println("== Log terstruktur selama satu permintaan berjalan ==");
span("svc-order", "POST /api/pesanan", () -> {
span("svc-order", "validasi pesanan", () -> log("isi keranjang diperiksa"));
panggilLayanan("svc-pembayaran", "POST /tagih", () -> {
span("svc-pembayaran", "tagih kartu", () -> log("permintaan ke gateway"));
});
panggilLayanan("svc-notifikasi", "kirim email", () -> log("email konfirmasi"));
});
System.out.println();
System.out.println("== Jejak lengkap: seluruh span milik satu traceId ==");
System.out.println("traceId = " + SPAN.get(0).traceId() + ", jumlah span = " + SPAN.size());
for (Span s : SPAN) {
String ortu = (s.spanIdOrtu() == null) ? "-" : s.spanIdOrtu();
System.out.println(" " + s.spanId() + " (ortu " + String.format("%-4s", ortu) + ") "
+ String.format("%-16s", s.layanan()) + s.nama());
}
}
}