Implementation of a SLF4J MDC Context
See discussion in PR #403 and issue #119
diff --git a/binary-compatibility-validator/build.gradle b/binary-compatibility-validator/build.gradle
index d650898..67d393f 100644
--- a/binary-compatibility-validator/build.gradle
+++ b/binary-compatibility-validator/build.gradle
@@ -11,7 +11,7 @@
compile 'com.google.code.gson:gson:2.6.2'
testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
-
+
testArtifacts project(':kotlinx-coroutines-core')
testArtifacts project(':kotlinx-coroutines-reactive')
@@ -23,6 +23,7 @@
testArtifacts project(':kotlinx-coroutines-jdk8')
testArtifacts project(':kotlinx-coroutines-nio')
testArtifacts project(':kotlinx-coroutines-quasar')
+ testArtifacts project(':kotlinx-coroutines-slf4j')
testArtifacts project(':kotlinx-coroutines-android')
testArtifacts project(':kotlinx-coroutines-javafx')
diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-slf4j.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-slf4j.txt
new file mode 100644
index 0000000..d8587b3
--- /dev/null
+++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-slf4j.txt
@@ -0,0 +1,19 @@
+public final class kotlinx/coroutines/experimental/slf4j/MDCContext : kotlin/coroutines/experimental/AbstractCoroutineContextElement, kotlinx/coroutines/experimental/ThreadContextElement {
+ public static final field Key Lkotlinx/coroutines/experimental/slf4j/MDCContext$Key;
+ public fun <init> ()V
+ public fun <init> (Ljava/util/Map;)V
+ public synthetic fun <init> (Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
+ public fun get (Lkotlin/coroutines/experimental/CoroutineContext$Key;)Lkotlin/coroutines/experimental/CoroutineContext$Element;
+ public final fun getContextMap ()Ljava/util/Map;
+ public fun minusKey (Lkotlin/coroutines/experimental/CoroutineContext$Key;)Lkotlin/coroutines/experimental/CoroutineContext;
+ public fun plus (Lkotlin/coroutines/experimental/CoroutineContext;)Lkotlin/coroutines/experimental/CoroutineContext;
+ public synthetic fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/lang/Object;)V
+ public fun restoreThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;Ljava/util/Map;)V
+ public synthetic fun updateThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;)Ljava/lang/Object;
+ public fun updateThreadContext (Lkotlin/coroutines/experimental/CoroutineContext;)Ljava/util/Map;
+}
+
+public final class kotlinx/coroutines/experimental/slf4j/MDCContext$Key : kotlin/coroutines/experimental/CoroutineContext$Key {
+}
+
diff --git a/integration/README.md b/integration/README.md
index 83b0a4b..099a17e 100644
--- a/integration/README.md
+++ b/integration/README.md
@@ -9,6 +9,7 @@
* [kotlinx-coroutines-nio](kotlinx-coroutines-nio/README.md) -- integration with asynchronous IO on JDK7+ (Android O Preview).
* [kotlinx-coroutines-guava](kotlinx-coroutines-guava/README.md) -- integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained).
* [kotlinx-coroutines-quasar](kotlinx-coroutines-quasar/README.md) -- integration with [Quasar](http://docs.paralleluniverse.co/quasar/).
+* [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j/README.md) -- integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
## Contributing
diff --git a/integration/kotlinx-coroutines-slf4j/README.md b/integration/kotlinx-coroutines-slf4j/README.md
new file mode 100644
index 0000000..265a9fc
--- /dev/null
+++ b/integration/kotlinx-coroutines-slf4j/README.md
@@ -0,0 +1,24 @@
+# Module kotlinx-coroutines-slf4j
+
+Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
+
+## Example
+
+Add [MDCContext] to the coroutine context so that the SLF4J MDC context is captured and passed into the coroutine.
+
+```kotlin
+MDC.put("kotlin", "rocks") // put a value into the MDC context
+
+launch(MDCContext()) {
+ logger.info { "..." } // the MDC context will contain the mapping here
+}
+```
+
+# Package kotlinx.coroutines.experimental.slf4j
+
+Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html).
+
+<!--- MODULE kotlinx-coroutines-slf4j -->
+<!--- INDEX kotlinx.coroutines.experimental.slf4j -->
+[MDCContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-slf4j/kotlinx.coroutines.experimental.slf4j/-m-d-c-context/index.html
+<!--- END -->
diff --git a/integration/kotlinx-coroutines-slf4j/build.gradle b/integration/kotlinx-coroutines-slf4j/build.gradle
new file mode 100644
index 0000000..e2d3a34
--- /dev/null
+++ b/integration/kotlinx-coroutines-slf4j/build.gradle
@@ -0,0 +1,12 @@
+dependencies {
+ compile 'org.slf4j:slf4j-api:1.7.25'
+ testCompile 'io.github.microutils:kotlin-logging:1.5.4'
+ testRuntime 'ch.qos.logback:logback-classic:1.2.3'
+ testRuntime 'ch.qos.logback:logback-core:1.2.3'
+}
+
+tasks.withType(dokka.getClass()) {
+ externalDocumentationLink {
+ url = new URL("https://www.slf4j.org/apidocs/")
+ }
+}
\ No newline at end of file
diff --git a/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
new file mode 100644
index 0000000..6976b36
--- /dev/null
+++ b/integration/kotlinx-coroutines-slf4j/src/MDCContext.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.experimental.slf4j
+
+import kotlinx.coroutines.experimental.*
+import org.slf4j.MDC
+import kotlin.coroutines.experimental.AbstractCoroutineContextElement
+import kotlin.coroutines.experimental.CoroutineContext
+
+/**
+ * The value of [MDC] context map.
+ * See [MDC.getCopyOfContextMap].
+ */
+public typealias MDCContextMap = Map<String, String>?
+
+/**
+ * [MDC] context element for [CoroutineContext].
+ *
+ * Example:
+ *
+ * ```
+ * MDC.put("kotlin", "rocks") // Put a value into the MDC context
+ *
+ * launch(MDCContext()) {
+ * logger.info { "..." } // The MDC context contains the mapping here
+ * }
+ * ```
+ *
+ * Note, that you cannot update MDC context from inside of the coroutine simply
+ * using [MDC.put]. These updates are going to be lost on the next suspension and
+ * reinstalled to the MDC context that was captured or explicitly specified in
+ * [contextMap] when this object was created on the next resumption.
+ * Use `withContext(MDCContext()) { ... }` to capture updated map of MDC keys and values
+ * for the specified block of code.
+ *
+ * @param contextMap the value of [MDC] context map.
+ * Default value is the copy of the current thread's context map that is acquired via
+ * [MDC.getCopyOfContextMap].
+ */
+public class MDCContext(
+ /**
+ * The value of [MDC] context map.
+ */
+ public val contextMap: MDCContextMap = MDC.getCopyOfContextMap()
+) : ThreadContextElement<MDCContextMap>, AbstractCoroutineContextElement(Key) {
+ /**
+ * Key of [MDCContext] in [CoroutineContext].
+ */
+ companion object Key : CoroutineContext.Key<MDCContext>
+
+ override fun updateThreadContext(context: CoroutineContext): MDCContextMap {
+ val oldState = MDC.getCopyOfContextMap()
+ setCurrent(contextMap)
+ return oldState
+ }
+
+ override fun restoreThreadContext(context: CoroutineContext, oldState: MDCContextMap) {
+ setCurrent(oldState)
+ }
+
+ private fun setCurrent(contextMap: MDCContextMap) {
+ if (contextMap == null) {
+ MDC.clear()
+ } else {
+ MDC.setContextMap(contextMap)
+ }
+ }
+}
diff --git a/integration/kotlinx-coroutines-slf4j/test-resources/logback-test.xml b/integration/kotlinx-coroutines-slf4j/test-resources/logback-test.xml
new file mode 100644
index 0000000..8051011
--- /dev/null
+++ b/integration/kotlinx-coroutines-slf4j/test-resources/logback-test.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration debug="false">
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <layout>
+ <Pattern>%X{first} %X{last} - %m%n</Pattern>
+ </layout>
+ </appender>
+
+ <root level="DEBUG">
+ <appender-ref ref="CONSOLE" />
+ </root>
+</configuration>
+
+
diff --git a/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
new file mode 100644
index 0000000..9838871
--- /dev/null
+++ b/integration/kotlinx-coroutines-slf4j/test/MDCContextTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.coroutines.experimental.slf4j
+
+import kotlinx.coroutines.experimental.*
+import org.junit.*
+import org.junit.Test
+import org.slf4j.*
+import kotlin.coroutines.experimental.*
+import kotlin.test.*
+
+class MDCContextTest : TestBase() {
+ @Before
+ fun setUp() {
+ MDC.clear()
+ }
+
+ @After
+ fun tearDown() {
+ MDC.clear()
+ }
+
+ @Test
+ fun testContextIsNotPassedByDefaultBetweenCoroutines() = runTest {
+ expect(1)
+ MDC.put("myKey", "myValue")
+ launch {
+ assertEquals(null, MDC.get("myKey"))
+ expect(2)
+ }.join()
+ finish(3)
+ }
+
+ @Test
+ fun testContextCanBePassedBetweenCoroutines() = runTest {
+ expect(1)
+ MDC.put("myKey", "myValue")
+ launch(MDCContext()) {
+ assertEquals("myValue", MDC.get("myKey"))
+ expect(2)
+ }.join()
+
+ finish(3)
+ }
+
+ @Test
+ fun testContextPassedWhileOnMainThread() {
+ MDC.put("myKey", "myValue")
+ // No MDCContext element
+ runBlocking {
+ assertEquals("myValue", MDC.get("myKey"))
+ }
+ }
+
+ @Test
+ fun testContextCanBePassedWhileOnMainThread() {
+ MDC.put("myKey", "myValue")
+ runBlocking(MDCContext()) {
+ assertEquals("myValue", MDC.get("myKey"))
+ }
+ }
+
+ @Test
+ fun testContextNeededWithOtherContext() {
+ MDC.put("myKey", "myValue")
+ runBlocking(MDCContext()) {
+ assertEquals("myValue", MDC.get("myKey"))
+ }
+ }
+
+ @Test
+ fun testContextMayBeEmpty() {
+ runBlocking(MDCContext()) {
+ assertEquals(null, MDC.get("myKey"))
+ }
+ }
+
+ @Test
+ fun testContextWithContext() = runTest {
+ MDC.put("myKey", "myValue")
+ val mainDispatcher = kotlin.coroutines.experimental.coroutineContext[ContinuationInterceptor]!!
+ withContext(DefaultDispatcher + MDCContext()) {
+ assertEquals("myValue", MDC.get("myKey"))
+ withContext(mainDispatcher) {
+ assertEquals("myValue", MDC.get("myKey"))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index d27a455..25d1cd7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -28,6 +28,7 @@
module('integration/kotlinx-coroutines-jdk8')
module('integration/kotlinx-coroutines-nio')
module('integration/kotlinx-coroutines-quasar')
+module('integration/kotlinx-coroutines-slf4j')
module('reactive/kotlinx-coroutines-reactive')
module('reactive/kotlinx-coroutines-reactor')
diff --git a/site/docs/index.md b/site/docs/index.md
index e396687..1941573 100644
--- a/site/docs/index.md
+++ b/site/docs/index.md
@@ -24,6 +24,7 @@
| [kotlinx-coroutines-nio](kotlinx-coroutines-nio) | Integration with asynchronous IO on JDK7+ (Android O Preview) |
| [kotlinx-coroutines-guava](kotlinx-coroutines-guava) | Integration with Guava [ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained) |
| [kotlinx-coroutines-quasar](kotlinx-coroutines-quasar) | Integration with [Quasar](http://docs.paralleluniverse.co/quasar/) |
+| [kotlinx-coroutines-slf4j](kotlinx-coroutines-slf4j) | Integration with SLF4J [MDC](https://logback.qos.ch/manual/mdc.html) |
## Examples