Skip to content

Commit 844e08e

Browse files
committed
Merge branch 'rg/context-convenience-fns'
* rg/context-convenience-fns: Update to 1.5.0-SNAPSHOT Additional coroutine context tests Add docs about context and coroutines integration Add convenience functions coroutines thread context
2 parents 2049947 + 3d21dcb commit 844e08e

File tree

5 files changed

+338
-4
lines changed

5 files changed

+338
-4
lines changed

log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package org.apache.logging.log4j.kotlin
1818

1919
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
20+
import kotlinx.coroutines.CoroutineScope
2021
import kotlinx.coroutines.ThreadContextElement
22+
import kotlinx.coroutines.withContext
2123
import org.apache.logging.log4j.ThreadContext
2224
import kotlin.coroutines.AbstractCoroutineContextElement
2325
import kotlin.coroutines.CoroutineContext
@@ -34,7 +36,12 @@ import kotlin.coroutines.CoroutineContext
3436
data class ThreadContextData(
3537
val map: Map<String, String>? = ContextMap.view,
3638
val stack: Collection<String>? = ContextStack.view
37-
)
39+
) {
40+
operator fun plus(data: ThreadContextData) = ThreadContextData(
41+
map = this.map.orEmpty() + data.map.orEmpty(),
42+
stack = this.stack.orEmpty() + data.stack.orEmpty(),
43+
)
44+
}
3845

3946
/**
4047
* Log4j2 [ThreadContext] element for [CoroutineContext].
@@ -59,6 +66,9 @@ data class ThreadContextData(
5966
* Use `withContext(CoroutineThreadContext()) { ... }` to capture updated map of Thread keys and values
6067
* for the specified block of code.
6168
*
69+
* See [loggingContext] and [additionalLoggingContext] for convenience functions that make working with a
70+
* [CoroutineThreadContext] simpler.
71+
*
6272
* @param contextData the value of [Thread] context map and context stack.
6373
* Default value is the copy of the current thread's context map that is acquired via
6474
* [ContextMap.view] and [ContextStack.view].
@@ -95,3 +105,73 @@ class CoroutineThreadContext(
95105
contextData.stack?.let { ContextStack.set(it) }
96106
}
97107
}
108+
109+
/**
110+
* Convenience function to obtain a [CoroutineThreadContext] with the given map and stack, which default
111+
* to no context. Any existing logging context in scope is ignored.
112+
*
113+
* Example:
114+
*
115+
* ```
116+
* launch(loggingContext(mapOf("kotlin" to "rocks"))) {
117+
* logger.info { "..." } // The Thread context contains the mapping here
118+
* }
119+
* ```
120+
*/
121+
fun loggingContext(
122+
map: Map<String, String>? = null,
123+
stack: Collection<String>? = null,
124+
): CoroutineThreadContext = CoroutineThreadContext(ThreadContextData(map = map, stack = stack))
125+
126+
/**
127+
* Convenience function to obtain a [CoroutineThreadContext] that inherits the current context (if any), plus adds
128+
* the context from the given map and stack, which default to nothing.
129+
*
130+
* Example:
131+
*
132+
* ```
133+
* launch(additionalLoggingContext(mapOf("kotlin" to "rocks"))) {
134+
* logger.info { "..." } // The Thread context contains the mapping plus whatever context was in scope at launch
135+
* }
136+
* ```
137+
*/
138+
fun additionalLoggingContext(
139+
map: Map<String, String>? = null,
140+
stack: Collection<String>? = null,
141+
): CoroutineThreadContext = CoroutineThreadContext(ThreadContextData() + ThreadContextData(map = map, stack = stack))
142+
143+
/**
144+
* Run the given block with the provided logging context, which default to no context. Any existing logging context
145+
* in scope is ignored.
146+
*
147+
* Example:
148+
*
149+
* ```
150+
* withLoggingContext(mapOf("kotlin" to "rocks")) {
151+
* logger.info { "..." } // The Thread context contains the mapping
152+
* }
153+
* ```
154+
*/
155+
suspend fun <R> withLoggingContext(
156+
map: Map<String, String>? = null,
157+
stack: Collection<String>? = null,
158+
block: suspend CoroutineScope.() -> R,
159+
): R = withContext(loggingContext(map, stack), block)
160+
161+
/**
162+
* Run the given block with the provided additional logging context. The given context is added to any existing
163+
* logging context in scope.
164+
*
165+
* Example:
166+
*
167+
* ```
168+
* withAdditionalLoggingContext(mapOf("kotlin" to "rocks")) {
169+
* logger.info { "..." } // The Thread context contains the mapping plus whatever context was in the scope previously
170+
* }
171+
* ```
172+
*/
173+
suspend fun <R> withAdditionalLoggingContext(
174+
map: Map<String, String>? = null,
175+
stack: Collection<String>? = null,
176+
block: suspend CoroutineScope.() -> R,
177+
): R = withContext(additionalLoggingContext(map, stack), block)

log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ class ThreadContextTest {
5353
assertNull(ContextMap["myKey"])
5454
assertTrue(ContextStack.empty)
5555
}.join()
56+
GlobalScope.launch(loggingContext()) {
57+
assertNull(ContextMap["myKey"])
58+
assertTrue(ContextStack.empty)
59+
}.join()
5660
}
5761

5862
@DelicateCoroutinesApi
@@ -65,6 +69,10 @@ class ThreadContextTest {
6569
assertEquals("myValue", ContextMap["myKey"])
6670
assertEquals("test", ContextStack.peek())
6771
}.join()
72+
GlobalScope.launch(additionalLoggingContext()) {
73+
assertEquals("myValue", ContextMap["myKey"])
74+
assertEquals("test", ContextStack.peek())
75+
}.join()
6876
}
6977

7078
@Test
@@ -75,11 +83,37 @@ class ThreadContextTest {
7583
withContext(CoroutineThreadContext()) {
7684
ContextMap["myKey"] = "myValue2"
7785
ContextStack.push("test2")
78-
// Scoped launch with inherited MDContext element
86+
// Scoped launch with non-inherited MDContext element
7987
launch(Dispatchers.Default) {
8088
assertEquals("myValue", ContextMap["myKey"])
8189
assertEquals("test", ContextStack.peek())
8290
}
91+
// Scoped launch with non-inherited MDContext element
92+
launch(Dispatchers.Default + loggingContext()) {
93+
assertTrue(ContextMap.empty)
94+
assertTrue(ContextStack.empty)
95+
}
96+
// Scoped launch with non-inherited MDContext element
97+
launch(Dispatchers.Default + loggingContext(mapOf("myKey2" to "myValue2"), listOf("test3"))) {
98+
assertEquals(null, ContextMap["myKey"])
99+
assertEquals("myValue2", ContextMap["myKey2"])
100+
assertEquals(listOf("test3"), ContextStack.view.asList())
101+
}
102+
// Scoped launch with inherited MDContext element
103+
launch(Dispatchers.Default + CoroutineThreadContext()) {
104+
assertEquals("myValue2", ContextMap["myKey"])
105+
assertEquals("test2", ContextStack.peek())
106+
}
107+
// Scoped launch with inherited plus additional empty MDContext element
108+
launch(Dispatchers.Default + additionalLoggingContext()) {
109+
assertEquals("myValue2", ContextMap["myKey"])
110+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
111+
}
112+
launch(Dispatchers.Default + additionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test3"))) {
113+
assertEquals("myValue2", ContextMap["myKey"])
114+
assertEquals("myValue2", ContextMap["myKey2"])
115+
assertEquals(listOf("test", "test2", "test3"), ContextStack.view.asList())
116+
}
83117
}
84118
assertEquals("myValue", ContextMap["myKey"])
85119
assertEquals("test", ContextStack.peek())
@@ -104,6 +138,10 @@ class ThreadContextTest {
104138
assertEquals("myValue", ContextMap["myKey"])
105139
assertEquals("test", ContextStack.peek())
106140
}
141+
runBlocking(additionalLoggingContext()) {
142+
assertEquals("myValue", ContextMap["myKey"])
143+
assertEquals("test", ContextStack.peek())
144+
}
107145
}
108146

109147
@Test
@@ -112,10 +150,18 @@ class ThreadContextTest {
112150
assertTrue(ContextMap.empty)
113151
assertTrue(ContextStack.empty)
114152
}
153+
runBlocking(loggingContext()) {
154+
assertTrue(ContextMap.empty)
155+
assertTrue(ContextStack.empty)
156+
}
157+
runBlocking(additionalLoggingContext()) {
158+
assertTrue(ContextMap.empty)
159+
assertTrue(ContextStack.empty)
160+
}
115161
}
116162

117163
@Test
118-
fun `Context with context`() = runBlocking {
164+
fun `Context using withContext`() = runBlocking {
119165
ContextMap["myKey"] = "myValue"
120166
ContextStack.push("test")
121167
val mainDispatcher = coroutineContext[ContinuationInterceptor]!!
@@ -127,6 +173,80 @@ class ThreadContextTest {
127173
assertEquals("test", ContextStack.peek())
128174
}
129175
}
176+
withContext(Dispatchers.Default + additionalLoggingContext()) {
177+
assertEquals("myValue", ContextMap["myKey"])
178+
assertEquals("test", ContextStack.peek())
179+
withContext(mainDispatcher) {
180+
assertEquals("myValue", ContextMap["myKey"])
181+
assertEquals("test", ContextStack.peek())
182+
}
183+
}
184+
withContext(Dispatchers.Default + additionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
185+
assertEquals("myValue", ContextMap["myKey"])
186+
assertEquals("myValue2", ContextMap["myKey2"])
187+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
188+
withContext(mainDispatcher) {
189+
assertEquals("myValue", ContextMap["myKey"])
190+
assertEquals("myValue2", ContextMap["myKey2"])
191+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
192+
}
193+
}
194+
withContext(Dispatchers.Default + loggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
195+
assertEquals(null, ContextMap["myKey"])
196+
assertEquals("myValue2", ContextMap["myKey2"])
197+
assertEquals(listOf("test2"), ContextStack.view.asList())
198+
withContext(mainDispatcher) {
199+
assertEquals(null, ContextMap["myKey"])
200+
assertEquals("myValue2", ContextMap["myKey2"])
201+
assertEquals(listOf("test2"), ContextStack.view.asList())
202+
}
203+
}
204+
}
205+
206+
@Test
207+
fun `Context using withLoggingContext`() = runBlocking {
208+
ContextMap["myKey"] = "myValue"
209+
ContextStack.push("test")
210+
val mainDispatcher = coroutineContext[ContinuationInterceptor]!!
211+
withAdditionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
212+
assertEquals("myValue", ContextMap["myKey"])
213+
assertEquals("myValue2", ContextMap["myKey2"])
214+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
215+
withContext(mainDispatcher) {
216+
assertEquals("myValue", ContextMap["myKey"])
217+
assertEquals("myValue2", ContextMap["myKey2"])
218+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
219+
}
220+
withAdditionalLoggingContext(mapOf("myKey3" to "myValue3"), listOf("test3")) {
221+
assertEquals("myValue", ContextMap["myKey"])
222+
assertEquals("myValue2", ContextMap["myKey2"])
223+
assertEquals("myValue3", ContextMap["myKey3"])
224+
assertEquals(listOf("test", "test2", "test3"), ContextStack.view.asList())
225+
}
226+
assertEquals("myValue", ContextMap["myKey"])
227+
assertEquals("myValue2", ContextMap["myKey2"])
228+
assertEquals(null, ContextMap["myKey3"])
229+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
230+
}
231+
withLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
232+
assertEquals(null, ContextMap["myKey"])
233+
assertEquals("myValue2", ContextMap["myKey2"])
234+
assertEquals(listOf("test2"), ContextStack.view.asList())
235+
withContext(mainDispatcher) {
236+
assertEquals(null, ContextMap["myKey"])
237+
assertEquals("myValue2", ContextMap["myKey2"])
238+
assertEquals(listOf("test2"), ContextStack.view.asList())
239+
}
240+
withLoggingContext(mapOf("myKey3" to "myValue3"), listOf("test3")) {
241+
assertEquals(null, ContextMap["myKey"])
242+
assertEquals(null, ContextMap["myKey2"])
243+
assertEquals(listOf("test3"), ContextStack.view.asList())
244+
}
245+
assertEquals(null, ContextMap["myKey"])
246+
assertEquals("myValue2", ContextMap["myKey2"])
247+
assertEquals(null, ContextMap["myKey3"])
248+
assertEquals(listOf("test2"), ContextStack.view.asList())
249+
}
130250
}
131251

