Skip to content

Commit a4cf22f

Browse files
author
Andrew Turgeon
committed
feat(jvm) add withCoroutineLoggingContext
1 parent cb2f3c9 commit a4cf22f

2 files changed

Lines changed: 135 additions & 2 deletions

File tree

src/jvmMain/kotlin/io/github/oshai/kotlinlogging/coroutines/KotlinLoggingAsyncMDC.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ public suspend inline fun <T> withLoggingContextAsync(
2626
withContext(MDCContext()) { body() }
2727
}
2828

29+
/**
30+
* Use a pair inheriting coroutine context but not current MDC Context. Example:
31+
* ```
32+
* withLoggingContextAsync("userId" to userId) {
33+
* doSomething()
34+
* }
35+
* ```
36+
* ```
37+
* withLoggingContextAsync("userId" to userId, restorePrevious = false) {
38+
* doSomething()
39+
* }
40+
* ```
41+
*/
42+
public suspend inline fun <T> withCoroutineLoggingContext(
43+
pair: Pair<String, String?>,
44+
crossinline body: suspend () -> T,
45+
): T = withCoroutineLoggingContext(mapOf(pair), body)
46+
2947
/**
3048
* Use a varying number of pairs in an asynchronous MDC context. Example:
3149
* ```
@@ -43,6 +61,19 @@ public suspend inline fun <T> withLoggingContextAsync(
4361
withContext(MDCContext()) { body() }
4462
}
4563

64+
/**
65+
* Use a varying number of pairs inheriting coroutine context but not current MDC Context. Example:
66+
* ```
67+
* withCoroutineLoggingContext("userId" to userId) {
68+
* doSomething()
69+
* }
70+
* ```
71+
*/
72+
public suspend inline fun <T> withCoroutineLoggingContext(
73+
vararg pair: Pair<String, String?>,
74+
crossinline body: suspend () -> T,
75+
): T = withCoroutineLoggingContext(pair.toMap(), body)
76+
4677
/**
4778
* Use a map in an asynchronous MDC context. Example:
4879
* ```
@@ -64,3 +95,22 @@ public suspend inline fun <T> withLoggingContextAsync(
6495
withLoggingContext(map, restorePrevious = restorePrevious) {
6596
withContext(MDCContext()) { body() }
6697
}
98+
99+
100+
/**
101+
* Use a map inheriting coroutine context but not current MDC Context. Example:
102+
* ```
103+
* withCoroutineLoggingContext(mapOf("userId" to userId)) {
104+
* doSomething()
105+
* }
106+
* ```
107+
*/
108+
public suspend inline fun <T> withCoroutineLoggingContext(
109+
map: Map<String, String?>,
110+
crossinline body: suspend () -> T,
111+
): T {
112+
val parent = currentCoroutineContext()[MDCContext]?.contextMap ?: emptyMap()
113+
val merged = (parent + map.filterValues { it != null }).mapValues { it.value!! }
114+
115+
return withContext(MDCContext(merged)) { body() }
116+
}

src/jvmTest/kotlin/io/github/oshai/kotlinlogging/coroutines/KotlinLoggingAsyncMDCTest.kt

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,91 @@ class KotlinLoggingAsyncMDCTest {
197197
jobB.join()
198198

199199
assertEquals(emptyMap(), requestAAfterBlock.await())
200-
// THIS IS THE BUG: Request B sees "AAA" after its own block exits
200+
// THIS IS THE BUG: Request B sees "original" after its own block exits
201201
// It should be null (no context) but it's leaking Request A's value
202-
assertEquals(mapOf("foo" to "original"), requestBAfterBlock.await()) // This will FAIL
202+
assertEquals(mapOf("foo" to "original"), requestBAfterBlock.await())
203+
}
204+
205+
@Test
206+
fun `withCoroutineLoggingContext does not context across coroutines`() = runTest {
207+
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
208+
// establish order of events with deferreds:
209+
// 1. request A will start, set its context, and then wait for request B (before exiting withLoggingContextAsync)
210+
// 2. request B will start, set its context, and then capture the MDC
211+
val requestAStarted = CompletableDeferred<Unit>()
212+
val requestBDone = CompletableDeferred<Unit>()
213+
214+
val requestAAfterBlock = CompletableDeferred<Map<String, String>?>()
215+
val requestBAfterBlock = CompletableDeferred<Map<String, String>?>()
216+
217+
val jobA = launch(dispatcher) {
218+
withCoroutineLoggingContext("foo" to "original") {
219+
requestAStarted.complete(Unit)
220+
requestBDone.await() // Suspend while B runs
221+
}
222+
// will be empty
223+
requestAAfterBlock.complete(MDC.getCopyOfContextMap())
224+
}
225+
226+
val jobB = launch(dispatcher) {
227+
requestAStarted.await()
228+
withCoroutineLoggingContext("foo" to "bar") { }
229+
// Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
230+
requestBAfterBlock.complete(MDC.getCopyOfContextMap())
231+
requestBDone.complete(Unit)
232+
}
233+
234+
jobA.join()
235+
jobB.join()
236+
237+
assertEquals(emptyMap(), requestAAfterBlock.await())
238+
assertEquals(emptyMap(), requestBAfterBlock.await())
239+
}
240+
241+
@Test
242+
fun `map withCoroutineLoggingContext`() = runTest {
243+
assertNull(MDC.get("a"))
244+
assertNull(MDC.get("c"))
245+
assertNull(MDC.get("e"))
246+
assertNull(MDC.get("f"))
247+
assertNull(MDC.get("k"))
248+
249+
// original MDC outside of coroutine context
250+
MDC.put("e", "g")
251+
MDC.put("k", "l")
252+
253+
withCoroutineLoggingContext(mapOf("a" to "b", "c" to "d", "e" to null, "f" to "h")) {
254+
assertEquals("b", MDC.get("a"))
255+
assertEquals("d", MDC.get("c"))
256+
// does NOT inherit original MDC
257+
assertNull(MDC.get("e"))
258+
assertEquals("h", MDC.get("f"))
259+
// does NOT inherit original MDC
260+
assertNull(MDC.get("k"))
261+
262+
withCoroutineLoggingContext(mapOf("a" to "b", "e" to "i", "f" to "j")) {
263+
assertEquals("b", MDC.get("a"))
264+
// DOES inherit previous coroutine context
265+
assertEquals("d", MDC.get("c"))
266+
assertEquals("i", MDC.get("e"))
267+
assertEquals("j", MDC.get("f"))
268+
assertNull(MDC.get("k"))
269+
}
270+
271+
assertEquals("b", MDC.get("a"))
272+
assertEquals("d", MDC.get("c"))
273+
assertNull(MDC.get("e"))
274+
assertEquals("h", MDC.get("f"))
275+
assertNull(MDC.get("k"))
276+
}
277+
278+
assertNull(MDC.get("a"))
279+
assertNull(MDC.get("c"))
280+
281+
// original MDC is restored
282+
assertEquals("g", MDC.get("e"))
283+
assertNull(MDC.get("f"))
284+
// original MDC is restored
285+
assertEquals("l", MDC.get("k"))
203286
}
204287
}

0 commit comments

Comments
 (0)