Skip to content

Commit f272f8f

Browse files
Add an integration with Log4J 2's ThreadContext
Log4J 2 has a ThreadContext, which works the same way as SLF4J's MDC. Using the ThreadContext directly with coroutines breaks, but the same approach for an integration that exists for SLF4J can be used for Log4J. The tests are copied from the SLF4J project, and are only modified to also include verification of stack state, since ThreadContext contains both a Map and a Stack.
1 parent e153863 commit f272f8f

File tree

9 files changed

+289
-0
lines changed

9 files changed

+289
-0
lines changed

Diff for: README.md

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ suspend fun main() = coroutineScope {
5252
* [integration](integration/README.md) — modules that provide integration with various asynchronous callback- and future-based libraries:
5353
* JDK8 [CompletionStage.await], Guava [ListenableFuture.await], and Google Play Services [Task.await];
5454
* SLF4J MDC integration via [MDCContext].
55+
* Log4J 2 ThreadContext integration via [Log4JThreadContext]
5556

5657
## Documentation
5758

@@ -265,6 +266,9 @@ The `develop` branch is pushed to `master` during release.
265266
<!--- MODULE kotlinx-coroutines-slf4j -->
266267
<!--- INDEX kotlinx.coroutines.slf4j -->
267268
[MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html
269+
<!--- MODULE kotlinx-coroutines-log4j -->
270+
<!--- INDEX kotlinx.coroutines.log4j -->
271+
[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html
268272
<!--- MODULE kotlinx-coroutines-jdk8 -->
269273
<!--- INDEX kotlinx.coroutines.future -->
270274
[CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html

Diff for: integration/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Module name below corresponds to the artifact name in Maven/Gradle.
99
* [kotlinx-coroutines-guava](kotlinx-coroutines-guava/README.md) -- integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
1010
* [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j/README.md) -- integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
1111
* [kotlinx-coroutines-play-services](kotlinx-coroutines-play-services) -- integration with Google Play Services [Tasks API](https://developers.google.com/android/guides/tasks).
12+
* [kotlinx-coroutines-log4j](kotlinx-coroutines-log4j/README.md) -- integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html).
1213

1314
## Contributing
1415

Diff for: integration/kotlinx-coroutines-log4j/README.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Module kotlinx-coroutines-log4j
2+
3+
Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html).
4+
5+
## Example
6+
7+
Add [Log4JThreadContext] to the coroutine context so that the Log4J `ThreadContext` state is captured and passed into the coroutine.
8+
9+
```kotlin
10+
ThreadContext.put("kotlin", "rocks")
11+
12+
launch(Log4JThreadContext()) {
13+
logger.info(...) // the ThreadContext will contain the mapping here
14+
}
15+
```
16+
17+
# Package kotlinx.coroutines.log4jj
18+
19+
Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html).
20+
21+
<!--- MODULE kotlinx-coroutines-log4j -->
22+
<!--- INDEX kotlinx.coroutines.log4j -->
23+
[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html
24+
<!--- END -->

Diff for: integration/kotlinx-coroutines-log4j/build.gradle

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
dependencies {
2+
implementation 'org.apache.logging.log4j:log4j-api:2.13.0'
3+
testImplementation 'org.apache.logging.log4j:log4j-core:2.13.0'
4+
}
5+
6+
tasks.withType(dokka.getClass()) {
7+
externalDocumentationLink {
8+
packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL()
9+
url = new URL("https://logging.apache.org/log4j/2.x/log4j-api/apidocs")
10+
}
11+
}

Diff for: integration/kotlinx-coroutines-log4j/package.list

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.apache.logging.log4j
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.log4j
6+
7+
import kotlinx.coroutines.*
8+
import org.apache.logging.log4j.ThreadContext
9+
import kotlin.coroutines.AbstractCoroutineContextElement
10+
import kotlin.coroutines.CoroutineContext
11+
12+
/**
13+
* Context element for [CoroutineContext], enabling the use of Log4J 2's [ThreadContext] with coroutines.
14+
*
15+
* # Example
16+
*
17+
* The following example demonstrates usage of this class. All `assert`s pass. Though this example only uses the mapped
18+
* diagnostic context, the nested diagnostic context is also supported.
19+
*
20+
* ```kotlin
21+
* 1. runBlocking {
22+
* 2. ThreadContext.put("kotlin", "rocks") // Put a value into the ThreadContext
23+
* 3.
24+
* 4. withContext(Log4JThreadContext()) {
25+
* 5. assert(ThreadContext.get("kotlin") == "rocks")
26+
* 6. logger.info(...) // The ThreadContext contains the mapping here
27+
* 7.
28+
* 8. ThreadContext.put("kotlin", "is great")
29+
* 9. launch(Dispatchers.IO) {
30+
* 10. assert(ThreadContext.get("kotlin") == "rocks")
31+
* 11. }
32+
* 12. }
33+
* 13. }
34+
* ```
35+
* It may be surprising that the [ThreadContext] contains the pair (`"kotlin"`, `"rocks"`) at line 10. However, recall
36+
* that on line 4, the [CoroutineContext] was updated with the [Log4JThreadContext] element. When, on line 9, a new
37+
* [CoroutineContext] is forked from [CoroutineContext] created on line 4, the same [Log4JThreadContext] element from
38+
* line 4 is applied. The [ThreadContext] modification made on line 8 is not part of the [state].
39+
*
40+
* ## Combine with other
41+
* You may wish to combine this [ThreadContextElement] with other [CoroutineContext]s.
42+
*
43+
* ```kotlin
44+
* launch(Dispatchers.IO + Log4JThreadContext()) { ... }
45+
* ```
46+
*
47+
* # CloseableThreadContext
48+
* [org.apache.logging.log4j.CloseableThreadContext] is useful for automatically cleaning up the [ThreadContext] after a
49+
* block of code. The structured concurrency provided by coroutines offers the same functionality.
50+
*
51+
* In the following example, the modifications to the [ThreadContext] are cleaned up when the coroutine exits.
52+
*
53+
* ```kotlin
54+
* ThreadContext.put("kotlin", "rocks")
55+
*
56+
* withContext(Log4JThreadContext()) {
57+
* ThreadContext.put("kotlin", "is awesome")
58+
* }
59+
* assert(ThreadContext.get("kotlin") == "rocks")
60+
* ```
61+
*
62+
* @param state the values of [ThreadContext]. The default value is a copy of the current state.
63+
*/
64+
public class Log4JThreadContext(
65+
public val state: Log4JThreadContextState = Log4JThreadContextState()
66+
) : ThreadContextElement<Log4JThreadContextState>, AbstractCoroutineContextElement(Key) {
67+
/**
68+
* Key of [Log4JThreadContext] in [CoroutineContext].
69+
*/
70+
companion object Key : CoroutineContext.Key<Log4JThreadContext>
71+
72+
/** @suppress */
73+
override fun updateThreadContext(context: CoroutineContext): Log4JThreadContextState {
74+
val oldState = Log4JThreadContextState()
75+
setCurrent(state)
76+
return oldState
77+
}
78+
79+
/** @suppress */
80+
override fun restoreThreadContext(context: CoroutineContext, oldState: Log4JThreadContextState) {
81+
setCurrent(oldState)
82+
}
83+
84+
private fun setCurrent(state: Log4JThreadContextState) {
85+
ThreadContext.clearMap()
86+
ThreadContext.putAll(state.mdc)
87+
88+
// setStack clears the existing stack
89+
ThreadContext.setStack(state.ndc)
90+
}
91+
}
92+
93+
/**
94+
* Holder for the state of a [ThreadContext].
95+
*
96+
* @param mdc a copy of the mapped diagnostic context.
97+
* @param ndc a copy of the nested diagnostic context.
98+
*/
99+
public class Log4JThreadContextState(
100+
val mdc: Map<String, String> = ThreadContext.getImmutableContext(),
101+
val ndc: ThreadContext.ContextStack = ThreadContext.getImmutableStack()
102+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration debug="false">
3+
<Appenders>
4+
<Console name="Console" target="SYSTEM_OUT">
5+
<PatternLayout>
6+
<pattern>%X{first} %X{last} - %m%n</pattern>
7+
</PatternLayout>
8+
</Console>
9+
</Appenders>
10+
11+
<Loggers>
12+
<Root level="trace">
13+
<AppenderRef ref="Console"/>
14+
</Root>
15+
</Loggers>
16+
</Configuration>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.log4j
6+
7+
import kotlinx.coroutines.*
8+
import org.apache.logging.log4j.CloseableThreadContext
9+
import org.apache.logging.log4j.ThreadContext
10+
import org.junit.*
11+
import org.junit.Test
12+
import kotlin.coroutines.*
13+
import kotlin.test.*
14+
15+
class Log4JThreadContextTest : TestBase() {
16+
@Before
17+
fun setUp() {
18+
ThreadContext.clearAll()
19+
}
20+
21+
@After
22+
fun tearDown() {
23+
ThreadContext.clearAll()
24+
}
25+
26+
@Test
27+
fun testContextIsNotPassedByDefaultBetweenCoroutines() = runTest {
28+
expect(1)
29+
ThreadContext.put("myKey", "myValue")
30+
ThreadContext.push("stack1")
31+
// Standalone launch
32+
GlobalScope.launch {
33+
assertEquals(null, ThreadContext.get("myKey"))
34+
assertEquals("", ThreadContext.peek())
35+
expect(2)
36+
}.join()
37+
finish(3)
38+
}
39+
40+
@Test
41+
fun testContextCanBePassedBetweenCoroutines() = runTest {
42+
expect(1)
43+
ThreadContext.put("myKey", "myValue")
44+
ThreadContext.push("stack1")
45+
// Scoped launch with Log4JThreadContext element
46+
launch(Log4JThreadContext()) {
47+
assertEquals("myValue", ThreadContext.get("myKey"))
48+
assertEquals("stack1", ThreadContext.peek())
49+
expect(2)
50+
}.join()
51+
52+
finish(3)
53+
}
54+
55+
@Test
56+
fun testContextInheritance() = runTest {
57+
expect(1)
58+
ThreadContext.put("myKey", "myValue")
59+
ThreadContext.push("stack1")
60+
withContext(Log4JThreadContext()) {
61+
ThreadContext.put("myKey", "myValue2")
62+
ThreadContext.push("stack2")
63+
// Scoped launch with inherited Log4JThreadContext element
64+
launch(Dispatchers.Default) {
65+
assertEquals("myValue", ThreadContext.get("myKey"))
66+
assertEquals("stack1", ThreadContext.peek())
67+
expect(2)
68+
}.join()
69+
70+
finish(3)
71+
}
72+
assertEquals("myValue", ThreadContext.get("myKey"))
73+
assertEquals("stack1", ThreadContext.peek())
74+
}
75+
76+
@Test
77+
fun testContextPassedWhileOnMainThread() {
78+
ThreadContext.put("myKey", "myValue")
79+
ThreadContext.push("stack1")
80+
// No ThreadContext element
81+
runBlocking {
82+
assertEquals("myValue", ThreadContext.get("myKey"))
83+
assertEquals("stack1", ThreadContext.peek())
84+
}
85+
}
86+
87+
@Test
88+
fun testContextCanBePassedWhileOnMainThread() {
89+
ThreadContext.put("myKey", "myValue")
90+
ThreadContext.push("stack1")
91+
runBlocking(Log4JThreadContext()) {
92+
assertEquals("myValue", ThreadContext.get("myKey"))
93+
assertEquals("stack1", ThreadContext.peek())
94+
}
95+
}
96+
97+
@Test
98+
fun testContextNeededWithOtherContext() {
99+
ThreadContext.put("myKey", "myValue")
100+
ThreadContext.push("stack1")
101+
runBlocking(Log4JThreadContext()) {
102+
assertEquals("myValue", ThreadContext.get("myKey"))
103+
assertEquals("stack1", ThreadContext.peek())
104+
}
105+
}
106+
107+
@Test
108+
fun testContextMayBeEmpty() {
109+
runBlocking(Log4JThreadContext()) {
110+
assertEquals(null, ThreadContext.get("myKey"))
111+
assertEquals("", ThreadContext.peek())
112+
}
113+
}
114+
115+
@Test
116+
fun testContextWithContext() = runTest {
117+
ThreadContext.put("myKey", "myValue")
118+
ThreadContext.push("stack1")
119+
val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!!
120+
withContext(Dispatchers.Default + Log4JThreadContext()) {
121+
assertEquals("myValue", ThreadContext.get("myKey"))
122+
assertEquals("stack1", ThreadContext.peek())
123+
withContext(mainDispatcher) {
124+
assertEquals("myValue", ThreadContext.get("myKey"))
125+
assertEquals("stack1", ThreadContext.peek())
126+
}
127+
}
128+
}
129+
}

Diff for: site/docs/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Library support for Kotlin coroutines. This reference is a companion to
2525
| [kotlinx-coroutines-guava](kotlinx-coroutines-guava) | Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained) |
2626
| [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j) | Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html) |
2727
| [kotlinx-coroutines-play-services](kotlinx-coroutines-play-services) | Integration with Google Play Services [Tasks API](https://developers.google.com/android/guides/tasks) |
28+
| [kotlinx-coroutines-log4j](kotlinx-coroutines-log4j) | Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html) |
2829

2930
## Examples
3031

0 commit comments

Comments
 (0)