You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2020/05/20 21:44:41 UTC

[logging-log4j-kotlin] branch master updated: LOG4J2-2433: Add ThreadContext support with coroutines

This is an automated email from the ASF dual-hosted git repository.

rgupta pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j-kotlin.git


The following commit(s) were added to refs/heads/master by this push:
     new a22efc2  LOG4J2-2433: Add ThreadContext support with coroutines
     new a3c1d6b  Merge branch 'LOG4J2-2433_Coroutines-support'
a22efc2 is described below

commit a22efc2d8b8ad64397bca86eaf884227ba2af42d
Author: Raman Gupta <ro...@gmail.com>
AuthorDate: Fri Dec 7 10:03:55 2018 -0500

    LOG4J2-2433: Add ThreadContext support with coroutines
    
    Create a Kotlin coroutines CoroutineThreadContext, which can be used to
    integrate the thread-local based log4j2 ThreadContext with Kotlin
    coroutine context.
    
    This is based on the SLF4J MDC adapter maintained by Jetbrains @
    https://github.com/Kotlin/kotlinx.coroutines/blob/master/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt.
---
 log4j-api-kotlin/pom.xml                           |   5 +
 .../logging/log4j/kotlin/CoroutineThreadContext.kt |  95 +++++++++++++++++++
 .../ThreadContextTest.kt                           | 105 +++++++++++++++++++++
 pom.xml                                            |   1 +
 4 files changed, 206 insertions(+)

diff --git a/log4j-api-kotlin/pom.xml b/log4j-api-kotlin/pom.xml
index b777302..bf4f774 100644
--- a/log4j-api-kotlin/pom.xml
+++ b/log4j-api-kotlin/pom.xml
@@ -50,6 +50,11 @@
       <version>${kotlin.version}</version>
     </dependency>
     <dependency>
