Browse Source
Prior to this commit, the Micrometer context-propagation project would
help propagating information from `ThreadLocal`, Reactor `Context` and
other context objects. This is already well supported for Micrometer
Observations.
In the case of Kotlin suspending functions, the processing of tasks
would not necessarily update the `ThreadLocal` when the function is
scheduled on a different thread.
This commit introduces the `PropagationContextElement` operator that
connects the `ThreadLocal`, Reactor `Context` and Coroutine `Context`
for all libraries using the "context-propagation" project.
Applications must manually use this operator in suspending functions
like so:
```
suspend fun suspendingFunction() {
return withContext(PropagationContextElement(currentCoroutineContext())) {
logger.info("Suspending function with traceId")
}
}
```
Closes gh-35185
pull/35428/head
6 changed files with 246 additions and 0 deletions
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.docs.languages.kotlin.coroutines.propagation |
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext |
||||
import kotlinx.coroutines.withContext |
||||
import org.apache.commons.logging.Log |
||||
import org.apache.commons.logging.LogFactory |
||||
import org.springframework.core.PropagationContextElement |
||||
|
||||
class ContextPropagationSample { |
||||
|
||||
companion object { |
||||
private val logger: Log = LogFactory.getLog( |
||||
ContextPropagationSample::class.java |
||||
) |
||||
} |
||||
|
||||
// tag::context[] |
||||
suspend fun suspendingFunction() { |
||||
return withContext(PropagationContextElement(currentCoroutineContext())) { |
||||
logger.info("Suspending function with traceId") |
||||
} |
||||
} |
||||
// end::context[] |
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.core |
||||
|
||||
import io.micrometer.context.ContextRegistry |
||||
import io.micrometer.context.ContextSnapshot |
||||
import io.micrometer.context.ContextSnapshotFactory |
||||
import kotlinx.coroutines.ThreadContextElement |
||||
import kotlinx.coroutines.reactor.ReactorContext |
||||
import reactor.util.context.ContextView |
||||
import kotlin.coroutines.AbstractCoroutineContextElement |
||||
import kotlin.coroutines.CoroutineContext |
||||
|
||||
|
||||
/** |
||||
* [ThreadContextElement] that restores `ThreadLocals` from the Reactor [ContextSnapshot] |
||||
* every time the coroutine with this element in the context is resumed on a thread. |
||||
* |
||||
* This effectively ensures that Kotlin Coroutines, Reactor and Micrometer Context Propagation |
||||
* work together in an application, typically for observability purposes. |
||||
* |
||||
* Applications need to have both `"io.micrometer:context-propagation"` and |
||||
* `"org.jetbrains.kotlinx:kotlinx-coroutines-reactor"` on the classpath to use this context element. |
||||
* |
||||
* The `PropagationContextElement` can be used like this: |
||||
* |
||||
* ```kotlin |
||||
* suspend fun suspendable() { |
||||
* withContext(PropagationContextElement(coroutineContext)) { |
||||
* logger.info("Log statement with traceId") |
||||
* } |
||||
* } |
||||
* ``` |
||||
* |
||||
* @author Brian Clozel |
||||
* @since 7.0 |
||||
*/ |
||||
class PropagationContextElement(private val context: CoroutineContext) : ThreadContextElement<ContextSnapshot.Scope>, |
||||
AbstractCoroutineContextElement(Key) { |
||||
|
||||
companion object Key : CoroutineContext.Key<PropagationContextElement> |
||||
|
||||
val contextSnapshot: ContextSnapshot |
||||
get() { |
||||
val contextView: ContextView? = context[ReactorContext]?.context |
||||
val contextSnapshotFactory = |
||||
ContextSnapshotFactory.builder().contextRegistry(ContextRegistry.getInstance()).build() |
||||
if (contextView != null) { |
||||
return contextSnapshotFactory.captureFrom(contextView) |
||||
} |
||||
return contextSnapshotFactory.captureAll() |
||||
} |
||||
|
||||
override fun restoreThreadContext(context: CoroutineContext, oldState: ContextSnapshot.Scope) { |
||||
oldState.close() |
||||
} |
||||
|
||||
override fun updateThreadContext(context: CoroutineContext): ContextSnapshot.Scope { |
||||
return contextSnapshot.setThreadLocals() |
||||
} |
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed 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 |
||||
* |
||||
* https://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.springframework.core |
||||
|
||||
import io.micrometer.observation.Observation |
||||
import io.micrometer.observation.tck.TestObservationRegistry |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.currentCoroutineContext |
||||
import kotlinx.coroutines.runBlocking |
||||
import kotlinx.coroutines.withContext |
||||
import org.assertj.core.api.Assertions |
||||
import org.assertj.core.api.Assertions.assertThat |
||||
import org.junit.jupiter.api.AfterAll |
||||
import org.junit.jupiter.api.BeforeAll |
||||
import org.junit.jupiter.api.Test |
||||
import org.reactivestreams.Publisher |
||||
import reactor.core.publisher.Hooks |
||||
import reactor.core.publisher.Mono |
||||
import reactor.core.scheduler.Schedulers |
||||
import kotlin.coroutines.Continuation |
||||
|
||||
|
||||
/** |
||||
* Kotlin tests for [PropagationContextElement]. |
||||
* |
||||
* @author Brian Clozel |
||||
*/ |
||||
class PropagationContextElementTests { |
||||
|
||||
private val observationRegistry = TestObservationRegistry.create() |
||||
|
||||
companion object { |
||||
|
||||
@BeforeAll |
||||
@JvmStatic |
||||
fun init() { |
||||
Hooks.enableAutomaticContextPropagation() |
||||
} |
||||
|
||||
@AfterAll |
||||
@JvmStatic |
||||
fun cleanup() { |
||||
Hooks.disableAutomaticContextPropagation() |
||||
} |
||||
|
||||
} |
||||
|
||||
@Test |
||||
fun restoresFromThreadLocal() { |
||||
val observation = Observation.createNotStarted("coroutine", observationRegistry) |
||||
observation.observe { |
||||
val result = runBlocking(Dispatchers.Unconfined) { |
||||
suspendingFunction("test") |
||||
} |
||||
Assertions.assertThat(result).isEqualTo("coroutine") |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
@Suppress("UNCHECKED_CAST") |
||||
fun restoresFromReactorContext() { |
||||
val method = PropagationContextElementTests::class.java.getDeclaredMethod("suspendingFunction", String::class.java, Continuation::class.java) |
||||
val publisher = CoroutinesUtils.invokeSuspendingFunction(method, this, "test", null) as Publisher<String> |
||||
val observation = Observation.createNotStarted("coroutine", observationRegistry) |
||||
observation.observe { |
||||
val result = Mono.from<String>(publisher).publishOn(Schedulers.boundedElastic()).block() |
||||
assertThat(result).isEqualTo("coroutine") |
||||
} |
||||
} |
||||
|
||||
suspend fun suspendingFunction(value: String): String? { |
||||
return withContext(PropagationContextElement(currentCoroutineContext())) { |
||||
val currentObservation = observationRegistry.currentObservation |
||||
assertThat(currentObservation).isNotNull |
||||
currentObservation?.context?.name |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue