diff --git a/README.md b/README.md index 8bafa78efa..4fbd99a7cc 100644 --- a/README.md +++ b/README.md @@ -52,7 +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]. + * SLF4J MDC integration via [MDCContext]; + * Log4J 2 ThreadContext integration via [MutableDiagnosticContext] and [immutableDiagnosticContext]. ## Documentation @@ -270,6 +271,10 @@ 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 + + +[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/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..6a67dda656 --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/README.md @@ -0,0 +1,28 @@ +# Module kotlinx-coroutines-log4j + +Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html). + +## Example + +Add a [DiagnosticContext] to the coroutine context so that the Log4J `ThreadContext` state is set for the duration of +coroutine context. + +```kotlin +launch(MutableDiagnosticContext().put("kotlin", "rocks")) { + 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(...) +} +``` + +# Package kotlinx.coroutines.log4jj + +Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/manual/thread-context.html). + + + +[DiagnosticContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-diagnostic-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..dd78bf322a --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/build.gradle @@ -0,0 +1,11 @@ +dependencies { + implementation 'org.apache.logging.log4j:log4j-api:2.13.1' + testImplementation 'org.apache.logging.log4j:log4j-core:2.13.1' +} + +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/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-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/Log4jDiagnosticContextTest.kt b/integration/kotlinx-coroutines-log4j/test/Log4jDiagnosticContextTest.kt new file mode 100644 index 0000000000..00b95e9501 --- /dev/null +++ b/integration/kotlinx-coroutines-log4j/test/Log4jDiagnosticContextTest.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.ThreadContext +import org.junit.* +import org.junit.Test +import kotlin.coroutines.* +import kotlin.test.* + +class Log4jDiagnosticContextTest : TestBase() { + + @Before + @After + fun clearThreadContext() { + ThreadContext.clearAll() + } + + @Test + fun testContextIsNotPassedByDefaultBetweenCoroutines() = runTest { + expect(1) + ThreadContext.put("myKey", "myValue") + // Standalone launch + GlobalScope.launch { + assertEquals(null, ThreadContext.get("myKey")) + expect(2) + }.join() + finish(3) + } + + @Test + fun testImmutableContextContainsOriginalContent() = runTest { + expect(1) + ThreadContext.put("myKey", "myValue") + // Standalone launch + GlobalScope.launch(immutableDiagnosticContext()) { + assertEquals("myValue", ThreadContext.get("myKey")) + 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) + 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") + // Scoped launch with inherited Log4JThreadContext element + launch(Dispatchers.Default) { + assertEquals("myValue", ThreadContext.get("myKey")) + expect(2) + }.join() + + finish(3) + } + assertEquals("myValue", ThreadContext.get("myKey")) + } + + @Test + fun testContextPassedWhileOnSameThread() { + ThreadContext.put("myKey", "myValue") + // No ThreadContext element + runBlocking { + assertEquals("myValue", ThreadContext.get("myKey")) + } + } + + @Test + fun testImmutableContextMayBeEmpty() { + runBlocking(immutableDiagnosticContext()) { + assertEquals(null, ThreadContext.get("myKey")) + } + } + + @Test + fun testContextMayBeEmpty() { + runBlocking(MutableDiagnosticContext()) { + assertEquals(null, ThreadContext.get("myKey")) + } + } + + @Test + fun testCoroutineContextWithLoggingContext() = runTest { + val mainDispatcher = kotlin.coroutines.coroutineContext[ContinuationInterceptor]!! + withContext(Dispatchers.Default + + MutableDiagnosticContext().put("myKey", "myValue") + ) { + assertEquals("myValue", ThreadContext.get("myKey")) + withContext(mainDispatcher) { + assertEquals("myValue", ThreadContext.get("myKey")) + } + } + } + + @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/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') 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