+      <groupId>org.jetbrains.kotlinx</groupId>
+      <artifactId>kotlinx-coroutines-jdk8</artifactId>
+      <version>${kotlinx.coroutines.version}</version>
+    </dependency>
+    <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <type>test-jar</type>
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
new file mode 100644
index 0000000..8be3cf0
--- /dev/null
+++ b/log4j-api-kotlin/src/main/kotlin/org/apache/logging/log4j/kotlin/CoroutineThreadContext.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.logging.log4j.kotlin
+
+import kotlinx.coroutines.ThreadContextElement
+import org.apache.logging.log4j.ThreadContext
+import kotlin.coroutines.AbstractCoroutineContextElement
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * The value of [ThreadContext] map and stack.
+ * See [ThreadContext.getImmutableContext] and [ThreadContext.getImmutableStack].
+ */
+data class ThreadContextData(
+  val map: Map<String, String>?,
+  val stack: Collection<String>?
+)
+
+/**
+ * Log4j2 [ThreadContext] element for [CoroutineContext].
+ *
+ * This is based on the SLF4J MDCContext maintained by Jetbrains:
+ * https://github.com/Kotlin/kotlinx.coroutines/blob/master/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
+ *
+ * Example:
+ *
+ * ```
+ * ThreadContext.put("kotlin", "rocks") // Put a value into the Thread context
+ *
+ * launch(CoroutineThreadContext()) {
+ *     logger.info { "..." }   // The Thread context contains the mapping here
+ * }
+ * ```
+ *
+ * Note, that you cannot update Thread context from inside of the coroutine simply
+ * using [ThreadContext.put]. These updates are going to be lost on the next suspension and
+ * reinstalled to the Thread context that was captured or explicitly specified in
+ * [contextMap] when this object was created on the next resumption.
+ * Use `withContext(ThreadContext()) { ... }` to capture updated map of Thread keys and values
+ * for the specified block of code.
+ *
+ * @param contextMap the value of [Thread] context map.
+ * Default value is the copy of the current thread's context map that is acquired via
+ * [ThreadContext.getContext].
+ */
+class CoroutineThreadContext(
+  /**
+   * The value of [Thread] context map.
+   */
+  val contextData: ThreadContextData = ThreadContextData(ThreadContext.getImmutableContext(), ThreadContext.getImmutableStack())
+) : ThreadContextElement<ThreadContextData>, AbstractCoroutineContextElement(Key) {
+  /**
+   * Key of [ThreadContext] in [CoroutineContext].
+   */
+  companion object Key : CoroutineContext.Key<CoroutineThreadContext>
+
+  /** @suppress */
+  override fun updateThreadContext(context: CoroutineContext): ThreadContextData {
+    val oldState = ThreadContextData(ThreadContext.getImmutableContext(), ThreadContext.getImmutableStack())
+    setCurrent(contextData)
+    return oldState
+  }
+
+  /** @suppress */
+  override fun restoreThreadContext(context: CoroutineContext, oldState: ThreadContextData) {
+    setCurrent(oldState)
+  }
+
+  private fun setCurrent(contextData: ThreadContextData) {
+    if (contextData.map == null) {
+      ThreadContext.clearMap()
+    } else {
+      ThreadContext.putAll(contextData.map)
+    }
+    if (contextData.stack == null) {
+      ThreadContext.clearStack()
+    } else {
+      ThreadContext.setStack(contextData.stack)
+    }
+  }
+}
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
new file mode 100644
index 0000000..e0ab4c4
--- /dev/null
+++ b/log4j-api-kotlin/src/test/kotlin/org.apache.logging.log4j.kotlin/ThreadContextTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+package org.apache.logging.log4j.kotlin
+
+import kotlinx.coroutines.*
+import org.apache.logging.log4j.ThreadContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.test.assertEquals
+
+class ThreadContextTest {
+  @Before
+  fun setUp() {
+    ThreadContext.clearAll()
+  }
+
+  @After
+  fun tearDown() {
+    ThreadContext.clearAll()
+  }
+
+  @Test
+  fun `Context is not passed by default between coroutines`() = runBlocking<Unit> {
+    ThreadContext.put("myKey", "myValue")
+    // Scoped launch with MDCContext element
+    GlobalScope.launch {
+      assertEquals(null, ThreadContext.get("myKey"))
+    }.join()
+  }
+
+  @Test
+  fun `Context can be passed between coroutines`() = runBlocking<Unit> {
+    ThreadContext.put("myKey", "myValue")
+    // Scoped launch with MDCContext element
+    GlobalScope.launch(CoroutineThreadContext()) {
+      assertEquals("myValue", ThreadContext.get("myKey"))
+    }.join()
+  }
+
+  @Test
+  fun `Context inheritance`() = runBlocking<Unit> {
+    ThreadContext.put("myKey", "myValue")
+    // Scoped launch with MDCContext element
+    withContext(CoroutineThreadContext()) {
+      ThreadContext.put("myKey", "myValue2")
+      // Scoped launch with inherited MDContext element
+      launch(Dispatchers.Default) {
+        assertEquals("myValue", ThreadContext.get("myKey"))
+      }
+    }
+    assertEquals("myValue", ThreadContext.get("myKey"))
+  }
+
+  @Test
+  fun `Context passed while on main thread`() {
+    ThreadContext.put("myKey", "myValue")
+    // No MDCContext element
+    runBlocking {
+      assertEquals("myValue", ThreadContext.get("myKey"))
+    }
+  }
+
+  @Test
+  fun `Context can be passed while on main thread`() {
+    ThreadContext.put("myKey", "myValue")
+    runBlocking(CoroutineThreadContext()) {
+      assertEquals("myValue", ThreadContext.get("myKey"))
+    }
+  }
+
+  @Test
+  fun `Context may be empty`() {
+    runBlocking(CoroutineThreadContext()) {
+      assertEquals(null, ThreadContext.get("myKey"))
+    }
+  }
+
+  @Test
+  fun `Context with context`() = runBlocking {
+    ThreadContext.put("myKey", "myValue")
+    val mainDispatcher = coroutineContext[ContinuationInterceptor]!!
+    withContext(Dispatchers.Default + CoroutineThreadContext()) {
+      assertEquals("myValue", ThreadContext.get("myKey"))
+      withContext(mainDispatcher) {
+        assertEquals("myValue", ThreadContext.get("myKey"))
+      }
+    }
+  }
+}
diff --git a/pom.xml b/pom.xml
index bad357c..4a32d8d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,7 @@
     <javadoc.plugin.version>2.10.3</javadoc.plugin.version>
     <jxr.plugin.version>2.5</jxr.plugin.version>
     <kotlin.version>1.3.72</kotlin.version>
+    <kotlinx.coroutines.version>1.3.6</kotlinx.coroutines.version>
     <log4j.version>2.13.2</log4j.version>
     <pmd.plugin.version>3.8</pmd.plugin.version>
     <rat.plugin.version>0.12</rat.plugin.version>