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