Skip to content

Commit 8f88619

Browse files
maniax89Andrew Turgeon
authored andcommitted
feat(jvm) add withCoroutineLoggingContext
1 parent be64f7a commit 8f88619

2 files changed

Lines changed: 157 additions & 20 deletions

File tree

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

Lines changed: 49 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,21 @@ public suspend inline fun <T> withLoggingContextAsync(
6495
withLoggingContext(map, restorePrevious = restorePrevious) {
6596
withContext(MDCContext()) { body() }
6697
}
98+
99+
/**
100+
* Use a map inheriting coroutine context but not current MDC Context. Example:
101+
* ```
102+
* withCoroutineLoggingContext(mapOf("userId" to userId)) {
103+
* doSomething()
104+
* }
105+
* ```
106+
*/
107+
public suspend inline fun <T> withCoroutineLoggingContext(
108+
map: Map<String, String?>,
109+
crossinline body: suspend () -> T,
110+
): T {
111+
val parent = currentCoroutineContext()[MDCContext]?.contextMap ?: emptyMap()
112+
val merged = (parent + map.filterValues { it != null }).mapValues { it.value!! }
113+
114+
return withContext(MDCContext(merged)) { body() }
115+
}

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

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package io.github.oshai.kotlinlogging.coroutines
22

3-
import kotlinx.coroutines.CompletableDeferred
3+
import java.util.concurrent.Executors
44
import kotlin.test.*
5+
import kotlinx.coroutines.CompletableDeferred
56
import kotlinx.coroutines.ExperimentalCoroutinesApi
67
import kotlinx.coroutines.asCoroutineDispatcher
78
import kotlinx.coroutines.launch
8-
import kotlinx.coroutines.runBlocking
99
import kotlinx.coroutines.test.runTest
1010
import org.apache.logging.log4j.*
1111
import org.apache.logging.log4j.core.config.*
1212
import org.junit.jupiter.api.BeforeEach
1313
import org.slf4j.*
14-
import java.util.concurrent.Executors
1514

1615
@ExperimentalCoroutinesApi
1716
class KotlinLoggingAsyncMDCTest {
@@ -168,37 +167,126 @@ class KotlinLoggingAsyncMDCTest {
168167
fun `withLoggingContextAsync leaks context across coroutines`() = runTest {
169168
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
170169
// establish order of events with deferreds:
171-
// 1. request A will start, set its context, and then wait for request B (before exiting withLoggingContextAsync)
170+
// 1. request A will start, set its context, and then wait for request B (before exiting
171+
// withLoggingContextAsync)
172172
// 2. request B will start, set its context, and then capture the MDC
173173
val requestAStarted = CompletableDeferred<Unit>()
174174
val requestBDone = CompletableDeferred<Unit>()
175175

176176
val requestAAfterBlock = CompletableDeferred<Map<String, String>?>()
177177
val requestBAfterBlock = CompletableDeferred<Map<String, String>?>()
178178

179-
val jobA = launch(dispatcher) {
180-
withLoggingContextAsync("foo" to "original") {
181-
requestAStarted.complete(Unit)
182-
requestBDone.await() // Suspend while B runs
179+
val jobA =
180+
launch(dispatcher) {
181+
withLoggingContextAsync("foo" to "original") {
182+
requestAStarted.complete(Unit)
183+
requestBDone.await() // Suspend while B runs
184+
}
185+
// will be empty
186+
requestAAfterBlock.complete(MDC.getCopyOfContextMap())
183187
}
184-
// will be empty
185-
requestAAfterBlock.complete(MDC.getCopyOfContextMap())
186-
}
187188

188-
val jobB = launch(dispatcher) {
189-
requestAStarted.await()
190-
withLoggingContextAsync("foo" to "bar") { }
191-
// Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
192-
requestBAfterBlock.complete(MDC.getCopyOfContextMap())
193-
requestBDone.complete(Unit)
194-
}
189+
val jobB =
190+
launch(dispatcher) {
191+
requestAStarted.await()
192+
withLoggingContextAsync("foo" to "bar") {}
193+
// Capture MDC state immediately after Request B's block, will NOT be empty due to the bug
194+
requestBAfterBlock.complete(MDC.getCopyOfContextMap())
195+
requestBDone.complete(Unit)
196+
}
195197

196198
jobA.join()
197199
jobB.join()
198200

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

0 commit comments

Comments
 (0)