Adds support for Kotlin Coroutines (#1977)

* Added Kotlin Coroutine support
* Added docs, polished code and added one more case for reactor context
This commit is contained in:
Marcin Grzejszczak
2021-06-18 07:30:49 +02:00
committed by GitHub
parent 75b40bb884
commit 7c4d8b1479
16 changed files with 992 additions and 204 deletions

View File

@@ -1,28 +1,33 @@
root = true
[*]
end_of_line = crlf
insert_final_newline = true
[*.java]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.groovy]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.xml]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.yml]
indent_style = space
indent_size = 2
[*.yaml]
indent_style = space
indent_size = 2
root = true
[*]
end_of_line = crlf
insert_final_newline = true
[*.java]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.kt]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.groovy]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.xml]
indent_style = tab
indent_size = 4
continuation_indent_size = 8
[*.yml]
indent_style = space
indent_size = 2
[*.yaml]
indent_style = space
indent_size = 2

View File

@@ -67,7 +67,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<phase>generate-sources</phase>

View File

@@ -729,3 +729,10 @@ This feature is available for all tracer implementations.
We're instrumenting the `Session` repositories that wraps all operations in a span.
In order to disable this instrumentation set `spring.sleuth.session.enabled` to `false`.
[[sleuth-kotlin-integration]]
== Kotlin Coroutines
This feature is available for all tracer implementations.
We're adding Kotlin Coroutines that allow you to retrieve the current span via the `Tracer` bean. You can either pass the bean to the Kotlin Coroutine context via `Tracer.asContextElement()` method execution or if you have Reactor Kotlin Coroutine integration on the classpath, we will retrieve it from Reactor's context. To retrieve the current span you can call the `currentSpan()` method within the Kotlin Coroutine.

View File

@@ -88,6 +88,7 @@
<datasource-proxy.version>1.7</datasource-proxy.version>
<tomcat-jdbc.version>10.0.6</tomcat-jdbc.version>
<commons-dbcp2.version>2.8.0</commons-dbcp2.version>
<kotlin.version>1.5.10</kotlin.version>
<!-- Until we switch it to true in sc-build -->
<javadoc.failOnError>true</javadoc.failOnError>
@@ -98,6 +99,7 @@
<brave-propagation-aws.version>0.21.3</brave-propagation-aws.version>
<archunit-junit5.version>0.14.1</archunit-junit5.version>
<testcontainers.version>1.15.3</testcontainers.version>
<build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
</properties>
<build>

View File

@@ -1,40 +1,40 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import java.util.List;
import org.springframework.cloud.sleuth.exporter.FinishedSpan;
/**
* Writes finished spans in a provided format.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public interface FinishedSpanWriter<T> {
/**
* Writes the spans in a given format to String.
* @param format format in which spans should be stored
* @param spans spans to store
* @return string representation of spans or {@code null} if {@link TextOutputFormat}
* is not supported.
*/
T write(TextOutputFormat format, List<FinishedSpan> spans);
}
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import java.util.List;
import org.springframework.cloud.sleuth.exporter.FinishedSpan;
/**
* Writes finished spans in a provided format.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public interface FinishedSpanWriter<T> {
/**
* Writes the spans in a given format to String.
* @param format format in which spans should be stored
* @param spans spans to store
* @return string representation of spans or {@code null} if {@link TextOutputFormat}
* is not supported.
*/
T write(TextOutputFormat format, List<FinishedSpan> spans);
}

View File

@@ -1,87 +1,87 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import brave.handler.SpanHandler;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.endpoint.Producible;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.autoconfig.brave.ConditionalOnBraveEnabled;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} for Sleuth actuator endpoint.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true)
@ConditionalOnAvailableEndpoint(endpoint = TracesScrapeEndpoint.class)
@AutoConfigureBefore(BraveAutoConfiguration.class)
@EnableConfigurationProperties(SleuthActuatorProperties.class)
@ConditionalOnClass(Producible.class)
public class TraceSleuthActuatorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
BufferingSpanReporter sleuthBufferingSpanReporter(SleuthActuatorProperties sleuthActuatorProperties) {
return new BufferingSpanReporter(sleuthActuatorProperties.getCapacity());
}
@Bean
@ConditionalOnMissingBean
TracesScrapeEndpoint sleuthTracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter,
FinishedSpanWriter finishedSpanWriter) {
return new TracesScrapeEndpoint(bufferingSpanReporter, finishedSpanWriter);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(brave.Tracer.class)
@ConditionalOnBraveEnabled
static class BraveActuatorConfiguration {
@Bean
@ConditionalOnMissingBean
FinishedSpanWriter sleuthBraveFinishedSpanWriter() {
return new BraveFinishedSpanWriter();
}
/**
* We need to register at least one {@link SpanHandler} for sampling to hook in.
* If there are no span handlers all spans will be noop and no spans will get
* reported.
* @return a noop span handler
*/
@Bean
SpanHandler sleuthBraveCustomSpanHandler() {
return new SpanHandler() {
};
}
}
}
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import brave.handler.SpanHandler;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.endpoint.Producible;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.autoconfig.brave.ConditionalOnBraveEnabled;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} for Sleuth actuator endpoint.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true)
@ConditionalOnAvailableEndpoint(endpoint = TracesScrapeEndpoint.class)
@AutoConfigureBefore(BraveAutoConfiguration.class)
@EnableConfigurationProperties(SleuthActuatorProperties.class)
@ConditionalOnClass(Producible.class)
public class TraceSleuthActuatorAutoConfiguration {
@Bean
@ConditionalOnMissingBean
BufferingSpanReporter sleuthBufferingSpanReporter(SleuthActuatorProperties sleuthActuatorProperties) {
return new BufferingSpanReporter(sleuthActuatorProperties.getCapacity());
}
@Bean
@ConditionalOnMissingBean
TracesScrapeEndpoint sleuthTracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter,
FinishedSpanWriter finishedSpanWriter) {
return new TracesScrapeEndpoint(bufferingSpanReporter, finishedSpanWriter);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(brave.Tracer.class)
@ConditionalOnBraveEnabled
static class BraveActuatorConfiguration {
@Bean
@ConditionalOnMissingBean
FinishedSpanWriter sleuthBraveFinishedSpanWriter() {
return new BraveFinishedSpanWriter();
}
/**
* We need to register at least one {@link SpanHandler} for sampling to hook in.
* If there are no span handlers all spans will be noop and no spans will get
* reported.
* @return a noop span handler
*/
@Bean
SpanHandler sleuthBraveCustomSpanHandler() {
return new SpanHandler() {
};
}
}
}

View File

