-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
e153863
commit f272f8f
Showing
9 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Module kotlinx-coroutines-log4j | ||
|
||
Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html). | ||
|
||
## Example | ||
|
||
Add [Log4JThreadContext] to the coroutine context so that the Log4J `ThreadContext` state is captured and passed into the coroutine. | ||
|
||
```kotlin | ||
ThreadContext.put("kotlin", "rocks") | ||
|
||
launch(Log4JThreadContext()) { | ||
logger.info(...) // the ThreadContext will contain the mapping here | ||
} | ||
``` | ||
|
||
# Package kotlinx.coroutines.log4jj | ||
|
||
Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html). | ||
|
||
<!--- MODULE kotlinx-coroutines-log4j --> | ||
<!--- INDEX kotlinx.coroutines.log4j --> | ||
[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html | ||
<!--- END --> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
dependencies { | ||
implementation 'org.apache.logging.log4j:log4j-api:2.13.0' | ||
testImplementation 'org.apache.logging.log4j:log4j-core:2.13.0' | ||
} | ||
|
||
tasks.withType(dokka.getClass()) { | ||
externalDocumentationLink { | ||
packageListUrl = projectDir.toPath().resolve("package.list").toUri().toURL() | ||
url = new URL("https://logging.apache.org/log4j/2.x/log4j-api/apidocs") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
org.apache.logging.log4j |
102 changes: 102 additions & 0 deletions
102
integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines.log4j | ||
|
||
import kotlinx.coroutines.* | ||
import org.apache.logging.log4j.ThreadContext | ||
import kotlin.coroutines.AbstractCoroutineContextElement | ||
import kotlin.coroutines.CoroutineContext | ||
|
||
/** | ||
* Context element for [CoroutineContext], enabling the use of Log4J 2's [ThreadContext] with coroutines. | ||
* | ||
* # Example | ||
* | ||
* The following example demonstrates usage of this class. All `assert`s pass. Though this example only uses the mapped | ||
* diagnostic context, the nested diagnostic context is also supported. | ||
* | ||
* ```kotlin | ||
* 1. runBlocking { | ||
* 2. ThreadContext.put("kotlin", "rocks") // Put a value into the ThreadContext | ||
* 3. | ||
* 4. withContext(Log4JThreadContext()) { | ||
* 5. assert(ThreadContext.get("kotlin") == "rocks") | ||
* 6. logger.info(...) // The ThreadContext contains the mapping here | ||
* 7. | ||
* 8. ThreadContext.put("kotlin", "is great") | ||
* 9. launch(Dispatchers.IO) { | ||
* 10. assert(ThreadContext.get("kotlin") == "rocks") | ||
* 11. } | ||
* 12. } | ||
* 13. } | ||
* ``` | ||
* It may be surprising that the [ThreadContext] contains the pair (`"kotlin"`, `"rocks"`) at line 10. However, recall | ||
* that on line 4, the [CoroutineContext] was updated with the [Log4JThreadContext] element. When, on line 9, a new | ||
* [CoroutineContext] is forked from [CoroutineContext] created on line 4, the same [Log4JThreadContext] element from | ||
* line 4 is applied. The [ThreadContext] modification made on line 8 is not part of the [state]. | ||
* | ||
* ## Combine with other | ||
* You may wish to combine this [ThreadContextElement] with other [CoroutineContext]s. | ||
* | ||
* ```kotlin | ||
* launch(Dispatchers.IO + Log4JThreadContext()) { ... } | ||
* ``` | ||
* | ||
* # CloseableThreadContext | ||
* [org.apache.logging.log4j.CloseableThreadContext] is useful for automatically cleaning up the [ThreadContext] after a | ||
* block of code. The structured concurrency provided by coroutines offers the same functionality. | ||
* | ||
* In the following example, the modifications to the [ThreadContext] are cleaned up when the coroutine exits. | ||
* | ||
* ```kotlin | ||
* ThreadContext.put("kotlin", "rocks") | ||
* | ||
* withContext(Log4JThreadContext()) { | ||
* ThreadContext.put("kotlin", "is awesome") | ||
* } | ||
* assert(ThreadContext.get("kotlin") == "rocks") | ||
* ``` | ||
* | ||
* @param state the values of [ThreadContext]. The default value is a copy of the current state. | ||
*/ | ||
public class Log4JThreadContext( | ||
public val state: Log4JThreadContextState = Log4JThreadContextState() | ||
) : ThreadContextElement<Log4JThreadContextState>, AbstractCoroutineContextElement(Key) { | ||
/** | ||
* Key of [Log4JThreadContext] in [CoroutineContext]. | ||
*/ | ||
companion object Key : CoroutineContext.Key<Log4JThreadContext> | ||
|
||
/** @suppress */ | ||
override fun updateThreadContext(context: CoroutineContext): Log4JThreadContextState { | ||
val oldState = Log4JThreadContextState() | ||
setCurrent(state) | ||
return oldState | ||
} | ||
|
||
/** @suppress */ | ||
override fun restoreThreadContext(context: CoroutineContext, oldState: Log4JThreadContextState) { | ||
setCurrent(oldState) | ||
} | ||
|
||
private fun setCurrent(state: Log4JThreadContextState) { | ||
ThreadContext.clearMap() | ||
ThreadContext.putAll(state.mdc) | ||
|
||
// setStack clears the existing stack | ||
ThreadContext.setStack(state.ndc) | ||
} | ||
} | ||
|
||
/** | ||
* Holder for the state of a [ThreadContext]. | ||
* | ||
* @param mdc a copy of the mapped diagnostic context. | ||
* @param ndc a copy of the nested diagnostic context. | ||
*/ | ||
public class Log4JThreadContextState( | ||
val mdc: Map<String, String> = ThreadContext.getImmutableContext(), | ||
val ndc: ThreadContext.ContextStack = ThreadContext.getImmutableStack() | ||
) |
16 changes: 16 additions & 0 deletions
16
integration/kotlinx-coroutines-log4j/test-resources/log4j2-test.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<Configuration debug="false"> | ||
<Appenders> | ||
<Console name="Console" target="SYSTEM_OUT"> | ||
<PatternLayout> | ||
<pattern>%X{first} %X{last} - %m%n</pattern> | ||
</PatternLayout> | ||
</Console> | ||
</Appenders> | ||
|
||
<Loggers> | ||
<Root level="trace"> | ||
<AppenderRef ref="Console"/> | ||
</Root> | ||
</Loggers> | ||
</Configuration> |
129 changes: 129 additions & 0 deletions
129
integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* | ||
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines.log4j | ||
|
||
import kotlinx.coroutines.* | ||
import org.apache.logging.log4j.CloseableThreadContext | ||
import org.apache.logging.log4j.ThreadContext | ||
import org.junit.* | ||
import org.junit.Test | ||
import kotlin.coroutines.* | ||
import kotlin.test.* | ||
|
||
class Log4JThreadContextTest : TestBase() { | ||
@Before | ||
fun setUp() { | ||
ThreadContext.clearAll() | ||
} | ||
|
||
@After | ||
fun tearDown() { | ||
ThreadContext.clearAll() | ||
} | ||
|
||
@Test | ||
fun testContextIsNotPassedByDefaultBetweenCoroutines() = runTest { | ||
expect(1) | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
// Standalone launch | ||
GlobalScope.launch { | ||
assertEquals(null, ThreadContext.get("myKey")) | ||
assertEquals("", ThreadContext.peek()) | ||
expect(2) | ||
}.join() | ||
finish(3) | ||
} | ||
|
||
@Test | ||
fun testContextCanBePassedBetweenCoroutines() = runTest { | ||
expect(1) | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
// Scoped launch with Log4JThreadContext element | ||
launch(Log4JThreadContext()) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
expect(2) | ||
}.join() | ||
|
||
finish(3) | ||
} | ||
|
||
@Test | ||
fun testContextInheritance() = runTest { | ||
expect(1) | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
withContext(Log4JThreadContext()) { | ||
ThreadContext.put("myKey", "myValue2") | ||
ThreadContext.push("stack2") | ||
// Scoped launch with inherited Log4JThreadContext element | ||
launch(Dispatchers.Default) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
expect(2) | ||
}.join() | ||
|
||
finish(3) | ||
} | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
} | ||
|
||
@Test | ||
fun testContextPassedWhileOnMainThread() { | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
// No ThreadContext element | ||
runBlocking { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
} | ||
} | ||
|
||
@Test | ||
fun testContextCanBePassedWhileOnMainThread() { | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
runBlocking(Log4JThreadContext()) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
} | ||
} | ||
|
||
@Test | ||
fun testContextNeededWithOtherContext() { | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
runBlocking(Log4JThreadContext()) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
} | ||
} | ||
|
||
@Test | ||
fun testContextMayBeEmpty() { | ||
runBlocking(Log4JThreadContext()) { | ||
assertEquals(null, ThreadContext.get("myKey")) | ||
assertEquals("", ThreadContext.peek()) | ||
} | ||
} | ||
|
||
@Test | ||
fun testContextWithContext() = runTest { | ||
ThreadContext.put("myKey", "myValue") | ||
ThreadContext.push("stack1") | ||
val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!! | ||
withContext(Dispatchers.Default + Log4JThreadContext()) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
withContext(mainDispatcher) { | ||
assertEquals("myValue", ThreadContext.get("myKey")) | ||
assertEquals("stack1", ThreadContext.peek()) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters