diff --git a/.editorconfig b/.editorconfig index ffe385c6d..3240ec9da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/docs/pom.xml b/docs/pom.xml index 2481231ef..fd336d69f 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -67,7 +67,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + ${build-helper-maven-plugin.version} generate-sources diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 9e1cb711d..3b6e2ca9e 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -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. diff --git a/pom.xml b/pom.xml index 087ddb99a..fc67df95b 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ 1.7 10.0.6 2.8.0 + 1.5.10 true @@ -98,6 +99,7 @@ 0.21.3 0.14.1 1.15.3 + 3.2.0 diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java index 4e40a55db..c174467cc 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java @@ -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 { - - /** - * 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 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 { + + /** + * 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 spans); + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java index 8cfa0005f..03add3b2d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java @@ -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() { + + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java index 16bb57137..2f675933c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java @@ -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)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 4af8bedbc..471bae1db 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -212,6 +212,26 @@ spring-session-data-redis true + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + p6spy @@ -246,6 +266,78 @@ + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + fast diff --git a/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java new file mode 100644 index 000000000..c1b9129bb --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java @@ -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 { + + static final CoroutineContext.Key KEY = new CoroutineContext.Key() { + }; + + 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 fold(R initial, Function2 operation) { + return CoroutineContext.Element.DefaultImpls.fold(this, initial, operation); + } + + @Nullable + @Override + public E get(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.get(this, key); + } + + @Override + public CoroutineContext minusKey(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.minusKey(this, key); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt new file mode 100644 index 000000000..9c0641e38 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt @@ -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 +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt b/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt new file mode 100644 index 000000000..ca25386c7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt @@ -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() + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 9e87b6cb5..50083a144 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -48,6 +48,7 @@ spring-cloud-sleuth-instrumentation-grpc-tests spring-cloud-sleuth-instrumentation-jdbc-tests spring-cloud-sleuth-instrumentation-kafka-tests + spring-cloud-sleuth-instrumentation-kotlin-tests spring-cloud-sleuth-instrumentation-lettuce-tests spring-cloud-sleuth-instrumentation-messaging-tests spring-cloud-sleuth-instrumentation-mvc-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml new file mode 100644 index 000000000..0dc78f359 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml @@ -0,0 +1,170 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-kotlin-tests + jar + Spring Cloud Sleuth Brave Kotlin Instrumentation Tests + Spring Cloud Sleuth Brave Kotlin Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt new file mode 100644 index 000000000..6a33b677a --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt @@ -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() + } + } +} diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 46fe1655d..8f52be66c 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -260,6 +260,87 @@ + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + diff --git a/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt b/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt new file mode 100644 index 000000000..213148959 --- /dev/null +++ b/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt @@ -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() + } +}