From c4ff503b670bef5a42bbca435b6229e9de312e95 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Mon, 10 Feb 2020 23:30:29 +0100 Subject: [PATCH 1/6] 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. --- README.md | 4 + integration/README.md | 1 + .../kotlinx-coroutines-log4j/README.md | 24 ++++ .../kotlinx-coroutines-log4j/build.gradle | 11 ++ .../kotlinx-coroutines-log4j/package.list | 1 + .../src/Log4JThreadContext.kt | 102 ++++++++++++++ .../test-resources/log4j2-test.xml | 16 +++ .../test/Log4JThreadContextTest.kt | 129 ++++++++++++++++++ site/docs/index.md | 1 + 9 files changed, 289 insertions(+) create mode 100644 integration/kotlinx-coroutines-log4j/README.md create mode 100644 integration/kotlinx-coroutines-log4j/build.gradle create mode 100644 integration/kotlinx-coroutines-log4j/package.list create mode 100644 integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt create mode 100644 integration/kotlinx-coroutines-log4j/test-resources/log4j2-test.xml create mode 100644 integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt diff --git a/README.md b/README.md index 8bafa78efa..d27c2d651d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,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 @@ -270,6 +271,9 @@ The `develop` branch is pushed to `master` during release. [MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html + + +[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html [CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html diff --git a/integration/README.md b/integration/README.md index 89100179a8..1f58b80849 100644 --- a/integration/README.md +++ b/integration/README.md @@ -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 diff --git a/integration/kotlinx-coroutines-log4j/README.md b/integration/kotlinx-coroutines-log4j/README.md new file mode 100644 index 0000000000..58ca22e89a --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/README.md @@ -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). + + + +[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html + diff --git a/integration/kotlinx-coroutines-log4j/build.gradle b/integration/kotlinx-coroutines-log4j/build.gradle new file mode 100644 index 0000000000..b63ae7ec5d --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/build.gradle @@ -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") + } +} \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/package.list b/integration/kotlinx-coroutines-log4j/package.list new file mode 100644 index 0000000000..3792c188a8 --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/package.list @@ -0,0 +1 @@ +org.apache.logging.log4j \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt b/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt new file mode 100644 index 0000000000..dbde275d2c --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt @@ -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, AbstractCoroutineContextElement(Key) { + /** + * Key of [Log4JThreadContext] in [CoroutineContext]. + */ + companion object Key : CoroutineContext.Key + + /** @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 = ThreadContext.getImmutableContext(), + val ndc: ThreadContext.ContextStack = ThreadContext.getImmutableStack() +) \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/test-resources/log4j2-test.xml b/integration/kotlinx-coroutines-log4j/test-resources/log4j2-test.xml new file mode 100644 index 0000000000..6799e341b3 --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/test-resources/log4j2-test.xml @@ -0,0 +1,16 @@ + + + + + + %X{first} %X{last} - %m%n + + + + + + + + + + \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt b/integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt new file mode 100644 index 0000000000..a286b5474b --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt @@ -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()) + } + } + } +} \ No newline at end of file diff --git a/site/docs/index.md b/site/docs/index.md index 3e6bb93494..ba34345faf 100644 --- a/site/docs/index.md +++ b/site/docs/index.md @@ -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 From e67dac1e0df582a43b2842d8370d4e17ad82bb16 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sat, 22 Feb 2020 22:45:56 +0100 Subject: [PATCH 2/6] fixup! Add an integration with Log4J 2's ThreadContext --- README.md | 4 +- .../kotlinx-coroutines-log4j/README.md | 12 +- .../kotlinx-coroutines-log4j/build.gradle | 4 +- .../src/Log4JThreadContext.kt | 102 ------------ .../src/Log4jDiagnosticContext.kt | 156 ++++++++++++++++++ ...tTest.kt => Log4jDiagnosticContextTest.kt} | 94 +++++------ kotlinx-coroutines-core/build.gradle | 14 +- settings.gradle | 1 + 8 files changed, 223 insertions(+), 164 deletions(-) delete mode 100644 integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt create mode 100644 integration/kotlinx-coroutines-log4j/src/Log4jDiagnosticContext.kt rename integration/kotlinx-coroutines-log4j/test/{Log4JThreadContextTest.kt => Log4jDiagnosticContextTest.kt} (54%) diff --git a/README.md b/README.md index d27c2d651d..f198f50e30 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ suspend fun main() = coroutineScope { * Android, JavaFX, and Swing. * [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] + * SLF4J MDC integration via [MDCContext]; + * Log4J 2 ThreadContext integration via [Log4JThreadContext]. ## Documentation diff --git a/integration/kotlinx-coroutines-log4j/README.md b/integration/kotlinx-coroutines-log4j/README.md index 58ca22e89a..0e16975555 100644 --- a/integration/kotlinx-coroutines-log4j/README.md +++ b/integration/kotlinx-coroutines-log4j/README.md @@ -4,13 +4,17 @@ Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/ ## Example -Add [Log4JThreadContext] to the coroutine context so that the Log4J `ThreadContext` state is captured and passed into the coroutine. +Add a [DiagnosticContext] to the coroutine context so that the Log4J `ThreadContext` state is set for the duration of +coroutine context. ```kotlin -ThreadContext.put("kotlin", "rocks") +launch(MutableDiagnosticContext().put("kotlin", "rocks")) { + logger.info(...) // The ThreadContext will contain the mapping here +} -launch(Log4JThreadContext()) { - logger.info(...) // the ThreadContext will contain the mapping here +// If not modifying the context state, use an immutable context for fewer allocations +launch(immutableDiagnosticContext()) { + logger.info(...) } ``` diff --git a/integration/kotlinx-coroutines-log4j/build.gradle b/integration/kotlinx-coroutines-log4j/build.gradle index b63ae7ec5d..dd78bf322a 100644 --- a/integration/kotlinx-coroutines-log4j/build.gradle +++ b/integration/kotlinx-coroutines-log4j/build.gradle @@ -1,6 +1,6 @@ dependencies { - implementation 'org.apache.logging.log4j:log4j-api:2.13.0' - testImplementation 'org.apache.logging.log4j:log4j-core:2.13.0' + implementation 'org.apache.logging.log4j:log4j-api:2.13.1' + testImplementation 'org.apache.logging.log4j:log4j-core:2.13.1' } tasks.withType(dokka.getClass()) { diff --git a/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt b/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt deleted file mode 100644 index dbde275d2c..0000000000 --- a/integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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, AbstractCoroutineContextElement(Key) { - /** - * Key of [Log4JThreadContext] in [CoroutineContext]. - */ - companion object Key : CoroutineContext.Key - - /** @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 = ThreadContext.getImmutableContext(), - val ndc: ThreadContext.ContextStack = ThreadContext.getImmutableStack() -) \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/src/Log4jDiagnosticContext.kt b/integration/kotlinx-coroutines-log4j/src/Log4jDiagnosticContext.kt new file mode 100644 index 0000000000..a611683e3a --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/src/Log4jDiagnosticContext.kt @@ -0,0 +1,156 @@ +/* + * 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.ThreadContextElement +import org.apache.logging.log4j.ThreadContext +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext + +/** + * Creates a new, immutable, [DiagnosticContext]. + * + * See [DiagnosticContext] for usage instructions. + * + * If not modifying the [ThreadContext], this method is preferred over [MutableDiagnosticContext] as it performs fewer + * unnecessary allocations. + */ +public fun immutableDiagnosticContext(): DiagnosticContext = DiagnosticContext(ThreadContext.getImmutableContext()) + +/** + * Enables the use of Log4J 2's [ThreadContext] with coroutines. + * + * See [DiagnosticContext] for usage instructions. + * + * @param mappedContext Mapped diagnostic context to apply for the duration of the corresponding [CoroutineContext]. + */ +public class MutableDiagnosticContext private constructor( + // Local reference so we can mutate the state + private val mappedContext: MutableMap +) : DiagnosticContext(mappedContext, ThreadContext.getImmutableContext()) { + + /** + * Creates a new [MutableDiagnosticContext] populated with the current [ThreadContext]. + * + * If not intending to modify the [ThreadContext], consider using [immutableDiagnosticContext] instead. + * [immutableDiagnosticContext] is preferred in this case as it performs fewer unnecessary allocations. + */ + public constructor() : this(ThreadContext.getContext()) + + /** + * Adds an entry to the Log4J context map. + * + * The entry will remain as part of the diagnostic context for the duration of the current coroutine context. + * + * This is the coroutine-compatible equivalent of [ThreadContext.put]. + * + * @param key Key of the entry to add to the diagnostic context. + * @param value Value of the entry to add to the diagnostic context. + * @return This instance. + */ + public fun put(key: String, value: String?): MutableDiagnosticContext { + mappedContext[key] = value + return this + } + + /** + * Adds all entries to the Log4J context map. + * + * The entries will remain as part of the diagnostic context for the duration of the current coroutine context. + * + * This is the coroutine-compatible equivalent of [ThreadContext.putAll]. + * + * @param from Entries to add to the diagnostic context. + * @return This instance. + */ + public fun putAll(from: Map): MutableDiagnosticContext { + mappedContext.putAll(from) + return this + } +} + +/** + * Enables the use of Log4J 2's [ThreadContext] with coroutines. + * + * # Example + * The following example demonstrates usage of this class. All `assert`s pass. Note that only the mapped diagnostic + * context is supported. + * + * ```kotlin + * ThreadContext.put("kotlin", "rocks") // Put a value into the ThreadContext. + * launch(immutableDiagnosticContext()) { // The contents of the ThreadContext are captured into the newly created CoroutineContext. + * assert(ThreadContext.get("kotlin") == "rocks") + * + * withContext(MutableDiagnosticContext().put("kotlin", "is great") { + * assert(ThreadContext.get("kotlin") == "is great") + * + * launch(Dispatchers.IO) { + * assert(ThreadContext.get("kotlin") == "is great") // The diagnostic context is inherited by child CoroutineContexts. + * } + * } + * assert(ThreadContext.get("kotlin") == "rocks") // The ThreadContext is reset when the CoroutineContext exits. + * } + * ``` + * + * ## Combine with others + * You may wish to combine this [ThreadContextElement] with other [CoroutineContext]s. + * + * ```kotlin + * launch(Dispatchers.IO + immutableDiagnosticContext()) { ... } + * ``` + * + * # 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(MutableDiagnosticContext().put("kotlin", "is awesome") { + * assert(ThreadContext.get("kotlin") == "is awesome") + * } + * assert(ThreadContext.get("kotlin") == "rocks") + * ``` + * + * @param mappedContextBefore Mapped diagnostic context to apply for the duration of the corresponding [CoroutineContext]. + * @param mappedContextAfter Mapped diagnostic context to restore when the corresponding [CoroutineContext] exits. + */ +public open class DiagnosticContext internal constructor( + private val mappedContextBefore: Map, + private val mappedContextAfter: Map = mappedContextBefore +) : ThreadContextElement, AbstractCoroutineContextElement(Key) { + + /** + * Key of [DiagnosticContext] in [CoroutineContext]. + */ + public companion object Key : CoroutineContext.Key + + /** @suppress */ + final override fun updateThreadContext(context: CoroutineContext): DiagnosticContext { + setCurrent(mappedContextBefore) + return this + } + + /** @suppress */ + final override fun restoreThreadContext(context: CoroutineContext, oldState: DiagnosticContext) { + setCurrent(oldState.mappedContextAfter) + } + + private fun setCurrent(map: Map) { + /* + * The logic here varies significantly from how CloseableThreadContext works. CloseableThreadContext has the + * luxury of assuming that it is appending new state to the existing state of the current thread. We cannot make + * this assumption. It is very realistic for us to be restoring a context to a thread that has loads of state + * that we are not at all interested in, due to the Log4J ThreadContext being implemented as a ThreadLocal. + * + * So, to make sure that the ThreadLocal belonging to the Thread servicing this CoroutineContext is has the + * correct state, we first clear everything existing, and then apply the desired state. + */ + ThreadContext.clearMap() + ThreadContext.putAll(map) + } +} \ No newline at end of file diff --git a/integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt b/integration/kotlinx-coroutines-log4j/test/Log4jDiagnosticContextTest.kt similarity index 54% rename from integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt rename to integration/kotlinx-coroutines-log4j/test/Log4jDiagnosticContextTest.kt index a286b5474b..00b95e9501 100644 --- a/integration/kotlinx-coroutines-log4j/test/Log4JThreadContextTest.kt +++ b/integration/kotlinx-coroutines-log4j/test/Log4jDiagnosticContextTest.kt @@ -5,21 +5,17 @@ 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() - } +class Log4jDiagnosticContextTest : TestBase() { + @Before @After - fun tearDown() { + fun clearThreadContext() { ThreadContext.clearAll() } @@ -27,103 +23,107 @@ class Log4JThreadContextTest : TestBase() { 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 { + fun testImmutableContextContainsOriginalContent() = runTest { expect(1) ThreadContext.put("myKey", "myValue") - ThreadContext.push("stack1") - // Scoped launch with Log4JThreadContext element - launch(Log4JThreadContext()) { + // Standalone launch + GlobalScope.launch(immutableDiagnosticContext()) { assertEquals("myValue", ThreadContext.get("myKey")) - assertEquals("stack1", ThreadContext.peek()) expect(2) }.join() + finish(3) + } + @Test + fun testMutableContextContainsOriginalContent() = runTest { + expect(1) + ThreadContext.put("myKey", "myValue") + // Standalone launch + GlobalScope.launch(MutableDiagnosticContext()) { + assertEquals("myValue", ThreadContext.get("myKey")) + expect(2) + }.join() finish(3) } @Test fun testContextInheritance() = runTest { expect(1) - ThreadContext.put("myKey", "myValue") - ThreadContext.push("stack1") - withContext(Log4JThreadContext()) { + withContext(MutableDiagnosticContext() + .put("myKey", "myValue") + ) { + // Update the global ThreadContext. This isn't tied to the CoroutineContext though, so shouldn't get used. 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() { + fun testContextPassedWhileOnSameThread() { 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()) + fun testImmutableContextMayBeEmpty() { + runBlocking(immutableDiagnosticContext()) { + assertEquals(null, ThreadContext.get("myKey")) } } @Test fun testContextMayBeEmpty() { - runBlocking(Log4JThreadContext()) { + runBlocking(MutableDiagnosticContext()) { assertEquals(null, ThreadContext.get("myKey")) - assertEquals("", ThreadContext.peek()) } } @Test - fun testContextWithContext() = runTest { - ThreadContext.put("myKey", "myValue") - ThreadContext.push("stack1") + fun testCoroutineContextWithLoggingContext() = runTest { val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!! - withContext(Dispatchers.Default + Log4JThreadContext()) { + withContext(Dispatchers.Default + + MutableDiagnosticContext().put("myKey", "myValue") + ) { assertEquals("myValue", ThreadContext.get("myKey")) - assertEquals("stack1", ThreadContext.peek()) withContext(mainDispatcher) { assertEquals("myValue", ThreadContext.get("myKey")) - assertEquals("stack1", ThreadContext.peek()) } } } + + @Test + fun testNestedContexts() { + runBlocking(MutableDiagnosticContext().put("key", "value")) { + withContext(MutableDiagnosticContext().put("key", "value2")){ + assertEquals("value2", ThreadContext.get("key")) + } + assertEquals("value", ThreadContext.get("key")) + } + } + + @Test + fun testAcceptsNullValues() { + runBlocking(MutableDiagnosticContext().put("key", null)) { + assertNull(ThreadContext.get("key")) + } + } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 547a12b4c6..f08b5671a9 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -65,13 +65,13 @@ kotlin.sourceSets { task checkJdk16() { // only fail w/o JDK_16 when actually trying to compile, not during project setup phase - doLast { - if (!System.env.JDK_16) { - throw new GradleException("JDK_16 environment variable is not defined. " + - "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + - "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") - } - } +// doLast { +// if (!System.env.JDK_16) { +// throw new GradleException("JDK_16 environment variable is not defined. " + +// "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + +// "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") +// } +// } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { diff --git a/settings.gradle b/settings.gradle index 95fcd7cb2d..3ca7ef8152 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ module('integration/kotlinx-coroutines-guava') module('integration/kotlinx-coroutines-jdk8') module('integration/kotlinx-coroutines-slf4j') module('integration/kotlinx-coroutines-play-services') +module('integration/kotlinx-coroutines-log4j') module('reactive/kotlinx-coroutines-reactive') module('reactive/kotlinx-coroutines-reactor') From 29e2d4b672310c8ad879d2450377132feb06f1de Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sat, 18 Apr 2020 20:01:03 +0200 Subject: [PATCH 3/6] fixup! Add an integration with Log4J 2's ThreadContext --- kotlinx-coroutines-core/build.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index f08b5671a9..547a12b4c6 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -65,13 +65,13 @@ kotlin.sourceSets { task checkJdk16() { // only fail w/o JDK_16 when actually trying to compile, not during project setup phase -// doLast { -// if (!System.env.JDK_16) { -// throw new GradleException("JDK_16 environment variable is not defined. " + -// "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + -// "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") -// } -// } + doLast { + if (!System.env.JDK_16) { + throw new GradleException("JDK_16 environment variable is not defined. " + + "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + + "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { From a95344260ffad3b52e8d5b706c262d9a3ea0797f Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sat, 18 Apr 2020 20:12:43 +0200 Subject: [PATCH 4/6] fixup! Add an integration with Log4J 2's ThreadContext --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f198f50e30..1172d47a9b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,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]. + * Log4J 2 ThreadContext integration via [MutableDiagnosticContext] and [immutableDiagnosticContext()]. ## Documentation From 59ad41dd8d70562a75424cd930ad6a389e2884c2 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sat, 18 Apr 2020 20:32:26 +0200 Subject: [PATCH 5/6] fixup! Add an integration with Log4J 2's ThreadContext --- README.md | 5 +++-- integration/kotlinx-coroutines-log4j/README.md | 2 +- kotlinx-coroutines-core/build.gradle | 14 +++++++------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1172d47a9b..4fbd99a7cc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,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 [MutableDiagnosticContext] and [immutableDiagnosticContext()]. + * Log4J 2 ThreadContext integration via [MutableDiagnosticContext] and [immutableDiagnosticContext]. ## Documentation @@ -273,7 +273,8 @@ The `develop` branch is pushed to `master` during release. [MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.slf4j/-m-d-c-context/index.html -[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html +[MutableDiagnosticContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-mutable-diagnostic-context/index.html +[immutableDiagnosticContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/immutable-diagnostic-context.html [CompletionStage.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-jdk8/kotlinx.coroutines.future/java.util.concurrent.-completion-stage/await.html diff --git a/integration/kotlinx-coroutines-log4j/README.md b/integration/kotlinx-coroutines-log4j/README.md index 0e16975555..6a67dda656 100644 --- a/integration/kotlinx-coroutines-log4j/README.md +++ b/integration/kotlinx-coroutines-log4j/README.md @@ -24,5 +24,5 @@ Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/ -[Log4JThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-log4-j-thread-context/index.html +[DiagnosticContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-diagnostic-context/index.html diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index 547a12b4c6..f08b5671a9 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -65,13 +65,13 @@ kotlin.sourceSets { task checkJdk16() { // only fail w/o JDK_16 when actually trying to compile, not during project setup phase - doLast { - if (!System.env.JDK_16) { - throw new GradleException("JDK_16 environment variable is not defined. " + - "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + - "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") - } - } +// doLast { +// if (!System.env.JDK_16) { +// throw new GradleException("JDK_16 environment variable is not defined. " + +// "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + +// "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") +// } +// } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { From 766905713e4d2be1d82bf3f5a2eb34e7ce0f3400 Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sat, 18 Apr 2020 20:32:45 +0200 Subject: [PATCH 6/6] fixup! Add an integration with Log4J 2's ThreadContext --- kotlinx-coroutines-core/build.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kotlinx-coroutines-core/build.gradle b/kotlinx-coroutines-core/build.gradle index f08b5671a9..547a12b4c6 100644 --- a/kotlinx-coroutines-core/build.gradle +++ b/kotlinx-coroutines-core/build.gradle @@ -65,13 +65,13 @@ kotlin.sourceSets { task checkJdk16() { // only fail w/o JDK_16 when actually trying to compile, not during project setup phase -// doLast { -// if (!System.env.JDK_16) { -// throw new GradleException("JDK_16 environment variable is not defined. " + -// "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + -// "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") -// } -// } + doLast { + if (!System.env.JDK_16) { + throw new GradleException("JDK_16 environment variable is not defined. " + + "Can't build against JDK 1.6 runtime and run JDK 1.6 compatibility tests. " + + "Please ensure JDK 1.6 is installed and that JDK_16 points to it.") + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {