Log Context and MDC
Log Context, MDC, ThreadLocal, and Context Propagation
Cara membawa request context, correlation ID, trace ID, span ID, tenant ID, actor ID, request ID, dan business key ke log menggunakan MDC/ThreadLocal secara aman di Java/JAX-RS, thread pool, async job, virtual thread, messaging, dan production systems.
Cheatsheet Observability Part 008 — Log Context and MDC
Tujuan Part Ini
Part ini membahas log context dan MDC sebagai mekanisme agar setiap log event memiliki konteks yang cukup untuk dikorelasikan saat production debugging.
Dalam sistem enterprise Java 17+ / JAX-RS / Jakarta RESTful Web Services, satu request bisa melewati banyak boundary:
NGINX / ingress
-> JAX-RS request filter
-> resource method
-> service layer
-> PostgreSQL via JDBC/MyBatis/JPA
-> Redis
-> Kafka/RabbitMQ publish
-> downstream HTTP call
-> background worker
-> Camunda/workflow
Tanpa context propagation, log akan terlihat seperti potongan-potongan terpisah:
Order submission failed
Database timeout
Message published
Retry scheduled
Dengan context yang benar, log menjadi rantai evidence:
correlation_id=corr-abc trace_id=trace-123 order_id=O-91821 tenant_id=T-7 actor_id=A-44
Setelah membaca part ini, Anda harus mampu:
- memahami apa itu MDC dan hubungannya dengan ThreadLocal
- menentukan field context yang perlu masuk log
- memahami lifecycle request context di JAX-RS filter
- mencegah context leak antar request
- memahami masalah context loss pada thread pool, async execution, scheduler, messaging, dan virtual thread
- menghubungkan MDC dengan OpenTelemetry trace ID/span ID
- mereview PR yang menyentuh correlation ID, request context, logging filter, executor, dan async jobs
Ringkasan Mental Model
Log context adalah metadata yang menjawab:
Log ini milik request apa?
Log ini milik trace apa?
Log ini milik tenant/customer mana?
Log ini terkait actor/user mana?
Log ini terkait business entity apa?
Log ini terjadi pada operation apa?
Log ini bagian dari causal chain mana?
MDC adalah mekanisme umum di ekosistem logging Java untuk menyimpan context per execution thread.
Konsepnya:
Request masuk
-> filter membaca/generate correlation_id
-> context dimasukkan ke MDC
-> semua log di thread tersebut otomatis membawa field MDC
-> request selesai
-> MDC dibersihkan
Contoh output log:
{
"timestamp": "2026-07-11T15:20:31.123Z",
"level": "INFO",
"service": "order-service",
"event": "order.submission.completed",
"message": "Order submission completed",
"correlation_id": "corr-20260711-9918",
"trace_id": "7f1d0b1c9f1e4b5d98c7",
"span_id": "a93b12f9d441",
"request_id": "req-771",
"tenant_id": "tenant-123",
"actor_id": "actor-456",
"order_id": "O-91821"
}
Tanpa MDC, setiap log statement harus mengisi field ini manual. Itu rawan lupa, tidak konsisten, dan sulit dijaga.
Namun MDC bukan magic. MDC bisa hilang atau bocor jika thread berpindah, executor digunakan, async processing terjadi, atau cleanup tidak benar.
Apa Itu MDC?
MDC adalah singkatan dari Mapped Diagnostic Context.
Dalam SLF4J, API umumnya:
import org.slf4j.MDC;
MDC.put("correlation_id", correlationId);
MDC.put("tenant_id", tenantId);
MDC.put("actor_id", actorId);
log.info("order submission started order_id={}", orderId);
MDC.clear();
Logging backend seperti Logback atau Log4j2 dapat mengeluarkan MDC field ke JSON log.
Contoh Logback pattern sederhana:
%X{correlation_id} %X{tenant_id} %X{actor_id} %msg%n
Dalam JSON encoder, MDC biasanya dapat dimasukkan sebagai fields:
{
"correlation_id": "corr-abc",
"tenant_id": "tenant-123",
"actor_id": "actor-456",
"message": "order submission started"
}
MDC bukan storage bisnis
MDC hanya untuk diagnostic context. Jangan gunakan MDC sebagai sumber kebenaran business logic.
Buruk:
String tenantId = MDC.get("tenant_id");
repository.findOrders(tenantId);
Lebih baik:
OrderContext context = requestContext.getOrderContext();
repository.findOrders(context.tenantId());
MDC boleh mirror context untuk logging, tetapi domain logic harus menggunakan explicit context object.
MDC dan ThreadLocal
Secara mental model, MDC biasanya berbasis ThreadLocal atau mekanisme yang serupa di logging implementation.
Artinya context melekat pada thread eksekusi saat ini.
Thread A MDC: correlation_id=corr-1
Thread B MDC: correlation_id=corr-2
Jika request A diproses di Thread A, semua log di Thread A mendapat corr-1.
Masalah muncul ketika:
- thread digunakan ulang oleh pool
- request berpindah ke thread lain
- async task dijalankan di executor
- CompletableFuture dipakai
- scheduler/background job berjalan tanpa request
- messaging consumer memakai worker pool
- virtual thread digunakan dengan asumsi lama
- reactive runtime tidak menjamin ThreadLocal continuity
Thread reuse dan context leak
Application server menggunakan thread pool. Thread yang sama bisa memproses request berbeda.
Jika MDC tidak dibersihkan:
Request 1 sets tenant_id=T1
Request 1 selesai tetapi MDC tidak clear
Request 2 diproses di thread yang sama
Request 2 log membawa tenant_id=T1 secara salah
Ini berbahaya.
Dampaknya:
- incident debugging salah arah
- tenant/customer impact salah
- audit investigation bisa misleading
- privacy boundary bisa terlihat dilanggar
- log query menghasilkan false evidence
Prinsip:
Every MDC put at request/job boundary must have deterministic cleanup.
Field Context yang Umum
Tidak semua field harus selalu ada. Tetapi field berikut umum untuk enterprise backend system.
| Field | Tujuan | Catatan |
|---|---|---|
correlation_id | Menghubungkan flow lintas service/event | Biasanya dibawa dari ingress atau dibuat jika tidak ada |
request_id | Identitas request individual | Bisa berbeda dari correlation ID |
trace_id | Distributed trace ID | Dari OpenTelemetry/W3C trace context |
span_id | Span aktif saat log dibuat | Berguna untuk log-trace correlation |
tenant_id | Scope tenant/customer | Harus sesuai privacy/internal policy |
actor_id | User/service actor | Jangan log PII langsung jika tidak allowed |
actor_type | user/system/service/job | Membantu audit/debugging |
session_id | Session correlation | Biasanya sensitif; sering tidak boleh raw |
business_key | Quote/order/process key | Gunakan nama spesifik jika bisa |
quote_id | Quote entity | Business debugging |
order_id | Order entity | Business debugging |
process_instance_id | Workflow instance | Camunda/workflow debugging |
event_id | Message/event identity | Messaging debugging |
idempotency_key | Duplicate/retry detection | Bisa sensitif; cek policy |
operation | Nama operasi teknis/domain | Queryability |
service | Service name | Biasanya resource attribute/log field global |
environment | dev/stage/prod | Global field |
version | build/version/image | Release correlation |
Field yang harus hati-hati
Beberapa field sangat berguna tetapi berisiko:
user_id -> berguna, tetapi privacy-sensitive
customer_id -> berguna, tetapi tenancy/commercial sensitive
tenant_id -> berguna, tetapi high cardinality jika jadi metric label
order_id -> berguna di log, buruk sebagai metric label
request_id -> berguna di log, buruk sebagai metric label
session_id -> sering sensitive; prefer hash atau jangan log
ip_address -> privacy-sensitive; cek policy
authorization -> jangan log
cookie -> jangan log
Rule praktis:
Context field boleh bagus untuk log query, tetapi belum tentu aman untuk metric label.
Request Context di JAX-RS
Di JAX-RS, tempat umum untuk setup MDC adalah ContainerRequestFilter.
Contoh sederhana:
@Provider
@Priority(Priorities.AUTHENTICATION)
public class RequestContextFilter implements ContainerRequestFilter, ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext requestContext) {
String correlationId = getOrCreateCorrelationId(requestContext);
String requestId = UUID.randomUUID().toString();
MDC.put("correlation_id", correlationId);
MDC.put("request_id", requestId);
MDC.put("http_method", requestContext.getMethod());
MDC.put("http_path", requestContext.getUriInfo().getPath());
requestContext.setProperty("correlation_id", correlationId);
requestContext.setProperty("request_id", requestId);
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
try {
// optional response-end logging can happen here
} finally {
MDC.clear();
}
}
}
Namun implementasi production harus lebih hati-hati.
Cleanup harus reliable
Response filter tidak selalu cukup jika exception terjadi sebelum response filter? Bergantung framework/container behavior. Banyak sistem tetap memanggil response filter, tetapi Anda harus verifikasi.
Pola yang lebih aman bisa menggunakan Servlet filter wrapping seluruh chain:
public class MdcServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
setupMdc((HttpServletRequest) request);
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
Route template vs raw path
Jangan hanya simpan raw path jika mengandung ID:
/orders/O-91821/items/123
Untuk low-cardinality grouping, gunakan route template:
/orders/{orderId}/items/{itemId}
Tetapi route template kadang baru diketahui setelah resource matching. Jadi setup awal bisa simpan correlation ID, lalu endpoint template bisa ditambahkan di later filter/interceptor jika tersedia.
Internal verification checklist
[ ] Apakah ada JAX-RS/Servlet filter untuk setup MDC?
[ ] Apakah MDC cleanup dijamin dengan finally?
[ ] Apakah exception path tetap cleanup?
[ ] Apakah correlation_id dibuat jika header tidak ada?
[ ] Apakah incoming correlation_id divalidasi/sanitized?
[ ] Apakah request_id baru dibuat per request?
[ ] Apakah route template tersedia, bukan hanya raw path?
[ ] Apakah sensitive header/body tidak masuk MDC?
Correlation ID di MDC
correlation_id adalah field paling penting untuk menghubungkan banyak log dalam satu business/technical flow.
Biasanya sumber correlation ID:
Incoming HTTP header
Generated at edge/gateway
Generated by application if missing
Propagated from message header
Generated by scheduler/job for job execution
Contoh header internal yang mungkin digunakan:
X-Correlation-ID
X-Request-ID
traceparent
Jangan mengarang standar internal. Verifikasi di team/platform.
Generate jika missing
private String getOrCreateCorrelationId(ContainerRequestContext ctx) {
String incoming = ctx.getHeaderString("X-Correlation-ID");
if (isValidCorrelationId(incoming)) {
return incoming;
}
return UUID.randomUUID().toString();
}
Validasi incoming correlation ID
Jangan menerima string sembarang tanpa batas.
Risiko:
- log injection
- extremely long value
- invalid characters
- spoofing internal context
- query breakage
Contoh validation rule:
max length: 128
allowed chars: alphanumeric, dash, underscore, dot
no newline/control chars
Correlation ID vs trace ID
Correlation ID dan trace ID bisa berbeda.
correlation_id -> business/operational flow identifier, often propagated by app/gateway
tace_id -> distributed tracing identifier, managed by tracing system
Trace ID sangat bagus untuk trace backend. Correlation ID sering lebih fleksibel untuk support, logs, business workflows, dan async flow.
Di sistem modern, idealnya log punya keduanya:
correlation_id=corr-abc
trace_id=7f1d0b1c9f1e4b5d98c7
Trace ID dan Span ID di Logs
Jika OpenTelemetry aktif, log sebaiknya mengandung trace ID dan span ID agar engineer bisa pindah dari log ke trace.
Contoh:
{
"level": "ERROR",
"message": "dependency call failed",
"trace_id": "7f1d0b1c9f1e4b5d98c7",
"span_id": "a93b12f9d441",
"correlation_id": "corr-8891"
}
Cara mendapat trace ID
Bergantung stack. Dengan OpenTelemetry Java, trace context biasanya berada pada current context.
Pseudo-code:
Span currentSpan = Span.current();
SpanContext spanContext = currentSpan.getSpanContext();
if (spanContext.isValid()) {
MDC.put("trace_id", spanContext.getTraceId());
MDC.put("span_id", spanContext.getSpanId());
}
Namun banyak OTel/logback integration dapat otomatis inject trace/span ID ke logs. Verifikasi setup internal.
Pitfall
trace_id missing di log karena log bridge belum dikonfigurasi
trace_id berbeda dari correlation_id dan engineer bingung
span_id stale karena MDC tidak diperbarui ketika span berubah
manual MDC trace_id conflict dengan auto instrumentation
Jika trace/span ID di-inject otomatis oleh logging instrumentation, jangan overwrite manual tanpa alasan.
Internal verification checklist
[ ] Apakah logs mengandung trace_id dan span_id?
[ ] Apakah trace_id bisa dipakai membuka trace backend?
[ ] Apakah correlation_id juga ada?
[ ] Apakah trace/span injection otomatis atau manual?
[ ] Apakah manual MDC tidak konflik dengan OTel log correlation?
[ ] Apakah async spans tetap muncul benar di log?
Tenant ID dan Actor ID
Tenant dan actor context sangat berguna dalam enterprise system, tetapi juga sensitif.
Tenant ID
Tenant ID membantu menjawab:
Tenant mana yang terdampak?
Apakah incident isolated ke tenant tertentu?
Apakah latency spike hanya terjadi pada tenant besar?
Apakah data/config tenant tertentu menyebabkan failure?
Namun tenant ID bisa dianggap sensitive atau high-cardinality. Di log, biasanya masih berguna. Di metric label, harus hati-hati.
Actor ID
Actor ID membantu menjawab:
Siapa yang melakukan action?
Apakah action dilakukan user, system, integration, atau job?
Apakah actor punya permission?
Apakah ada impersonation/delegation?
Tapi actor ID mungkin personal data. Verifikasi policy.
Jangan log display name/email sembarangan
Buruk:
MDC.put("user_email", user.email());
MDC.put("user_name", user.fullName());
Lebih aman:
MDC.put("actor_id", user.id());
MDC.put("actor_type", "user");
Atau jika policy mengharuskan masking/hash:
MDC.put("actor_hash", hash(user.id()));
Internal verification checklist
[ ] Apakah tenant_id boleh masuk application log?
[ ] Apakah actor_id boleh masuk application log?
[ ] Apakah email/name dilarang atau dimasking?
[ ] Apakah service-to-service actor dibedakan dari human actor?
[ ] Apakah impersonation/delegation context tersedia?
[ ] Apakah tenant_id tidak dipakai sembarangan sebagai metric label?
Business Key Context
Untuk CPQ/order management, purely technical ID tidak cukup.
Business debugging sering membutuhkan:
quote_id
order_id
customer/account id jika allowed
process_instance_id
approval_id
fulfillment_request_id
fallout_id
amendment_id
cancellation_id
reconciliation_batch_id
Business key harus spesifik
Daripada generic:
{"business_key": "O-91821"}
Lebih queryable:
{"order_id": "O-91821"}
Generic business_key masih berguna untuk workflow engine/Camunda, tetapi field spesifik lebih baik untuk dashboard/search.
Jangan masukkan terlalu awal jika belum diketahui
Pada request start, order ID mungkin belum diparse. Tambahkan saat diketahui:
MDC.put("order_id", orderId);
log.info("order submission started");
Pastikan cleanup atau scope-nya benar jika processing lanjut ke entity lain.
Scoped MDC update
Gunakan helper untuk temporary MDC agar tidak bocor.
try (MdcScope ignored = MdcScope.put("order_id", orderId)) {
log.info("order validation started");
validate(order);
}
Helper bisa mengembalikan nilai lama setelah scope selesai.
Internal verification checklist
[ ] Apakah quote_id/order_id masuk log pada lifecycle event penting?
[ ] Apakah business key tidak bocor antar entity dalam thread yang sama?
[ ] Apakah business key policy sesuai privacy/security requirement?
[ ] Apakah process_instance_id tersedia untuk workflow debugging?
[ ] Apakah event_id/message_id tersedia untuk messaging debugging?
[ ] Apakah idempotency_key boleh masuk log atau harus di-hash?
MDC Cleanup
MDC cleanup adalah invariant penting.
If you set MDC at a boundary, you must clear/restore MDC at the same boundary.
Request boundary
try {
setupMdc(request);
chain.doFilter(request, response);
} finally {
MDC.clear();
}
Scoped field boundary
Jika menambahkan field sementara:
String previous = MDC.get("order_id");
try {
MDC.put("order_id", orderId);
processOrder(orderId);
} finally {
if (previous == null) {
MDC.remove("order_id");
} else {
MDC.put("order_id", previous);
}
}
Helper pattern
public final class MdcScope implements AutoCloseable {
private final String key;
private final String previous;
private MdcScope(String key, String value) {
this.key = key;
this.previous = MDC.get(key);
MDC.put(key, value);
}
public static MdcScope put(String key, String value) {
return new MdcScope(key, value);
}
@Override
public void close() {
if (previous == null) {
MDC.remove(key);
} else {
MDC.put(key, previous);
}
}
}
Usage:
try (MdcScope ignored = MdcScope.put("order_id", orderId)) {
log.info("order enrichment started");
}
Internal verification checklist
[ ] Apakah MDC.clear() dipanggil di finally pada boundary request/job/message?
[ ] Apakah temporary MDC fields di-restore, bukan hanya ditimpa?
[ ] Apakah test membuktikan context tidak bocor antar request?
[ ] Apakah exception path tetap cleanup?
[ ] Apakah framework async behavior tidak melewati cleanup?
Async Context Loss
MDC berbasis thread. Jika work pindah thread, context bisa hilang.
Contoh:
MDC.put("correlation_id", correlationId);
CompletableFuture.runAsync(() -> {
log.info("async work started"); // correlation_id mungkin hilang
});
Karena task berjalan di thread lain, MDC thread awal tidak otomatis ikut.
Executor propagation
Salah satu pola adalah capture context saat submit dan restore saat run.
public class MdcPropagatingRunnable implements Runnable {
private final Runnable delegate;
private final Map<String, String> contextMap;
public MdcPropagatingRunnable(Runnable delegate) {
this.delegate = delegate;
this.contextMap = MDC.getCopyOfContextMap();
}
@Override
public void run() {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
MDC.clear();
}
delegate.run();
} finally {
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
}
}
Usage:
executor.submit(new MdcPropagatingRunnable(() -> {
log.info("async enrichment completed");
}));
Better: standard context propagation
Jika OpenTelemetry digunakan, lebih baik memahami OTel context propagation dan menggunakan instrumentation/helper resmi. Jangan membuat propagation custom yang konflik dengan tracing.
Internal verification checklist
[ ] Apakah CompletableFuture/task executor mempertahankan MDC?
[ ] Apakah executor wrapper digunakan konsisten?
[ ] Apakah OTel context juga ikut, bukan hanya MDC?
[ ] Apakah cleanup terjadi di worker thread?
[ ] Apakah async task punya correlation_id saat log?
[ ] Apakah context lama tidak bocor dari worker thread pool?
Thread Pool Propagation
Thread pool memperburuk risiko context leak karena thread digunakan ulang.
Masalah umum:
Task A set correlation_id=corr-A
Task A selesai tanpa cleanup
Task B jalan di thread sama
Task B log membawa corr-A
Executor wrapper harus:
- capture context dari caller
- set context di worker sebelum delegate run
- restore previous worker context setelah selesai
Jangan hanya MDC.setContextMap(contextMap) tanpa restore.
Scheduled executor
Scheduled task sering tidak punya request context. Buat context baru untuk job execution.
String jobRunId = UUID.randomUUID().toString();
MDC.put("correlation_id", "job-" + jobRunId);
MDC.put("job_name", "order-reconciliation");
MDC.put("job_run_id", jobRunId);
Lalu cleanup.
Internal verification checklist
[ ] Apakah semua custom executor punya MDC/OTel propagation strategy?
[ ] Apakah scheduled jobs membuat context baru?
[ ] Apakah job_run_id tersedia?
[ ] Apakah worker thread cleanup selalu terjadi?
[ ] Apakah ada test concurrent untuk context leak?
Virtual Thread Awareness
Java modern dapat menggunakan virtual threads. Virtual threads mengubah beberapa asumsi, tetapi tidak menghilangkan kebutuhan context discipline.
ThreadLocal dapat bekerja dengan virtual threads, tetapi perhatian utamanya:
- jumlah virtual thread sangat besar
- context harus tetap dibersihkan
- jangan menyimpan object besar di ThreadLocal/MDC
- framework/library compatibility harus diverifikasi
- context propagation antar task tetap perlu dipahami
Risk
Jika MDC berisi banyak field besar atau object serialization, virtual thread scale bisa memperbesar memory overhead.
MDC sebaiknya hanya berisi string kecil dan stabil.
Internal verification checklist
[ ] Apakah aplikasi memakai virtual threads?
[ ] Apakah logging framework/MDC compatible dengan mode tersebut?
[ ] Apakah MDC hanya berisi string kecil?
[ ] Apakah cleanup tetap dilakukan?
[ ] Apakah context propagation behavior sudah diuji?
Reactive/Async Caveat
Jika aplikasi memakai reactive runtime, event loop, atau asynchronous framework, ThreadLocal/MDC sering tidak cukup.
Masalah:
Satu logical request bisa berpindah antar thread.
Satu thread bisa memproses banyak logical request.
ThreadLocal tidak mewakili logical context.
JAX-RS tradisional biasanya thread-per-request, tetapi beberapa server/framework mendukung async response, streaming, atau reactive extension.
Jika menggunakan reactive stack, gunakan context mechanism framework tersebut dan bridge ke logging.
Contoh konsep:
Reactive Context -> MDC only during log emission -> clear immediately
Jangan mengandalkan MDC global sepanjang request jika eksekusi tidak thread-bound.
Internal verification checklist
[ ] Apakah service menggunakan async JAX-RS response?
[ ] Apakah streaming response digunakan?
[ ] Apakah reactive library digunakan?
[ ] Apakah MDC tetap benar setelah thread switch?
[ ] Apakah context bridge tersedia?
[ ] Apakah trace context lebih reliable daripada MDC manual?
Messaging Context: Kafka and RabbitMQ
Request context tidak berhenti di HTTP. Jika service publish message, correlation harus masuk message header.
Publish
Saat publish Kafka/RabbitMQ:
Read MDC/context
Add correlation_id to message headers
Add traceparent if tracing enabled
Add causation/event id if convention exists
Log publish outcome
Pseudo-code:
headers.add("correlation_id", MDC.get("correlation_id"));
headers.add("traceparent", currentTraceParent());
headers.add("event_id", eventId);
Consume
Saat consume:
Read correlation_id from message header
Create new request/message processing context
Put correlation_id/event_id/message_id/topic/queue into MDC
Process message
Cleanup MDC
Example:
try {
MDC.put("correlation_id", headerCorrelationId(message));
MDC.put("event_id", message.eventId());
MDC.put("message_source", topic);
log.info("message processing started event_id={} event_type={}",
message.eventId(),
message.eventType());
process(message);
} finally {
MDC.clear();
}
Redelivery and DLQ
Add fields:
attempt
redelivery_count
dlq_topic/dlq_queue
consumer_group
partition
offset
queue
routing_key
Internal verification checklist
[ ] Apakah Kafka/RabbitMQ producer menulis correlation_id header?
[ ] Apakah traceparent propagated di headers?
[ ] Apakah consumer membuat MDC dari message headers?
[ ] Apakah consumer cleanup MDC setelah setiap message?
[ ] Apakah batch consumer tidak mencampur context antar message?
[ ] Apakah DLQ logs membawa original correlation_id/event_id?
[ ] Apakah message header convention diverifikasi internal?
Background Job Context
Background job tidak punya incoming HTTP request. Jadi context harus dibuat.
Field yang berguna:
correlation_id/job_run_id
job_name
job_type
scheduler_name
batch_id
reconciliation_id
trigger_type
triggered_by
attempt
Contoh:
public void runReconciliationJob() {
String jobRunId = UUID.randomUUID().toString();
try {
MDC.put("correlation_id", "job-" + jobRunId);
MDC.put("job_name", "order-reconciliation");
MDC.put("job_run_id", jobRunId);
log.info("background job started");
reconciliationService.run(jobRunId);
log.info("background job completed");
} catch (Exception ex) {
log.error("background job failed", ex);
throw ex;
} finally {
MDC.clear();
}
}
Kubernetes CronJob
Jika job berjalan sebagai Kubernetes CronJob, tambahkan:
pod_name
namespace
cronjob_name
job_name
image_version
Biasanya sebagian bisa berasal dari environment variable/downward API/resource attributes.
Internal verification checklist
[ ] Apakah setiap job run punya job_run_id?
[ ] Apakah job logs bisa dikelompokkan per execution?
[ ] Apakah batch/reconciliation ID tersedia?
[ ] Apakah job failure log membawa context cukup?
[ ] Apakah Kubernetes CronJob metadata masuk logs/resource attributes?
[ ] Apakah cleanup dilakukan setelah job selesai/gagal?
MDC dan OpenTelemetry Baggage
OpenTelemetry memiliki konsep baggage, yaitu key-value context yang bisa dipropagasikan lintas service.
Namun baggage bukan tempat bebas untuk semua data.
Risiko baggage:
- dikirim lintas service boundary
- bisa membesar di header
- bisa bocor ke service yang tidak perlu tahu
- bisa menjadi security/privacy issue
- bisa menambah network overhead
Jangan otomatis memasukkan semua MDC ke baggage.
Prinsip:
MDC is for local logging context.
Baggage is for explicit cross-service context propagation.
Field seperti tenant_id mungkin perlu dipropagasikan dalam sistem tertentu, tetapi harus mengikuti policy internal. Field seperti actor_email, token, atau order payload tidak boleh.
Internal verification checklist
[ ] Apakah baggage digunakan?
[ ] Field apa yang boleh masuk baggage?
[ ] Apakah tenant/actor propagation punya policy?
[ ] Apakah baggage size dibatasi?
[ ] Apakah baggage tidak menjadi channel data sensitif?
[ ] Apakah MDC dan baggage tidak dicampur otomatis tanpa review?
Context Spoofing
Incoming context dari client tidak selalu bisa dipercaya.
Jika public client bisa mengirim X-Correlation-ID, itu mungkin boleh untuk troubleshooting, tetapi harus divalidasi. Namun field seperti tenant_id, actor_id, atau role tidak boleh dipercaya dari header umum tanpa authentication/authorization.
Trusted vs untrusted headers
Allowed from external client:
- correlation/request ID, after validation
Must come from trusted auth/session/service context:
- actor_id
- tenant_id
- role
- permissions
- impersonation context
Spoofing example
Buruk:
MDC.put("tenant_id", request.getHeader("X-Tenant-ID"));
Lebih aman:
TenantContext tenant = authenticatedContext.resolveTenant();
MDC.put("tenant_id", tenant.id());
Internal verification checklist
[ ] Apakah incoming correlation_id divalidasi?
[ ] Apakah tenant_id tidak dipercaya dari untrusted header?
[ ] Apakah actor_id berasal dari authenticated principal?
[ ] Apakah internal headers dibersihkan/ditimpa di edge/gateway?
[ ] Apakah log injection via newline/control chars dicegah?
[ ] Apakah spoofed traceparent/correlation behavior disepakati?
Log Injection Risk
Karena context value bisa muncul di log, nilai tersebut harus aman.
Risiko:
correlation_id = "abc\nERROR fake log"
Jika logging backend tidak escape dengan benar, attacker bisa membuat log palsu atau merusak parsing.
Mitigation:
- validate allowed characters
- limit length
- reject/control chars
- JSON encoder yang benar
- sanitize inbound context
Contoh validator:
private boolean isValidContextValue(String value) {
if (value == null || value.length() > 128) {
return false;
}
return value.matches("[A-Za-z0-9._:-]+"); // example only; verify against internal standard
}
Perhatikan contoh regex di atas hanya ilustrasi. Jangan copy tanpa test. Lebih baik gunakan allowlist yang sudah diuji.
Context Field Size
MDC harus kecil.
Jangan masukkan:
- request body
- response body
- large object JSON
- list item IDs panjang
- full SQL
- stack trace
- token
- cookie
- arbitrary metadata map
MDC idealnya berisi string pendek:
correlation_id
trace_id
span_id
request_id
tenant_id
actor_id
operation
route
quote_id
order_id
job_run_id
event_id
Jika field besar dibutuhkan, log sebagai explicit structured field pada event tertentu dengan masking/sampling, bukan global MDC.
Context di Logback/Log4j2 JSON Output
MDC harus dikonfigurasi agar benar-benar muncul di output.
MDC put saja tidak cukup.
Internal check:
Application code sets MDC
Logging framework includes MDC in layout/encoder
Log collector preserves JSON fields
Backend indexes fields as expected
Dashboard/query uses those fields
Jika salah satu putus, context tidak berguna.
Common issue
MDC ada di local console pattern, tetapi tidak ada di JSON production output.
MDC fields nested sebagai map tetapi backend tidak index nested field.
MDC field name berbeda antara services.
trace_id muncul sebagai traceId di satu service dan trace_id di service lain.
Internal verification checklist
[ ] Apakah MDC fields muncul di JSON production log?
[ ] Apakah field names konsisten snake_case/camelCase sesuai standard?
[ ] Apakah log collector mempertahankan fields?
[ ] Apakah backend mengindex correlation_id/trace_id?
[ ] Apakah query contoh berhasil menemukan semua log satu request?
[ ] Apakah field name sama antar services?
Local Development and Testability
Context propagation harus bisa dites.
Unit test untuk MDC cleanup
Pseudo-test:
@Test
void filterClearsMdcAfterRequest() {
filter.doFilter(request, response, chain);
assertThat(MDC.getCopyOfContextMap()).isNullOrEmpty();
}
Integration test untuk correlation ID
Send HTTP request with X-Correlation-ID=corr-test-1
Call endpoint that logs service-layer event
Assert captured logs contain correlation_id=corr-test-1
Assert response propagates correlation_id if expected
Async test
Set correlation_id
Submit async task
Assert async task log contains same correlation_id
Assert next task does not inherit stale context
Messaging test
Publish event with correlation_id header
Consumer processes event
Assert consumer log contains same correlation_id and event_id
Internal verification checklist
[ ] Apakah ada test untuk request MDC setup?
[ ] Apakah ada test untuk MDC cleanup?
[ ] Apakah ada test untuk async propagation?
[ ] Apakah ada test untuk messaging header propagation?
[ ] Apakah ada test bahwa sensitive headers tidak masuk MDC?
[ ] Apakah captured structured logs bisa diassert?
Failure Modes
Failure mode 1: Missing correlation ID
Gejala:
Log ada, tetapi tidak bisa dikelompokkan per request/incident.
Penyebab:
- filter tidak jalan
- log terjadi sebelum MDC setup
- async context loss
- messaging consumer tidak membaca header
- log encoder tidak include MDC
Deteksi:
query logs where correlation_id missing by service/endpoint/version
Failure mode 2: Context leak antar request
Gejala:
Request tenant A muncul dengan tenant_id B.
Correlation ID tidak cocok dengan access log.
Penyebab:
- MDC.clear missing
- thread pool reused
- temporary field tidak direstore
Deteksi:
concurrent tests
impossible tenant/request combinations
logs after request completed still carry old context
Failure mode 3: Trace ID missing di logs
Penyebab:
- OTel log bridge tidak aktif
- span belum dibuat saat log
- manual instrumentation salah
- async context loss
Deteksi:
logs with correlation_id but trace_id missing
trace backend has trace but logs cannot link
Failure mode 4: Context spoofing
Penyebab:
- untrusted header diterima sebagai tenant/actor
- no validation
- gateway tidak strip internal headers
Deteksi:
unexpected tenant_id from external traffic
invalid chars/long values in correlation_id
security review findings
Failure mode 5: Context too large
Penyebab:
- request body/payload dimasukkan ke MDC
- arbitrary metadata copied to MDC
- baggage abused
Dampak:
- log volume besar
- memory overhead
- privacy leak
- header bloat jika baggage
Debugging Missing Context
Saat menemukan log tanpa correlation ID, lakukan langkah ini:
1. Tentukan log berasal dari request, async task, message consumer, atau job.
2. Cek apakah boundary tersebut punya context setup.
3. Cek apakah log terjadi sebelum setup atau setelah cleanup.
4. Cek apakah execution pindah thread.
5. Cek apakah MDC di-set tetapi encoder tidak mengeluarkan field.
6. Cek apakah collector/backend menghapus atau rename field.
7. Cek apakah field name berbeda dari query.
8. Cek recent deployment/config change.
Untuk Java/JAX-RS:
- cek Servlet/JAX-RS filter ordering
- cek ExceptionMapper path
- cek async response path
- cek dependency injection lifecycle
- cek logs saat startup vs request runtime
Untuk messaging:
- cek producer headers
- cek broker/header preservation
- cek consumer extraction
- cek batch processing
- cek retry/DLQ propagation
Correctness Concern
Context yang salah lebih buruk daripada context yang hilang.
Missing correlation ID membuat debugging sulit. Wrong correlation ID membuat debugging menyesatkan.
Correctness invariant:
A log event must never claim to belong to a request/tenant/actor/business entity that it does not belong to.
Implication:
- cleanup wajib
- restore temporary context wajib
- untrusted context tidak boleh langsung dipercaya
- async propagation harus capture context yang benar
- batch processing harus scope per item/message jika context berbeda
Batch processing caveat
Jika batch berisi banyak order, jangan set satu order_id global untuk seluruh batch kecuali benar.
Lebih baik:
batch_id at job level
order_id only when processing individual order scope
Performance Concern
MDC overhead biasanya kecil jika field sedikit. Tetapi bisa menjadi masalah jika:
- terlalu banyak field
- field value besar
- context map sering dicopy di hot path
- executor propagation melakukan copy besar untuk setiap task
- logs sangat high frequency
- virtual thread jumlah besar
Guideline:
MDC field should be small, stable, and few.
Hindari MDC sebagai map semua metadata.
Internal verification checklist
[ ] Berapa jumlah field MDC default?
[ ] Apakah value MDC pendek?
[ ] Apakah context map dicopy di high-frequency task?
[ ] Apakah executor propagation overhead pernah diuji?
[ ] Apakah logging allocation terlihat di profiler?
Security and Privacy Concern
MDC membuat field muncul di banyak log secara otomatis. Ini meningkatkan risiko jika field sensitif masuk MDC.
Jangan masukkan ke MDC:
Authorization header
Cookie
session token
password
API key
raw request body
raw SQL parameter
credit/payment data
email/name jika policy melarang
IP address jika policy butuh masking dan belum dimasking
Karena MDC field menyebar ke semua log dalam scope, satu kesalahan bisa memperbanyak leakage.
Internal verification checklist
[ ] Apakah daftar allowed MDC fields terdokumentasi?
[ ] Apakah sensitive headers tidak masuk MDC?
[ ] Apakah tenant/actor policy disetujui security/privacy?
[ ] Apakah field high sensitivity di-hash/mask?
[ ] Apakah baggage tidak membawa PII?
[ ] Apakah log access control sesuai sensitivitas context?
Cost Concern
Context fields meningkatkan ukuran setiap log.
Misalnya menambah 10 field MDC ke semua log bisa meningkatkan volume signifikan, terutama di service high-throughput.
Cost trade-off:
correlation_id -> almost always worth it
trace_id/span_id -> worth it for log-trace correlation
tenant_id -> useful but policy/cardinality sensitive
actor_id -> useful but privacy sensitive
order_id/quote_id -> useful on business events, not necessarily every low-level log
large custom metadata -> usually not worth it
Guideline:
Global MDC: request/trace/correlation/tenant/actor basics.
Event-specific fields: order_id, quote_id, dependency, error_code, attempt, latency.
PR Review Checklist
Gunakan checklist ini saat mereview perubahan MDC/context propagation.
[ ] Apakah context field yang ditambahkan benar-benar dibutuhkan?
[ ] Apakah field aman dari sisi privacy/security?
[ ] Apakah field value divalidasi dan dibatasi panjangnya?
[ ] Apakah field cocok sebagai global MDC atau event-specific field?
[ ] Apakah setup terjadi di boundary yang tepat?
[ ] Apakah cleanup dilakukan di finally?
[ ] Apakah temporary field di-restore setelah scope selesai?
[ ] Apakah async/thread pool propagation ditangani?
[ ] Apakah messaging producer menulis context ke header?
[ ] Apakah messaging consumer membaca context dari header?
[ ] Apakah background job membuat context baru?
[ ] Apakah trace_id/span_id konsisten dengan OpenTelemetry?
[ ] Apakah log encoder benar-benar mengeluarkan MDC field?
[ ] Apakah tests mencakup cleanup dan propagation?
[ ] Apakah field tidak akan menyebabkan log cost tidak perlu?
Internal Verification Checklist
Checklist ini perlu dijalankan di codebase/team/platform internal.
[ ] Apa header correlation ID standar internal?
[ ] Apakah request ID dan correlation ID dibedakan?
[ ] Apakah traceparent digunakan?
[ ] Apakah logs memiliki trace_id/span_id?
[ ] Apakah MDC setup dilakukan di Servlet filter, JAX-RS filter, atau framework lain?
[ ] Apakah cleanup dijamin di finally?
[ ] Apakah ExceptionMapper path mempertahankan context?
[ ] Apakah executor/CompletableFuture/context async memakai propagation wrapper?
[ ] Apakah Kafka/RabbitMQ headers membawa correlation_id/traceparent?
[ ] Apakah consumer cleanup context per message?
[ ] Apakah background jobs punya job_run_id/correlation_id?
[ ] Apakah tenant_id/actor_id allowed di logs?
[ ] Apakah email/name/session/token dilarang di MDC?
[ ] Apakah route template tersedia di logs?
[ ] Apakah log collector/backend mengindex correlation_id dan trace_id?
[ ] Apakah ada test untuk context leak antar request?
[ ] Apakah incident notes pernah menyebut missing correlation ID?
[ ] Apakah dashboard/log query menggunakan field context yang konsisten?
Kesimpulan
Log context adalah perbedaan antara log sebagai noise dan log sebagai evidence.
MDC membantu membuat context otomatis muncul di log, tetapi MDC juga membawa risiko:
context loss
context leak
wrong tenant/actor attribution
async propagation failure
privacy leakage
cost increase
conflict with tracing instrumentation
Untuk senior backend engineer, invariant terpenting adalah:
Every important log must be attributable to the correct request, trace, tenant, actor, business entity, job, or message flow.
Dan invariant kedua:
No context may leak across unrelated execution scopes.
Di part berikutnya, kita akan memperdalam Correlation ID, Causation ID, and Request ID: kapan memakai masing-masing ID, bagaimana membedakan technical trace dari business causal chain, dan bagaimana menyambungkan HTTP, Kafka/RabbitMQ, background job, workflow, idempotency, dan quote/order lifecycle.
You just completed lesson 08 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.