132252
@Test
@@ -136,6 +256,80 @@ class ThreadContextTest {
136256
withContext(CoroutineThreadContext(ThreadContextData(mapOf("myKey" to "myValue"), listOf("test")))) {
137257
assertEquals("myValue", ContextMap["myKey"])
138258
assertEquals("test", ContextStack.peek())
259+
withContext(CoroutineThreadContext(ThreadContextData(mapOf("myKey2" to "myValue2"), listOf("test2")))) {
260+
assertEquals(null, ContextMap["myKey"])
261+
assertEquals("myValue2", ContextMap["myKey2"])
262+
assertEquals(listOf("test2"), ContextStack.view.asList())
263+
}
264+
assertEquals("myValue", ContextMap["myKey"])
265+
assertEquals("test", ContextStack.peek())
266+
}
267+
assertTrue(ContextMap.empty)
268+
assertTrue(ContextStack.empty)
269+
270+
withContext(loggingContext(mapOf("myKey" to "myValue"), listOf("test"))) {
271+
assertEquals("myValue", ContextMap["myKey"])
272+
assertEquals("test", ContextStack.peek())
273+
withContext(loggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
274+
assertEquals(null, ContextMap["myKey"])
275+
assertEquals("myValue2", ContextMap["myKey2"])
276+
assertEquals(listOf("test2"), ContextStack.view.asList())
277+
}
278+
assertEquals("myValue", ContextMap["myKey"])
279+
assertEquals("test", ContextStack.peek())
280+
}
281+
assertTrue(ContextMap.empty)
282+
assertTrue(ContextStack.empty)
283+
284+
withLoggingContext(mapOf("myKey" to "myValue"), listOf("test")) {
285+
assertEquals("myValue", ContextMap["myKey"])
286+
assertEquals("test", ContextStack.peek())
287+
withLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
288+
assertEquals(null, ContextMap["myKey"])
289+
assertEquals("myValue2", ContextMap["myKey2"])
290+
assertEquals(listOf("test2"), ContextStack.view.asList())
291+
}
292+
assertEquals("myValue", ContextMap["myKey"])
293+
assertEquals("test", ContextStack.peek())
294+
}
295+
assertTrue(ContextMap.empty)
296+
assertTrue(ContextStack.empty)
297+
298+
withAdditionalLoggingContext(mapOf("myKey" to "myValue"), listOf("test")) {
299+
assertEquals("myValue", ContextMap["myKey"])
300+
assertEquals("test", ContextStack.peek())
301+
withAdditionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
302+
assertEquals("myValue", ContextMap["myKey"])
303+
assertEquals("myValue2", ContextMap["myKey2"])
304+
assertEquals(listOf("test", "test2"), ContextStack.view.asList())
305+
}
306+
assertEquals("myValue", ContextMap["myKey"])
307+
assertEquals(null, ContextMap["myKey2"])
308+
assertEquals("test", ContextStack.peek())
309+
}
310+
assertTrue(ContextMap.empty)
311+
assertTrue(ContextStack.empty)
312+
}
313+
314+
@Test
315+
fun `Can override existing context, and restore it`() = runBlocking {
316+
assertTrue(ContextMap.empty)
317+
assertTrue(ContextStack.empty)
318+
withLoggingContext(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), listOf("test1", "test2")) {
319+
assertEquals(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), ContextMap.view)
320+
assertEquals(listOf("test1", "test2"), ContextStack.view.asList())
321+
withLoggingContext(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), listOf("test3", "test4")) {
322+
assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), ContextMap.view)
323+
assertEquals(listOf("test3", "test4"), ContextStack.view.asList())
324+
withAdditionalLoggingContext(mapOf("myKey4" to "myValue4Modified", "myKey5" to "myValue5"), listOf("test5")) {
325+
assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4Modified", "myKey5" to "myValue5"), ContextMap.view)
326+
assertEquals(listOf("test3", "test4", "test5"), ContextStack.view.asList())
327+
}
328+
assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), ContextMap.view)
329+
assertEquals(listOf("test3", "test4"), ContextStack.view.asList())
330+
}
331+
assertEquals(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), ContextMap.view)
332+
assertEquals(listOf("test1", "test2"), ContextStack.view.asList())
139333
}
140334
assertTrue(ContextMap.empty)
141335
assertTrue(ContextStack.empty)

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
<properties>
126126

127127
<!-- project version -->
128-
<revision>1.4.1-SNAPSHOT</revision>
128+
<revision>1.5.0-SNAPSHOT</revision>
129129

130130
<!-- `project.build.outputTimestamp` is required to be present for reproducible builds.
131131
We actually inherit one from the `org.apache:apache` through our parent `org.apache.logging:logging-parent`.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one or more
4+
~ contributor license agreements. See the NOTICE file distributed with
5+
~ this work for additional information regarding copyright ownership.
6+
~ The ASF licenses this file to you under the Apache License, Version 2.0
7+
~ (the "License"); you may not use this file except in compliance with
8+
~ the License. You may obtain a copy of the License at
9+
~
10+
~ http://www.apache.org/licenses/LICENSE-2.0
11+
~
12+
~ Unless required by applicable law or agreed to in writing, software
13+
~ distributed under the License is distributed on an "AS IS" BASIS,
14+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
~ See the License for the specific language governing permissions and
16+
~ limitations under the License.
17+
-->
18+
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
19+
xmlns="http://logging.apache.org/log4j/changelog"
20+
xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.2.xsd"
21+
type="added">
22+
<description format="asciidoc">Add convenience functions for managing logging context in coroutines</description>
23+
</entry>

0 commit comments

Comments
 (0)