Series MapLesson 08 / 62
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Start HereOrdered learning track

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.

13 min read2402 words
PrevNext
Lesson 0862 lesson track01–12 Start Here
#mdc#threadlocal#correlation-id#trace-id+2 more

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.

FieldTujuanCatatan
correlation_idMenghubungkan flow lintas service/eventBiasanya dibawa dari ingress atau dibuat jika tidak ada
request_idIdentitas request individualBisa berbeda dari correlation ID
trace_idDistributed trace IDDari OpenTelemetry/W3C trace context
span_idSpan aktif saat log dibuatBerguna untuk log-trace correlation
tenant_idScope tenant/customerHarus sesuai privacy/internal policy
actor_idUser/service actorJangan log PII langsung jika tidak allowed
actor_typeuser/system/service/jobMembantu audit/debugging
session_idSession correlationBiasanya sensitif; sering tidak boleh raw
business_keyQuote/order/process keyGunakan nama spesifik jika bisa
quote_idQuote entityBusiness debugging
order_idOrder entityBusiness debugging
process_instance_idWorkflow instanceCamunda/workflow debugging
event_idMessage/event identityMessaging debugging
idempotency_keyDuplicate/retry detectionBisa sensitif; cek policy
operationNama operasi teknis/domainQueryability
serviceService nameBiasanya resource attribute/log field global
environmentdev/stage/prodGlobal field
versionbuild/version/imageRelease 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:

  1. capture context dari caller
  2. set context di worker sebelum delegate run
  3. 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.

Lesson Recap

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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.