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
diff --git a/log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt b/log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt
index c266702..b9376f1 100644
--- a/log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt
+++ b/log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt
@@ -17,7 +17,9 @@
package org.apache.logging.log4j.kotlin
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ThreadContextElement
+import kotlinx.coroutines.withContext
import org.apache.logging.log4j.ThreadContext
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
@@ -34,7 +36,12 @@
data class ThreadContextData(
val map: Map<String, String>? = ContextMap.view,
val stack: Collection<String>? = ContextStack.view
-)
+) {
+ operator fun plus(data: ThreadContextData) = ThreadContextData(
+ map = this.map.orEmpty() + data.map.orEmpty(),
+ stack = this.stack.orEmpty() + data.stack.orEmpty(),
+ )
+}
/**
* Log4j2 [ThreadContext] element for [CoroutineContext].
@@ -59,6 +66,9 @@
* Use `withContext(CoroutineThreadContext()) { ... }` to capture updated map of Thread keys and values
* for the specified block of code.
*
+ * See [loggingContext] and [additionalLoggingContext] for convenience functions that make working with a
+ * [CoroutineThreadContext] simpler.
+ *
* @param contextData the value of [Thread] context map and context stack.
* Default value is the copy of the current thread's context map that is acquired via
* [ContextMap.view] and [ContextStack.view].
@@ -95,3 +105,73 @@
contextData.stack?.let { ContextStack.set(it) }
}
}
+
+/**
+ * Convenience function to obtain a [CoroutineThreadContext] with the given map and stack, which default
+ * to no context. Any existing logging context in scope is ignored.
+ *
+ * Example:
+ *
+ * ```
+ * launch(loggingContext(mapOf("kotlin" to "rocks"))) {
+ * logger.info { "..." } // The Thread context contains the mapping here
+ * }
+ * ```
+ */
+fun loggingContext(
+ map: Map<String, String>? = null,
+ stack: Collection<String>? = null,
+): CoroutineThreadContext = CoroutineThreadContext(ThreadContextData(map = map, stack = stack))
+
+/**
+ * Convenience function to obtain a [CoroutineThreadContext] that inherits the current context (if any), plus adds
+ * the context from the given map and stack, which default to nothing.
+ *
+ * Example:
+ *
+ * ```
+ * launch(additionalLoggingContext(mapOf("kotlin" to "rocks"))) {
+ * logger.info { "..." } // The Thread context contains the mapping plus whatever context was in scope at launch
+ * }
+ * ```
+ */
+fun additionalLoggingContext(
+ map: Map<String, String>? = null,
+ stack: Collection<String>? = null,
+): CoroutineThreadContext = CoroutineThreadContext(ThreadContextData() + ThreadContextData(map = map, stack = stack))
+
+/**
+ * Run the given block with the provided logging context, which default to no context. Any existing logging context
+ * in scope is ignored.
+ *
+ * Example:
+ *
+ * ```
+ * withLoggingContext(mapOf("kotlin" to "rocks")) {
+ * logger.info { "..." } // The Thread context contains the mapping
+ * }
+ * ```
+ */
+suspend fun <R> withLoggingContext(
+ map: Map<String, String>? = null,
+ stack: Collection<String>? = null,
+ block: suspend CoroutineScope.() -> R,
+): R = withContext(loggingContext(map, stack), block)
+
+/**
+ * Run the given block with the provided additional logging context. The given context is added to any existing
+ * logging context in scope.
+ *
+ * Example:
+ *
+ * ```
+ * withAdditionalLoggingContext(mapOf("kotlin" to "rocks")) {
+ * logger.info { "..." } // The Thread context contains the mapping plus whatever context was in the scope previously
+ * }
+ * ```
+ */
+suspend fun <R> withAdditionalLoggingContext(
+ map: Map<String, String>? = null,
+ stack: Collection<String>? = null,
+ block: suspend CoroutineScope.() -> R,
+): R = withContext(additionalLoggingContext(map, stack), block)
diff --git a/log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt b/log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt
index 64f71ab..1b1d5fa 100644
--- a/log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt
+++ b/log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt
@@ -53,6 +53,10 @@
assertNull(ContextMap["myKey"])
assertTrue(ContextStack.empty)
}.join()
+ GlobalScope.launch(loggingContext()) {
+ assertNull(ContextMap["myKey"])
+ assertTrue(ContextStack.empty)
+ }.join()
}
@DelicateCoroutinesApi
@@ -65,6 +69,10 @@
assertEquals("myValue", ContextMap["myKey"])
assertEquals("test", ContextStack.peek())
}.join()
+ GlobalScope.launch(additionalLoggingContext()) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }.join()
}
@Test
@@ -75,11 +83,37 @@
withContext(CoroutineThreadContext()) {
ContextMap["myKey"] = "myValue2"
ContextStack.push("test2")
- // Scoped launch with inherited MDContext element
+ // Scoped launch with non-inherited MDContext element
launch(Dispatchers.Default) {
assertEquals("myValue", ContextMap["myKey"])
assertEquals("test", ContextStack.peek())
}
+ // Scoped launch with non-inherited MDContext element
+ launch(Dispatchers.Default + loggingContext()) {
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+ }
+ // Scoped launch with non-inherited MDContext element
+ launch(Dispatchers.Default + loggingContext(mapOf("myKey2" to "myValue2"), listOf("test3"))) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test3"), ContextStack.view.asList())
+ }
+ // Scoped launch with inherited MDContext element
+ launch(Dispatchers.Default + CoroutineThreadContext()) {
+ assertEquals("myValue2", ContextMap["myKey"])
+ assertEquals("test2", ContextStack.peek())
+ }
+ // Scoped launch with inherited plus additional empty MDContext element
+ launch(Dispatchers.Default + additionalLoggingContext()) {
+ assertEquals("myValue2", ContextMap["myKey"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ }
+ launch(Dispatchers.Default + additionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test3"))) {
+ assertEquals("myValue2", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2", "test3"), ContextStack.view.asList())
+ }
}
assertEquals("myValue", ContextMap["myKey"])
assertEquals("test", ContextStack.peek())
@@ -104,6 +138,10 @@
assertEquals("myValue", ContextMap["myKey"])
assertEquals("test", ContextStack.peek())
}
+ runBlocking(additionalLoggingContext()) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }
}
@Test
@@ -112,10 +150,18 @@
assertTrue(ContextMap.empty)
assertTrue(ContextStack.empty)
}
+ runBlocking(loggingContext()) {
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+ }
+ runBlocking(additionalLoggingContext()) {
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+ }
}
@Test
- fun `Context with context`() = runBlocking {
+ fun `Context using withContext`() = runBlocking {
ContextMap["myKey"] = "myValue"
ContextStack.push("test")
val mainDispatcher = coroutineContext[ContinuationInterceptor]!!
@@ -127,6 +173,80 @@
assertEquals("test", ContextStack.peek())
}
}
+ withContext(Dispatchers.Default + additionalLoggingContext()) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ withContext(mainDispatcher) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }
+ }
+ withContext(Dispatchers.Default + additionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ withContext(mainDispatcher) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ }
+ }
+ withContext(Dispatchers.Default + loggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ withContext(mainDispatcher) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
+ }
+ }
+
+ @Test
+ fun `Context using withLoggingContext`() = runBlocking {
+ ContextMap["myKey"] = "myValue"
+ ContextStack.push("test")
+ val mainDispatcher = coroutineContext[ContinuationInterceptor]!!
+ withAdditionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ withContext(mainDispatcher) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ }
+ withAdditionalLoggingContext(mapOf("myKey3" to "myValue3"), listOf("test3")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals("myValue3", ContextMap["myKey3"])
+ assertEquals(listOf("test", "test2", "test3"), ContextStack.view.asList())
+ }
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(null, ContextMap["myKey3"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ }
+ withLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ withContext(mainDispatcher) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
+ withLoggingContext(mapOf("myKey3" to "myValue3"), listOf("test3")) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals(null, ContextMap["myKey2"])
+ assertEquals(listOf("test3"), ContextStack.view.asList())
+ }
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(null, ContextMap["myKey3"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
}
@Test
@@ -136,6 +256,80 @@
withContext(CoroutineThreadContext(ThreadContextData(mapOf("myKey" to "myValue"), listOf("test")))) {
assertEquals("myValue", ContextMap["myKey"])
assertEquals("test", ContextStack.peek())
+ withContext(CoroutineThreadContext(ThreadContextData(mapOf("myKey2" to "myValue2"), listOf("test2")))) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+
+ withContext(loggingContext(mapOf("myKey" to "myValue"), listOf("test"))) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ withContext(loggingContext(mapOf("myKey2" to "myValue2"), listOf("test2"))) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+
+ withLoggingContext(mapOf("myKey" to "myValue"), listOf("test")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ withLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
+ assertEquals(null, ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test2"), ContextStack.view.asList())
+ }
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ }
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+
+ withAdditionalLoggingContext(mapOf("myKey" to "myValue"), listOf("test")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+ withAdditionalLoggingContext(mapOf("myKey2" to "myValue2"), listOf("test2")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("myValue2", ContextMap["myKey2"])
+ assertEquals(listOf("test", "test2"), ContextStack.view.asList())
+ }
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals(null, ContextMap["myKey2"])
+ assertEquals("test", ContextStack.peek())
+ }
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+ }
+
+ @Test
+ fun `Can override existing context, and restore it`() = runBlocking {
+ assertTrue(ContextMap.empty)
+ assertTrue(ContextStack.empty)
+ withLoggingContext(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), listOf("test1", "test2")) {
+ assertEquals(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), ContextMap.view)
+ assertEquals(listOf("test1", "test2"), ContextStack.view.asList())
+ withLoggingContext(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), listOf("test3", "test4")) {
+ assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), ContextMap.view)
+ assertEquals(listOf("test3", "test4"), ContextStack.view.asList())
+ withAdditionalLoggingContext(mapOf("myKey4" to "myValue4Modified", "myKey5" to "myValue5"), listOf("test5")) {
+ assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4Modified", "myKey5" to "myValue5"), ContextMap.view)
+ assertEquals(listOf("test3", "test4", "test5"), ContextStack.view.asList())
+ }
+ assertEquals(mapOf("myKey3" to "myValue3", "myKey4" to "myValue4"), ContextMap.view)
+ assertEquals(listOf("test3", "test4"), ContextStack.view.asList())
+ }
+ assertEquals(mapOf("myKey" to "myValue", "myKey2" to "myValue2"), ContextMap.view)
+ assertEquals(listOf("test1", "test2"), ContextStack.view.asList())
}
assertTrue(ContextMap.empty)
assertTrue(ContextStack.empty)
diff --git a/pom.xml b/pom.xml
index ec22e41..54af239 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,7 +125,7 @@
<properties>
<!-- project version -->
- <revision>1.4.1-SNAPSHOT</revision>
+ <revision>1.5.0-SNAPSHOT</revision>
<!-- `project.build.outputTimestamp` is required to be present for reproducible builds.
We actually inherit one from the `org.apache:apache` through our parent `org.apache.logging:logging-parent`.
diff --git a/src/changelog/.1.x.x/add-coroutine-context-convenience-functions.xml b/src/changelog/.1.x.x/add-coroutine-context-convenience-functions.xml
new file mode 100644
index 0000000..772c8be
--- /dev/null
+++ b/src/changelog/.1.x.x/add-coroutine-context-convenience-functions.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to you under the Apache License, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://logging.apache.org/log4j/changelog"
+ xsi:schemaLocation="http://logging.apache.org/log4j/changelog https://logging.apache.org/log4j/changelog-0.1.2.xsd"
+ type="added">
+ <description format="asciidoc">Add convenience functions for managing logging context in coroutines</description>
+</entry>
diff --git a/src/site/index.adoc b/src/site/index.adoc
index 5e7152a..addaec5 100644
--- a/src/site/index.adoc
+++ b/src/site/index.adoc
@@ -138,6 +138,43 @@
assert(ContextStack.empty)
----
+A `CoroutineThreadContext` context element is provided to integrate logging context with coroutines.
+
+We provide convenience functions `loggingContext` and `additionalLoggingContext` to create instances of `CoroutineThreadContext` with the appropriate context data.
+The result of these functions can be passed directly to coroutine builders to set the context for the coroutine.
+
+To set the context, ignoring any context currently in scope:
+
+[source,kotlin]
+----
+launch(loggingContext(mapOf("myKey" to "myValue"), listOf("test"))) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+}
+----
+
+Or to preserve the existing context and add additional logging context:
+
+[source,kotlin]
+----
+launch(additionalLoggingContext(mapOf("myKey" to "myValue"), listOf("test"))) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+}
+----
+
+Alternatively, to change the context without launching a new coroutine, the `withLoggingContext` and `withAdditionalLoggingContext` functions are provided:
+
+[source,kotlin]
+----
+withAdditionalLoggingContext(mapOf("myKey" to "myValue"), listOf("test")) {
+ assertEquals("myValue", ContextMap["myKey"])
+ assertEquals("test", ContextStack.peek())
+}
+----
+
+These functions are shorthand for `withContext(loggingContext(...))` or `withContext(additionalLoggingContext(...))`.
+
[#params]
== Parameter substitution