@@ -1,48 +1,48 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class TraceSleuthActuatorAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.endpoints.web.exposure.include=traces")
.withConfiguration(AutoConfigurations.of(TraceSleuthActuatorAutoConfiguration.class));
@Test
void should_register_actuator_when_management_endpoint_included() {
this.contextRunner.run(context -> BDDAssertions.then(context).hasSingleBean(TracesScrapeEndpoint.class));
}
@Test
void should_not_register_actuator_when_endpoint_disabled() {
this.contextRunner.withPropertyValues("management.endpoint.traces.enabled=false")
.run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class));
}
@Test
void should_not_register_actuator_when_sleuth_disabled() {
this.contextRunner.withPropertyValues("spring.sleuth.enabled=false")
.run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class));
}
}
/*
* Copyright 2013-2021 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.cloud.sleuth.autoconfig.actuate;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class TraceSleuthActuatorAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.endpoints.web.exposure.include=traces")
.withConfiguration(AutoConfigurations.of(TraceSleuthActuatorAutoConfiguration.class));
@Test
void should_register_actuator_when_management_endpoint_included() {
this.contextRunner.run(context -> BDDAssertions.then(context).hasSingleBean(TracesScrapeEndpoint.class));
}
@Test
void should_not_register_actuator_when_endpoint_disabled() {
this.contextRunner.withPropertyValues("management.endpoint.traces.enabled=false")
.run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class));
}
@Test
void should_not_register_actuator_when_sleuth_disabled() {
this.contextRunner.withPropertyValues("spring.sleuth.enabled=false")
.run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class));
}
}

View File

@@ -212,6 +212,26 @@
<artifactId>spring-session-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>p6spy</groupId>
@@ -246,6 +266,78 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/kotlin</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>fast</id>

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.kotlin;
import kotlin.coroutines.CoroutineContext;
import kotlin.jvm.functions.Function2;
import kotlinx.coroutines.ThreadContextElement;
import org.jetbrains.annotations.Nullable;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAndScope;
import org.springframework.cloud.sleuth.Tracer;
/**
* {@link ThreadContextElement} for synchronizing a {@link SpanAndScope} across coroutine
* suspension and resumption.
*
* Inspired by OpenTelemetry's KotlinContextElement.
*
* @since 3.1.0
*/
class KotlinContextElement implements ThreadContextElement<SpanAndScope> {
static final CoroutineContext.Key<KotlinContextElement> KEY = new CoroutineContext.Key<KotlinContextElement>() {
};
private final Span span;
private final Tracer tracer;
KotlinContextElement(Tracer tracer) {
this.tracer = tracer;
this.span = tracer.currentSpan();
}
Span getSpan() {
return this.span;
}
@Override
public CoroutineContext.Key<?> getKey() {
return KEY;
}
@Override
@SuppressWarnings("MustBeClosedChecker")
public SpanAndScope updateThreadContext(CoroutineContext coroutineContext) {
Tracer.SpanInScope spanInScope = this.tracer.withSpan(this.span);
return new SpanAndScope(this.span, spanInScope);
}
@Override
public void restoreThreadContext(CoroutineContext coroutineContext, SpanAndScope spanAndScope) {
spanAndScope.close();
}
@Override
public CoroutineContext plus(CoroutineContext coroutineContext) {
return CoroutineContext.DefaultImpls.plus(this, coroutineContext);
}
@Override
public <R> R fold(R initial, Function2<? super R, ? super CoroutineContext.Element, ? extends R> operation) {
return CoroutineContext.Element.DefaultImpls.fold(this, initial, operation);
}
@Nullable
@Override
public <E extends CoroutineContext.Element> E get(CoroutineContext.Key<E> key) {
return CoroutineContext.Element.DefaultImpls.get(this, key);
}
@Override
public CoroutineContext minusKey(CoroutineContext.Key<?> key) {
return CoroutineContext.Element.DefaultImpls.minusKey(this, key);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.kotlin
import kotlinx.coroutines.reactor.ReactorContext
import org.springframework.cloud.sleuth.CurrentTraceContext
import org.springframework.cloud.sleuth.Span
import org.springframework.cloud.sleuth.TraceContext
import org.springframework.cloud.sleuth.Tracer
import org.springframework.util.ClassUtils
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.jvm.internal.impl.load.kotlin.KotlinClassFinder
/**
* Returns a [CoroutineContext] which will make this [Context] current when resuming a coroutine
* and restores the previous [Context] on suspension.
*
* Inspired by OpenTelemetry's asContextElement.
* @since 3.1.0
*/
fun Tracer.asContextElement(): CoroutineContext {
return KotlinContextElement(this)
}
/**
* Returns the [Span] in this [CoroutineContext] if present, or null otherwise.
*
* Inspired by OpenTelemetry's asContextElement.
* @since 3.1.0
*/
fun CoroutineContext.currentSpan(): Span? {
val element = get(KotlinContextElement.KEY)
if (element is KotlinContextElement) {
return element.span
}
if (!ClassUtils.isPresent("kotlinx.coroutines.reactor.ReactorContext", null)) {
return null
}
val reactorContext = get(ReactorContext.Key)
if (reactorContext != null) {
if (reactorContext.context.hasKey(Span::class.java)) {
return reactorContext.context.get(Span::class.java)
}
else if (reactorContext.context.hasKey(TraceContext::class.java) && reactorContext.context.hasKey(Tracer::class.java) && reactorContext.context.hasKey(CurrentTraceContext::class.java)) {
val traceContext = reactorContext.context.get(TraceContext::class.java)
reactorContext.context.get(CurrentTraceContext::class.java).maybeScope(traceContext).use {
return reactorContext.context.get(Tracer::class.java).currentSpan()
}
}
else if (reactorContext.context.hasKey(Tracer::class.java)) {
return reactorContext.context.get(Tracer::class.java).currentSpan()
}
}
return null
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.instrument.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactor.ReactorContext
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.FilteredClassLoader
import org.springframework.cloud.sleuth.CurrentTraceContext
import org.springframework.cloud.sleuth.Span
import org.springframework.cloud.sleuth.TraceContext
import org.springframework.cloud.sleuth.Tracer
import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext
import org.springframework.cloud.sleuth.tracer.SimpleTracer
import reactor.util.context.Context
internal class AsContextElementKtTests {
@Test
fun `should return current span from context`(): Unit = runBlocking {
val simpleTracer = SimpleTracer()
val nextSpan = simpleTracer.nextSpan().start()
var spanInGlobalScopeLaunch: Span? = null
var spanInGlobalScopeAsync: Span? = null
val asContextElement = simpleTracer.asContextElement()
GlobalScope.launch(asContextElement) {
spanInGlobalScopeLaunch = coroutineContext.currentSpan()
}
GlobalScope.async(asContextElement) {
spanInGlobalScopeAsync = coroutineContext.currentSpan()
}.await()
then(spanInGlobalScopeLaunch).isSameAs(nextSpan)
then(spanInGlobalScopeAsync).isSameAs(nextSpan)
}
@Test
fun `should return span from coroutine context when KotlinContextElement present`(): Unit = runBlocking {
val simpleTracer = SimpleTracer()
val nextSpan = simpleTracer.nextSpan().start()
val element = KotlinContextElement(simpleTracer)
then(element.currentSpan()).isSameAs(nextSpan)
}
@Test
fun `should return null from coroutine context when KotlinContextElement and Reactor extensions are missing`(): Unit = runBlocking {
val contextClassLoader = Thread.currentThread().contextClassLoader
try {
Thread.currentThread().contextClassLoader = FilteredClassLoader("kotlinx.coroutines.reactor.ReactorContext")
then(coroutineContext.currentSpan()).isNull()
} finally {
Thread.currentThread().contextClassLoader = contextClassLoader
}
}
@Test
fun `should return Span from Reactor extensions when KotlinContextElement missing`(): Unit = runBlocking {
val simpleTracer = SimpleTracer()
val nextSpan = simpleTracer.nextSpan().start()
val reactorContext = ReactorContext(Context.of(Span::class.java, nextSpan))
then(reactorContext.currentSpan()).isSameAs(nextSpan);
}
@Test
fun `should return Span from Reactor extensions CurrentTraceContext when KotlinContextElement missing and there is TraceContext in Reactor context`(): Unit = runBlocking {
val currentTraceContext = SimpleCurrentTraceContext()
val simpleTracer = SimpleTracer()
val nextSpan = simpleTracer.nextSpan().start()
val reactorContext = ReactorContext(Context.of(Tracer::class.java, simpleTracer, CurrentTraceContext::class.java, currentTraceContext, TraceContext::class.java, nextSpan.context()))
then(reactorContext.currentSpan()).isSameAs(nextSpan);
}
@Test
fun `should return Span from Reactor extensions Tracer when KotlinContextElement missing and there is no Span in context`(): Unit = runBlocking {
val simpleTracer = SimpleTracer()
val nextSpan = simpleTracer.nextSpan().start()
val reactorContext = ReactorContext(Context.of(Tracer::class.java, simpleTracer))
then(reactorContext.currentSpan()).isSameAs(nextSpan);
}
@Test
fun `should return null when no span is found`(): Unit = runBlocking {
val reactorContext = ReactorContext(Context.empty())
then(reactorContext.currentSpan()).isNull()
}
}

View File

@@ -48,6 +48,7 @@
<module>spring-cloud-sleuth-instrumentation-grpc-tests</module>
<module>spring-cloud-sleuth-instrumentation-jdbc-tests</module>
<module>spring-cloud-sleuth-instrumentation-kafka-tests</module>
<module>spring-cloud-sleuth-instrumentation-kotlin-tests</module>
<module>spring-cloud-sleuth-instrumentation-lettuce-tests</module>
<module>spring-cloud-sleuth-instrumentation-messaging-tests</module>
<module>spring-cloud-sleuth-instrumentation-mvc-tests</module>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2021 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.
~
~
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-sleuth-instrumentation-kotlin-tests</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Sleuth Brave Kotlin Instrumentation Tests</name>
<description>Spring Cloud Sleuth Brave Kotlin Instrumentation Tests</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-tests-brave</artifactId>
<version>3.1.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<properties>
<sonar.skip>true</sonar.skip>
</properties>
<build>
<plugins>
<plugin>
<!--skip deploy -->
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/kotlin</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/test/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-tests-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-tests</artifactId>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.brave.kotlin
import brave.sampler.Sampler
import brave.test.TestSpanHandler
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration
class SleuthCoroutinesApplicationTests : org.springframework.cloud.sleuth.kotlin.SleuthCoroutinesApplicationTests() {
@Configuration(proxyBeanMethods = false)
internal class Config {
@Bean
fun testSpanHandlerSupplier(testSpanHandler: TestSpanHandler?): org.springframework.cloud.sleuth.test.TestSpanHandler {
return BraveTestSpanHandler(testSpanHandler)
}
@Bean
fun alwaysSampler(): Sampler {
return Sampler.ALWAYS_SAMPLE
}
@Bean
fun braveTestSpanHandler(): TestSpanHandler {
return TestSpanHandler()
}
}
}

View File

@@ -260,6 +260,87 @@
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>${build-helper-maven-plugin.version}</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/kotlin</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2013-2021 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.cloud.sleuth.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.BDDAssertions.then
import org.junit.jupiter.api.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.web.server.LocalServerPort
import org.springframework.cloud.sleuth.Span
import org.springframework.cloud.sleuth.Tracer
import org.springframework.cloud.sleuth.instrument.kotlin.asContextElement
import org.springframework.cloud.sleuth.instrument.kotlin.currentSpan
import org.springframework.context.annotation.Bean
import org.springframework.test.context.ContextConfiguration
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.RestTemplate
import kotlin.coroutines.coroutineContext
@ContextConfiguration(classes = [SleuthCoroutinesApplicationTests.ControllerTestConfig::class])
abstract class SleuthCoroutinesApplicationTests {
@LocalServerPort
var serverPort: Int = 0
@Autowired
lateinit var tracer: Tracer
@Autowired
lateinit var webController: WebController
@Autowired
lateinit var restTemplate: RestTemplate
@Test
fun should_pass_tracing_context_within_coroutines(): Unit = runBlocking {
val nextSpan = tracer.nextSpan()
val traceId = nextSpan.context().traceId()
tracer.withSpan(nextSpan.start()).use { withSpan ->
val response = restTemplate.getForObject(applicationUrl(), String::class.java)
// span in the controller is set
then(webController.spanInController).isNotNull
then(webController.spanInController?.context()?.traceId()).isEqualTo(traceId)
// span in global scope launch is set
then(webController.spanInGlobalScopeLaunch).isNotNull
then(webController.spanInGlobalScopeLaunch?.context()?.traceId()).isEqualTo(traceId)
// span in global async scope is set
then(webController.spanInGlobalScopeAsync).isNotNull
then(webController.spanInGlobalScopeAsync?.context()?.traceId()).isEqualTo(traceId)
// trace id got propagated
then(response).isEqualTo(traceId)
}
}
private fun applicationUrl() = "http://localhost:$serverPort/hello"
@TestConfiguration
@EnableAutoConfiguration
internal class ControllerTestConfig {
@Bean
fun restTemplate(): RestTemplate = RestTemplate()
@Bean
fun webController(tracer: Tracer): WebController = WebController(tracer)
}
}
@RestController
class WebController(val tracer: Tracer) {
val log: Logger = LoggerFactory.getLogger(this::class.java)
var spanInController: Span? = null
var spanInGlobalScopeLaunch: Span? = null
var spanInGlobalScopeAsync: Span? = null
@GetMapping("/hello")
suspend fun hello(): String? {
spanInController = coroutineContext.currentSpan()
GlobalScope.launch(tracer.asContextElement()) {
log.info("in Coroutines context (launch) - current span {}", coroutineContext.currentSpan())
spanInGlobalScopeLaunch = coroutineContext.currentSpan()
}
GlobalScope.async(tracer.asContextElement()) {
log.info("in Coroutines context (async)- current span {}", coroutineContext.currentSpan())
spanInGlobalScopeAsync = coroutineContext.currentSpan()
}.await()
return coroutineContext.currentSpan()?.context()?.traceId()
}
}