Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an integration with Log4J 2's ThreadContext #1794

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -270,6 +271,10 @@ 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 -->
[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
<!--- 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
28 changes: 28 additions & 0 deletions integration/kotlinx-coroutines-log4j/README.md
Original file line number Diff line number Diff line change
@@ -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).

<!--- MODULE kotlinx-coroutines-log4j -->
<!--- INDEX kotlinx.coroutines.log4j -->
[DiagnosticContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-log4j/kotlinx.coroutines.log4j/-diagnostic-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.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")
}
}
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
156 changes: 156 additions & 0 deletions integration/kotlinx-coroutines-log4j/src/Log4jDiagnosticContext.kt
Original file line number Diff line number Diff line change
@@ -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<String, String?>
) : 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<String, String?>): 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<String, String?>,
private val mappedContextAfter: Map<String, String?> = mappedContextBefore
) : ThreadContextElement<DiagnosticContext>, AbstractCoroutineContextElement(Key) {

/**
* Key of [DiagnosticContext] in [CoroutineContext].
*/
public companion object Key : CoroutineContext.Key<DiagnosticContext>

/** @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<String, String?>) {
/*
* 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)
}
}
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>
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.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"))
}
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
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