Coroutine MDC propagation
Latest stable Based on Bluetape4k release 1.11.0
Plain MDC cannot follow a coroutine to another thread because its state is thread-local. withCoroutineLoggingContext combines scoped MDC with MDCContext from kotlinx-coroutines-slf4j.
A complete boundary
Section titled “A complete boundary”suspend fun handle(requestId: String) = withCoroutineLoggingContext( "requestId" to requestId, "component" to "checkout", ) { coroutineScope { val stock = async(Dispatchers.IO) { inventory.load() } val price = async(Dispatchers.Default) { pricing.load() } log.info { "Composing checkout" } stock.await() to price.await() } }The helper runs the block inside withContext(MDCContext()). Child coroutines inherit their parent context, so the snapshot crosses dispatcher switches.
The snapshot rule
Section titled “The snapshot rule”MDCContext captures MDC when the context is created. A plain MDC.put inside the block is not automatically recaptured for later suspensions. Use a new helper scope or MDCContext boundary when a new value must propagate.
Restoration policy
Section titled “Restoration policy”restorePrevious=true uses the same nested restoration contract as the synchronous helper. With false, the helper also removes the applied non-null keys in an outer finally so caller-thread residue is cleared.
Decision table
Section titled “Decision table”| Code shape | Choice |
|---|---|
| Synchronous block without suspension | withLoggingContext |
| suspend, async, or dispatcher switch | withCoroutineLoggingContext |
| Framework already owns trace MDC | avoid duplicate ownership and verify its lifecycle |
Source and tests
Section titled “Source and tests”Before asynchronous emission, inspect the additional lifecycle in Async channel.