Skip to content

Commit

Permalink
Add an integration with Log4J 2's ThreadContext
Browse files Browse the repository at this point in the history
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
MariusVolkhart committed Feb 10, 2020
1 parent e153863 commit f272f8f
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ suspend fun main() = coroutineScope {
* [integration](integration/README.md) — modules that provide integration with various asynchronous callback- and future-based libraries:
* JDK8 [CompletionStage.await], Guava [ListenableFuture.await], and Google Play Services [Task.await];
* SLF4J MDC integration via [MDCContext].
* Log4J 2 ThreadContext integration via [Log4JThreadContext]

## Documentation

Expand Down Expand Up @@ -265,6 +266,9 @@ The `develop` branch is pushed to `master` during release.
<!--- MODULE kotlinx-coroutines-slf4j -->
<!--- INDEX kotlinx.coroutines.slf4j -->
[MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.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
<!--- MODULE kotlinx-coroutines-jdk8 -->
<!--- INDEX kotlinx.coroutines.future -->
[CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html
Expand Down
1 change: 1 addition & 0 deletions integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Module name below corresponds to the artifact name in Maven/Gradle.
* [kotlinx-coroutines-guava](kotlinx-coroutines-guava/README.md) -- integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
* [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j/README.md) -- integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
* [kotlinx-coroutines-play-services](kotlinx-coroutines-play-services) -- integration with Google Play Services [Tasks API](https://developers.google.com/android/guides/tasks).
* [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).

## Contributing

Expand Down
24 changes: 24 additions & 0 deletions integration/kotlinx-coroutines-log4j/README.md
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 -->
11 changes: 11 additions & 0 deletions integration/kotlinx-coroutines-log4j/build.gradle
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")
}
}
1 change: 1 addition & 0 deletions integration/kotlinx-coroutines-log4j/package.list
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.apache.logging.log4j
102 changes: 102 additions & 0 deletions integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt
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()
)
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 integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt
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())
}
}
}
}
1 change: 1 addition & 0 deletions site/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Library support for Kotlin coroutines. This reference is a companion to
| [kotlinx-coroutines-guava](kotlinx-coroutines-guava) | Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained) |
| [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j) | Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html) |
| [kotlinx-coroutines-play-services](kotlinx-coroutines-play-services) | Integration with Google Play Services [Tasks API](https://developers.google.com/android/guides/tasks) |
| [kotlinx-coroutines-log4j](kotlinx-coroutines-log4j) | Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html) |

## Examples

Expand Down

0 comments on commit f272f8f

Please sign in to comment.