Tampilan
Kenapa penting
Metrik dari bab sebelumnya memberi tahu bahwa ada masalah: error checkout naik ke 3% dan p95 melonjak ke 2 detik. Pertanyaan berikutnya selalu sama — request mana, kenapa, dan di bagian mana waktunya habis? Di sistem yang terdiri dari beberapa service, satu request checkout bisa melewati API gateway, order-service, inventory-service, dan payment gateway. Mencari jawabannya dengan grep di log teks bebas dari empat mesin berbeda sangat melelahkan. Structured logging membuat log bisa dicari seperti database, dan distributed tracing menampilkan perjalanan satu request sebagai satu pohon waktu. Keduanya disatukan oleh satu kunci: traceId. Kemampuan ini yang membedakan tim yang bisa menemukan akar insiden dalam lima menit dengan tim yang butuh lima jam.
Konsep inti
Structured logging JSON bawaan Spring Boot
Log teks biasa enak dibaca manusia, tetapi sulit diproses mesin. Log JSON berisi field terpisah (level, logger, message, field tambahan) sehingga Elasticsearch, Loki, atau Cloud Logging bisa memfilter level=ERROR AND service=order-service tanpa regex rapuh. Sejak Spring Boot 3.4, structured logging tersedia bawaan tanpa dependency tambahan:
yaml
# application.yml
spring:
application:
name: order-service
logging:
structured:
format:
console: ecs # pilihan bawaan: ecs, logstash, gelfFormat ecs mengikuti Elastic Common Schema. Satu baris log menjadi kira-kira seperti ini (dirapikan agar mudah dibaca; aslinya satu baris):
json
{
"@timestamp": "2026-10-23T07:15:02.481Z",
"log": { "level": "INFO", "logger": "com.toko.order.CheckoutService" },
"process": { "pid": 4120, "thread": { "name": "http-nio-8080-exec-3" } },
"service": { "name": "order-service" },
"message": "Order dibuat",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"ecs": { "version": "8.11" }
}Bentuk persis field (misalnya apakah bersarang atau datar) bergantung pada versi Boot dan format yang dipilih, jadi cek output aplikasimu sendiri. Pola umumnya: di lokal kamu tetap memakai log teks yang nyaman dibaca, dan di production mengaktifkan JSON lewat profile atau environment variable LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs.
Menambah konteks dengan MDC dan key-value
Field tambahan masuk ke log lewat MDC (Mapped Diagnostic Context) atau API fluent SLF4J. Structured logging menuliskan isinya sebagai field JSON.
java
// Potongan — SLF4J 2 fluent API
log.atInfo()
.setMessage("Pembayaran diproses")
.addKeyValue("orderId", order.id())
.addKeyValue("outcome", result.outcome())
.log();
// Atau MDC untuk konteks yang berlaku selama satu blok kerja
try (var ignored = MDC.putCloseable("orderId", order.id())) {
inventory.reserve(order);
log.info("Stok dipesan"); // field orderId ikut tercatat
}MDC.putCloseable memakai try-with-resources sehingga nilai otomatis dihapus — penting karena MDC menempel ke thread, dan thread di pool dipakai ulang oleh request lain.
Trace, span, dan W3C traceparent
OpenTelemetry (OTel) adalah standar terbuka untuk data telemetri: API, SDK, dan protokol OTLP. Konsep utamanya:
| Istilah | Arti |
|---|---|
| Trace | seluruh perjalanan satu request, diidentifikasi traceId (128 bit) |
| Span | satu unit kerja (request HTTP, query DB, panggilan gateway) dengan waktu mulai, durasi, dan atribut |
| Parent span | span pemanggil; span membentuk pohon |
| Context propagation | meneruskan traceId dan span induk ke service berikutnya |
Propagasi antar-service memakai header standar W3C Trace Context:
text
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ └ flags (01 = sampled)
│ │ └ parent span ID (16 hex)
│ └ trace ID (32 hex)
└ versiKetika order-service memanggil payment-service dengan header ini, payment-service membuat span baru sebagai anak dari 00f067aa0ba902b7 dengan traceId yang sama. Di UI tracing, hasilnya satu diagram waterfall:
text
POST /checkout order-service 820 ms
├─ SELECT orders ... order-service 12 ms
├─ POST /inventory/reserve inventory-service 95 ms
│ └─ UPDATE stock ... inventory-service 70 ms
└─ POST /payments payment-service 690 ms ← biang lambatnya
└─ POST https://gateway/charge payment-service 670 msTracing di Spring Boot 4
Spring Boot memakai Micrometer Tracing sebagai facade dan OpenTelemetry sebagai implementasi. Observation API dari bab metrik menghasilkan span sekaligus metrik, jadi request HTTP masuk, RestClient, dan JDBC/JdbcTemplate yang terinstrumentasi otomatis membuat span. Di Boot 4 ada starter khusus untuk kombinasi OpenTelemetry + OTLP:
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>yaml
# application.yml
management:
tracing:
sampling:
probability: 1.0 # dev: rekam semua; production biasanya 0.05–0.2
opentelemetry:
tracing:
export:
otlp:
endpoint: http://localhost:4318/v1/traces # OTLP over HTTPDua jalur OpenTelemetry
Proyek OpenTelemetry juga punya starter sendiri (opentelemetry-spring-boot-starter) dan Java agent yang menginstrumentasi lewat bytecode. Bab ini memakai spring-boot-starter-opentelemetry karena itulah yang didokumentasikan Spring Boot dan terintegrasi dengan Observation API, properti management.*, serta korelasi log. Pilih salah satu jalur, jangan dicampur.
Agar propagasi bekerja, panggil service lain lewat builder yang dikelola Spring — RestClient.Builder yang di-inject — bukan RestClient.create(). Builder dari Spring sudah dipasangi instrumentasi yang menambahkan header traceparent.
java
// Potongan — client ke payment-service dengan propagasi otomatis
@Component
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://payment-service:8080").build();
}
public PaymentResponse charge(PaymentRequest request) {
return restClient.post().uri("/payments").body(request).retrieve().body(PaymentResponse.class);
}
}Sampling
Merekam setiap span di sistem dengan ribuan request per detik mahal: CPU, jaringan, dan storage. Sampling memutuskan trace mana yang disimpan. Spring Boot memakai parent-based sampling: service pertama memutuskan berdasarkan management.tracing.sampling.probability, lalu keputusan itu dibawa di flag traceparent sehingga service berikutnya mengikuti — trace tidak terpotong di tengah. Untuk menyimpan semua trace yang error tetapi hanya sebagian yang sukses, dibutuhkan tail sampling di OpenTelemetry Collector, karena keputusan baru bisa diambil setelah trace selesai.
Jaeger atau Tempo via Docker
Untuk mencoba di lokal, jalankan Jaeger yang menerima OTLP langsung:
yaml
# compose.yaml (potongan)
services:
jaeger:
image: jaegertracing/jaeger:latest # pin ke versi tertentu di luar eksperimen lokal
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTPBuka http://localhost:16686, pilih service order-service, dan cari trace. Di stack Grafana, perannya diambil Tempo (penyimpanan trace), Loki (log), dan Prometheus (metrik), sering dengan OpenTelemetry Collector di tengah sebagai penerima OTLP yang bisa melakukan batching, filtering, dan tail sampling sebelum diteruskan.
Korelasi log, metrik, dan trace
Saat tracing aktif, Spring Boot otomatis menaruh traceId dan spanId di MDC. Log teks mendapat correlation ID dalam format [traceId-spanId], dan log JSON mendapat field traceId/spanId. Alur investigasi jadi seperti ini:
- Metrik — dashboard RED menunjukkan p95
/checkoutnaik. - Trace — buka trace lambat dari periode itu; terlihat span
POST /paymentsmemakan 690 ms. - Log — cari log dengan
traceIdyang sama; munculWARN"retry ke gateway, percobaan ke-3".
Grafana bisa membuat lompatan ini satu klik: dari log Loki ke trace Tempo lewat field traceId, dan dari titik di grafik latency ke contoh trace lewat exemplar (trace ID yang ditempelkan Micrometer ke bucket histogram ketika tracing aktif).
Apa yang tidak boleh masuk log
Log dikirim ke banyak sistem, disimpan berbulan-bulan, dan dibaca banyak orang. Anggap semua isi log bisa bocor.
| Jangan dicatat | Catat ini sebagai gantinya |
|---|---|
| password, PIN, OTP | "login gagal" + user ID |
token, API key, header Authorization, cookie sesi | nama client / jenis kredensial |
| nomor kartu lengkap, CVV | 4 digit terakhir atau token dari gateway |
| NIK, alamat, nomor telepon lengkap | ID internal |
| body request/response penuh | field yang relevan saja |
Hal yang sama berlaku untuk atribut span: jangan menaruh body request atau data pribadi sebagai atribut trace. Untuk record yang punya field sensitif, override toString() agar field itu disamarkan, karena log.info("{}", request) memanggil toString().
Contoh di dunia backend
Checkout yang menghasilkan log JSON terkorelasi, span kustom untuk langkah bisnis, dan tanpa data sensitif:
java
// Potongan — CheckoutService (Spring Boot 4, starter OpenTelemetry + structured logging aktif)
@Service
public class CheckoutService {
private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);
private final InventoryClient inventory;
private final PaymentClient payments;
private final ObservationRegistry observations;
public CheckoutService(InventoryClient inventory, PaymentClient payments, ObservationRegistry observations) {
this.inventory = inventory;
this.payments = payments;
this.observations = observations;
}
public CheckoutResult checkout(Order order, CardToken card) {
return Observation.createNotStarted("checkout.process", observations)
.lowCardinalityKeyValue("channel", order.channel().name()) // metrik + span
.highCardinalityKeyValue("order.id", order.id()) // span saja
.observe(() -> {
inventory.reserve(order.items());
PaymentResponse payment = payments.charge(new PaymentRequest(order.id(), order.total(), card));
log.atInfo()
.setMessage("Checkout selesai")
.addKeyValue("orderId", order.id())
.addKeyValue("paymentOutcome", payment.outcome())
.addKeyValue("cardLast4", card.last4()) // bukan nomor kartu
.log();
return new CheckoutResult(order.id(), payment.outcome());
});
}
}
// Token kartu dari gateway; toString disamarkan agar aman bila tidak sengaja di-log
public record CardToken(String token, String last4) {
@Override
public String toString() {
return "CardToken[last4=" + last4 + "]";
}
}Hasilnya, satu baris log JSON berisi orderId, paymentOutcome, cardLast4, serta traceId yang bisa ditempel ke Jaeger untuk melihat seluruh perjalanan checkout tersebut.
Pertanyaan interview
1. Apa beda trace dan span?
Trace mewakili seluruh perjalanan satu request di sistem dan diidentifikasi oleh satu traceId. Span adalah satu unit kerja di dalam trace — menangani request HTTP, query database, atau panggilan ke service lain — dengan waktu mulai, durasi, atribut, dan referensi ke span induknya. Kumpulan span dengan traceId sama membentuk pohon yang menunjukkan di mana waktu dihabiskan.
2. Bagaimana traceId bisa sama di beberapa service?
Lewat context propagation. Service pemanggil menambahkan header W3C traceparent berisi versi, traceId, ID span induk, dan flag sampling ke request keluar. Service penerima membaca header itu dan membuat span baru sebagai anak dengan traceId yang sama. Di Spring Boot ini otomatis bila memakai RestClient.Builder atau WebClient.Builder yang dikelola Spring; untuk messaging seperti Kafka konteks dibawa di header pesan.
3. Kenapa memakai sampling, dan apa risikonya?
Merekam semua trace pada trafik tinggi mahal di CPU, jaringan, dan storage, sehingga production biasanya hanya merekam sebagian. Risikonya, trace untuk request bermasalah yang langka bisa tidak terekam. Parent-based sampling menjaga trace tetap utuh lintas service, dan tail sampling di OpenTelemetry Collector bisa memprioritaskan trace yang error atau lambat.
Jebakan umum
- Membuat client HTTP sendiri.
RestClient.create()ataunew RestTemplate()tidak diinstrumentasi, sehingga headertraceparenttidak terkirim dan trace terputus. Inject builder dari Spring. - MDC bocor antar request.
MDC.puttanparemovemeninggalkan nilai di thread pool; request berikutnya ikut tercatat denganorderIdyang salah. PakaiMDC.putCloseabledengan try-with-resources. - Sampling 100% di production tanpa perhitungan. Volume span bisa melebihi volume log; ukur dulu biaya dan kapasitas backend tracing.
- Data sensitif di log atau atribut span. Mencatat seluruh objek request sering tanpa sadar ikut mencatat token atau nomor kartu. Override
toString()dan catat field yang perlu saja. - Merangkai string di pesan log.
log.info("Order " + id)menghasilkan pesan berbeda untuk tiap order sehingga sulit dikelompokkan; taruh nilai variabel sebagai key-value atau placeholder{}. - Mencampur dua instrumentasi. Memasang Java agent OpenTelemetry bersamaan dengan starter Spring Boot bisa membuat span ganda.
Ringkasan
logging.structured.format.console(ecs,logstash,gelf) mengubah log Spring Boot menjadi JSON tanpa dependency tambahan; konteks tambahan masuk lewat MDC atau key-value SLF4J.- Trace adalah perjalanan satu request; span adalah unit kerja di dalamnya; header W3C
traceparentmembawa konteks antar-service. - Di Boot 4,
spring-boot-starter-opentelemetry+management.opentelemetry.tracing.export.otlp.*mengirim span ke Jaeger, Tempo, atau OpenTelemetry Collector. management.tracing.sampling.probabilitymengatur berapa banyak trace yang direkam; keputusan diwariskan ke service berikutnya.traceIddi log, trace, dan exemplar metrik menyatukan ketiga pilar; jangan pernah mencatat password, token, atau data kartu.