Skip to content

Commit e67dac1

Browse files
fixup! Add an integration with Log4J 2's ThreadContext
1 parent c4ff503 commit e67dac1

File tree

8 files changed

+223
-164
lines changed

8 files changed

+223
-164
lines changed

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ suspend fun main() = coroutineScope {
5252
* Android, JavaFX, and Swing.
5353
* [integration](integration/README.md) — modules that provide integration with various asynchronous callback- and future-based libraries:
5454
* JDK8 [CompletionStage.await], Guava [ListenableFuture.await], and Google Play Services [Task.await];
55-
* SLF4J MDC integration via [MDCContext].
56-
* Log4J 2 ThreadContext integration via [Log4JThreadContext]
55+
* SLF4J MDC integration via [MDCContext];
56+
* Log4J 2 ThreadContext integration via [Log4JThreadContext].
5757

5858
## Documentation
5959

Diff for: integration/kotlinx-coroutines-log4j/README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ Integration with Log4J 2's [ThreadContext](https://logging.apache.org/log4j/2.x/
44

55
## Example
66

7-
Add [Log4JThreadContext] to the coroutine context so that the Log4J `ThreadContext` state is captured and passed into the coroutine.
7+
Add a [DiagnosticContext] to the coroutine context so that the Log4J `ThreadContext` state is set for the duration of
8+
coroutine context.
89

910
```kotlin
10-
ThreadContext.put("kotlin", "rocks")
11+
launch(MutableDiagnosticContext().put("kotlin", "rocks")) {
12+
logger.info(...) // The ThreadContext will contain the mapping here
13+
}
1114

12-
launch(Log4JThreadContext()) {
13-
logger.info(...) // the ThreadContext will contain the mapping here
15+
// If not modifying the context state, use an immutable context for fewer allocations
16+
launch(immutableDiagnosticContext()) {
17+
logger.info(...)
1418
}
1519
```
1620

Diff for: integration/kotlinx-coroutines-log4j/build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
dependencies {
2-
implementation 'org.apache.logging.log4j:log4j-api:2.13.0'
3-
testImplementation 'org.apache.logging.log4j:log4j-core:2.13.0'
2+
implementation 'org.apache.logging.log4j:log4j-api:2.13.1'
3+
testImplementation 'org.apache.logging.log4j:log4j-core:2.13.1'
44
}
55

66
tasks.withType(dokka.getClass()) {

Diff for: integration/kotlinx-coroutines-log4j/src/Log4JThreadContext.kt

-102
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines.log4j
6+
7+
import kotlinx.coroutines.ThreadContextElement
8+
import org.apache.logging.log4j.ThreadContext
9+
import kotlin.coroutines.AbstractCoroutineContextElement
10+
import kotlin.coroutines.CoroutineContext
11+
12+
/**
13+
* Creates a new, immutable, [DiagnosticContext].
14+
*
15+
* See [DiagnosticContext] for usage instructions.
16+
*
17+
* If not modifying the [ThreadContext], this method is preferred over [MutableDiagnosticContext] as it performs fewer
18+
* unnecessary allocations.
19+
*/
20+
public fun immutableDiagnosticContext(): DiagnosticContext = DiagnosticContext(ThreadContext.getImmutableContext())
21+
22+
/**
23+
* Enables the use of Log4J 2's [ThreadContext] with coroutines.
24+
*
25+
* See [DiagnosticContext] for usage instructions.
26+
*
27+
* @param mappedContext Mapped diagnostic context to apply for the duration of the corresponding [CoroutineContext].
28+
*/
29+
public class MutableDiagnosticContext private constructor(
30+
// Local reference so we can mutate the state
31+
private val mappedContext: MutableMap<String, String?>
32+
) : DiagnosticContext(mappedContext, ThreadContext.getImmutableContext()) {
33+
34+
/**
35+
* Creates a new [MutableDiagnosticContext] populated with the current [ThreadContext].
36+
*
37+
* If not intending to modify the [ThreadContext], consider using [immutableDiagnosticContext] instead.
38+
* [immutableDiagnosticContext] is preferred in this case as it performs fewer unnecessary allocations.
39+
*/
40+
public constructor() : this(ThreadContext.getContext())
41+
42+
/**
43+
* Adds an entry to the Log4J context map.
44+
*
45+
* The entry will remain as part of the diagnostic context for the duration of the current coroutine context.
46+
*
47+
* This is the coroutine-compatible equivalent of [ThreadContext.put].
48+
*
49+
* @param key Key of the entry to add to the diagnostic context.
50+
* @param value Value of the entry to add to the diagnostic context.
51+
* @return This instance.
52+
*/
53+
public fun put(key: String, value: String?): MutableDiagnosticContext {
54+
mappedContext[key] = value
55+
return this
56+
}
57+
58+
/**
59+
* Adds all entries to the Log4J context map.
60+
*
61+
* The entries will remain as part of the diagnostic context for the duration of the current coroutine context.
62+
*
63+
* This is the coroutine-compatible equivalent of [ThreadContext.putAll].
64+
*
65+
* @param from Entries to add to the diagnostic context.
66+
* @return This instance.
67+
*/
68+
public fun putAll(from: Map<String, String?>): MutableDiagnosticContext {
69+
mappedContext.putAll(from)
70+
return this
71+
}
72+
}
73+
74+
/**
75+
* Enables the use of Log4J 2's [ThreadContext] with coroutines.
76+
*
77+
* # Example
78+
* The following example demonstrates usage of this class. All `assert`s pass. Note that only the mapped diagnostic
79+
* context is supported.
80+
*
81+
* ```kotlin
82+
* ThreadContext.put("kotlin", "rocks") // Put a value into the ThreadContext.
83+
* launch(immutableDiagnosticContext()) { // The contents of the ThreadContext are captured into the newly created CoroutineContext.
84+
* assert(ThreadContext.get("kotlin") == "rocks")
85+
*
86+
* withContext(MutableDiagnosticContext().put("kotlin", "is great") {
87+
* assert(ThreadContext.get("kotlin") == "is great")
88+
*
89+
* launch(Dispatchers.IO) {
90+
* assert(ThreadContext.get("kotlin") == "is great") // The diagnostic context is inherited by child CoroutineContexts.
91+
* }
92+
* }
93+
* assert(ThreadContext.get("kotlin") == "rocks") // The ThreadContext is reset when the CoroutineContext exits.
94+
* }
95+
* ```
96+
*
97+
* ## Combine with others
98+
* You may wish to combine this [ThreadContextElement] with other [CoroutineContext]s.
99+
*
100+
* ```kotlin
101+
* launch(Dispatchers.IO + immutableDiagnosticContext()) { ... }
102+
* ```
103+
*
104+
* # CloseableThreadContext
105+
* [org.apache.logging.log4j.CloseableThreadContext] is useful for automatically cleaning up the [ThreadContext] after a
106+
* block of code. The structured concurrency provided by coroutines offers the same functionality.
107+
*
108+
* In the following example, the modifications to the [ThreadContext] are cleaned up when the coroutine exits.
109+
*
110+
* ```kotlin
111+
* ThreadContext.put("kotlin", "rocks")
112+
*
113+
* withContext(MutableDiagnosticContext().put("kotlin", "is awesome") {
114+
* assert(ThreadContext.get("kotlin") == "is awesome")
115+
* }
116+
* assert(ThreadContext.get("kotlin") == "rocks")
117+
* ```
118+
*
119+
* @param mappedContextBefore Mapped diagnostic context to apply for the duration of the corresponding [CoroutineContext].
120+
* @param mappedContextAfter Mapped diagnostic context to restore when the corresponding [CoroutineContext] exits.
121+
*/
122+
public open class DiagnosticContext internal constructor(
123+
private val mappedContextBefore: Map<String, String?>,
124+
private val mappedContextAfter: Map<String, String?> = mappedContextBefore
125+
) : ThreadContextElement<DiagnosticContext>, AbstractCoroutineContextElement(Key) {
126+
127+
/**
128+
* Key of [DiagnosticContext] in [CoroutineContext].
129+
*/
130+
public companion object Key : CoroutineContext.Key<DiagnosticContext>
131+
132+
/** @suppress */
133+
final override fun updateThreadContext(context: CoroutineContext): DiagnosticContext {
134+
setCurrent(mappedContextBefore)
135+
return this
136+
}
137+
138+
/** @suppress */
139+
final override fun restoreThreadContext(context: CoroutineContext, oldState: DiagnosticContext) {
140+
setCurrent(oldState.mappedContextAfter)
141+
}
142+
143+
private fun setCurrent(map: Map<String, String?>) {
144+
/*
145+
* The logic here varies significantly from how CloseableThreadContext works. CloseableThreadContext has the
146+
* luxury of assuming that it is appending new state to the existing state of the current thread. We cannot make
147+
* this assumption. It is very realistic for us to be restoring a context to a thread that has loads of state
148+
* that we are not at all interested in, due to the Log4J ThreadContext being implemented as a ThreadLocal.
149+
*
150+
* So, to make sure that the ThreadLocal belonging to the Thread servicing this CoroutineContext is has the
151+
* correct state, we first clear everything existing, and then apply the desired state.
152+
*/
153+
ThreadContext.clearMap()
154+
ThreadContext.putAll(map)
155+
}
156+
}

0 commit comments

Comments
 (0)