From 216d681b04a2fc228ca5628e4f5c4593d9b33171 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 13:32:58 +0200 Subject: [PATCH] Adds RSocket instrumentation coauthor @olegdokuka fixes gh-1677 --- docs/src/main/asciidoc/integrations.adoc | 9 + .../cloud/sleuth/SpanAndScope.java | 5 + .../cloud/sleuth/TraceContext.java | 43 ++ .../springframework/cloud/sleuth/Tracer.java | 6 + .../cloud/sleuth/WithThreadLocalSpan.java | 91 ++++ spring-cloud-sleuth-autoconfigure/pom.xml | 5 + .../messaging/SleuthMessagingProperties.java | 34 ++ ...TraceSpringMessagingAutoConfiguration.java | 11 + .../rsocket/SleuthRSocketProperties.java | 43 ++ .../TraceRSocketAutoConfiguration.java | 89 ++++ .../main/resources/META-INF/spring.factories | 1 + .../autoconfig/NoOpTraceContextBuilder.java | 54 ++ .../cloud/sleuth/autoconfig/NoOpTracer.java | 5 + .../bridge/BraveTraceContextBuilder.java | 71 +++ .../sleuth/brave/bridge/BraveTracer.java | 5 + .../sleuth/brave/bridge/W3CPropagation.java | 174 +------ .../bridge/BraveTraceContextBuilderTests.java | 54 ++ .../brave/bridge/W3CPropagationTest.java | 8 +- spring-cloud-sleuth-instrumentation/pom.xml | 10 + .../instrument/async/TraceAsyncAspect.java | 1 + .../messaging/TraceMessageHandler.java | 6 - .../messaging/TraceMessagingAspect.java | 100 ++++ .../messaging/TracingChannelInterceptor.java | 49 +- .../instrument/reactor/ReactorSleuth.java | 75 ++- .../instrument/rsocket/ByteBufGetter.java | 38 ++ .../instrument/rsocket/ByteBufSetter.java | 35 ++ .../rsocket/CompositeMetadataUtils.java | 42 ++ .../instrument/rsocket/PayloadUtils.java | 68 +++ .../TracingRSocketConnectorConfigurer.java | 49 ++ .../TracingRSocketServerCustomizer.java | 49 ++ .../rsocket/TracingRequesterRSocketProxy.java | 168 +++++++ .../rsocket/TracingResponderRSocketProxy.java | 170 +++++++ .../cloud/sleuth/internal/EncodingUtils.java | 322 ++++++++++++ .../instrument/deployer/SimpleTracer.java | 5 + .../sleuth/internal/EncodingUtilsTests.java | 43 ++ tests/brave/pom.xml | 1 + .../pom.xml | 100 ++++ .../instrument/rsocket/TraceRSocketTests.java | 53 ++ .../src/test/resources/logback.xml | 32 ++ tests/common/pom.xml | 465 +++++++++--------- .../config/ConfigServerIntegrationTests.java | 2 +- .../instrument/rsocket/TraceRSocketTests.java | 363 ++++++++++++++ 42 files changed, 2494 insertions(+), 460 deletions(-) create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java create mode 100644 spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java create mode 100644 spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 9d8530d57..322c2ff23 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -583,3 +583,12 @@ This feature is available for all tracer implementations. If you have Spring Cloud Deployer running on the classpath, we wrap the `AppDeployer` in a trace representation. We are polling the application for its status at a default interval. You can change that default by setting the `spring.sleuth.deployer.status-poll-delay` property. In order to disable this instrumentation set `spring.sleuth.deployer.enabled` to `false`. + + +[[sleuth-deployer-integration]] +== Spring RSocket + +This feature is available for all tracer implementations. + +If you have Spring RSocket running on the classpath, we wrap the inbound and outbound communication to propagate the tracing context via the metadata. +In order to disable this instrumentation set `spring.sleuth.rsocket.enabled` to `false`. diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java index e18e028d7..807e871c6 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java @@ -41,4 +41,9 @@ public class SpanAndScope { return this.scope; } + @Override + public String toString() { + return "SpanAndScope{" + "span=" + this.span + '}'; + } + } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java index d9e564bbd..15cb7231e 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java @@ -51,4 +51,47 @@ public interface TraceContext { */ Boolean sampled(); + /** + * Builder for {@link TraceContext}. + * + * @since 3.1.0 + */ + interface Builder { + + /** + * Sets trace id on the trace context. + * @param traceId trace id + * @return this + */ + TraceContext.Builder traceId(String traceId); + + /** + * Sets parent id on the trace context. + * @param parentId parent trace id + * @return this + */ + TraceContext.Builder parentId(String parentId); + + /** + * Sets span id on the trace context. + * @param spanId span id + * @return this + */ + TraceContext.Builder spanId(String spanId); + + /** + * Sets sampled on the trace context. + * @param sampled if span is sampled + * @return this + */ + TraceContext.Builder sampled(Boolean sampled); + + /** + * Builds the trace context. + * @return trace context + */ + TraceContext build(); + + } + } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java index bf05dea69..b5a06b5b9 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java @@ -134,6 +134,12 @@ public interface Tracer extends BaggageManager { */ Span.Builder spanBuilder(); + /** + * Builder for {@link TraceContext}. + * @return a trace context builder + */ + TraceContext.Builder traceContextBuilder(); + /** * Allows to customize the current span in scope. * @return current span customizer diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java new file mode 100644 index 000000000..89aec40f8 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java @@ -0,0 +1,91 @@ +/* + * 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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Represents a {@link Span} stored in thread local. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface WithThreadLocalSpan { + + /** + * Logger. + */ + Log log = LogFactory.getLog(WithThreadLocalSpan.class); + + /** + * Sets the span in thread local scope. + * @param span span to put in thread local + */ + default void setSpanInScope(Span span) { + getThreadLocalSpan().set(span); + if (log.isDebugEnabled()) { + log.debug("Put span in scope " + span); + } + } + + /** + * Finishes the thread local span. + * @param error potential error to be stored in span + */ + default void finishSpan(@Nullable Throwable error) { + SpanAndScope spanAndScope = takeSpanFromThreadLocal(); + if (spanAndScope == null) { + return; + } + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + if (span.isNoop()) { + if (log.isDebugEnabled()) { + log.debug("Span " + span + " is noop - will stope the scope"); + } + scope.close(); + return; + } + if (error != null) { // an error occurred, adding error to span + span.error(error); + } + if (log.isDebugEnabled()) { + log.debug("Will finish the span and its corresponding scope " + span); + } + span.end(); + scope.close(); + } + + /** + * Takes a span from thread local and restores the previous one if present. + * @return span from a thread local span + */ + default SpanAndScope takeSpanFromThreadLocal() { + SpanAndScope span = getThreadLocalSpan().get(); + if (log.isDebugEnabled()) { + log.debug("Took span [" + span + "] from thread local"); + } + getThreadLocalSpan().remove(); + return span; + } + + ThreadLocalSpan getThreadLocalSpan(); + +} diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 994f00cd4..7f314a3b0 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -324,6 +324,11 @@ spring-boot-starter-data-mongodb true + + org.springframework.boot + spring-boot-starter-rsocket + true + diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java index 4a4d88c4a..3175d87ee 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java @@ -17,6 +17,7 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.messaging; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.messaging.handler.annotation.MessageMapping; /** * Properties for messaging. @@ -32,6 +33,11 @@ public class SleuthMessagingProperties { */ private boolean enabled; + /** + * Aspect related properties. + */ + private Aspect aspect = new Aspect(); + /** * Rabbit related properties. */ @@ -55,6 +61,14 @@ public class SleuthMessagingProperties { this.enabled = enabled; } + public Aspect getAspect() { + return this.aspect; + } + + public void setAspect(Aspect aspect) { + this.aspect = aspect; + } + public Rabbit getRabbit() { return this.rabbit; } @@ -79,6 +93,26 @@ public class SleuthMessagingProperties { this.jms = jms; } + /** + * Aspect configuration. + */ + public static class Aspect { + + /** + * Should {@link MessageMapping} wrapping be enabled. + */ + private boolean enabled; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + /** * RabbitMQ configuration. */ diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java index fa596a866..a143bdb75 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java @@ -16,12 +16,16 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.messaging; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.messaging.MessageHeaderPropagatorGetter; import org.springframework.cloud.sleuth.instrument.messaging.MessageHeaderPropagatorSetter; +import org.springframework.cloud.sleuth.instrument.messaging.TraceMessagingAspect; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,10 +33,17 @@ import org.springframework.messaging.support.MessageHeaderAccessor; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MessageHeaderAccessor.class) +@ConditionalOnBean(Tracer.class) @ConditionalOnProperty(value = "spring.sleuth.messaging.enabled", matchIfMissing = true) @EnableConfigurationProperties({ SleuthIntegrationMessagingProperties.class, SleuthMessagingProperties.class }) class TraceSpringMessagingAutoConfiguration { + @Bean + @ConditionalOnProperty(value = "spring.sleuth.messaging.aspect.enabled", matchIfMissing = true) + TraceMessagingAspect traceMessagingAspect(Tracer tracer, SpanNamer spanNamer) { + return new TraceMessagingAspect(tracer, spanNamer); + } + @Bean @ConditionalOnMissingBean Propagator.Setter traceMessagePropagationSetter() { diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java new file mode 100644 index 000000000..7dd0b0776 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java @@ -0,0 +1,43 @@ +/* + * 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.instrument.rsocket; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sleuth RSocket settings. + * + * @author Oleh Dokuka + * @since 3.1.0 + */ +@ConfigurationProperties("spring.sleuth.rsocket") +public class SleuthRSocketProperties { + + /** + * When true enables instrumentation for rsocket. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java new file mode 100644 index 000000000..584f76199 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -0,0 +1,89 @@ +/* + * 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.instrument.rsocket; + +import java.util.List; + +import io.rsocket.RSocket; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +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.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.propagation.PropagationType; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) +@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) +@EnableConfigurationProperties(SleuthRSocketProperties.class) +public class TraceRSocketAutoConfiguration { + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + Builder rSocketRequesterBuilder(RSocketStrategies strategies, + ObjectProvider connectorConfigurerProvider) { + // TODO: should be in spring boot + final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); + connectorConfigurerProvider.forEach(builder::rsocketConnector); + return builder; + } + + private boolean containsZipkinPropagationType(List types) { + return types.contains(PropagationType.B3); + } + + @Bean + RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); + } + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index d96b177e0..91d031660 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -19,6 +19,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAu org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.rsocket.TraceRSocketAutoConfiguration, \ org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java new file mode 100644 index 000000000..c0b17da52 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java @@ -0,0 +1,54 @@ +/* + * 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; + +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class NoOpTraceContextBuilder implements TraceContext.Builder { + + @Override + public TraceContext.Builder traceId(String traceId) { + return this; + } + + @Override + public TraceContext.Builder parentId(String traceId) { + return this; + } + + @Override + public TraceContext.Builder spanId(String spanId) { + return null; + } + + @Override + public TraceContext.Builder sampled(Boolean sampled) { + return this; + } + + @Override + public TraceContext build() { + return new NoOpTraceContext(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java index 95a8f281e..08d4d99ea 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java @@ -69,6 +69,11 @@ class NoOpTracer implements Tracer { return new NoOpSpanBuilder(); } + @Override + public TraceContext.Builder traceContextBuilder() { + return new NoOpTraceContextBuilder(); + } + @Override public Map getAllBaggage() { return new HashMap<>(); diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java new file mode 100644 index 000000000..0c19ac5b8 --- /dev/null +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java @@ -0,0 +1,71 @@ +/* + * 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.bridge; + +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.internal.EncodingUtils; + +/** + * Brave implementation of a {@link TraceContext.Builder}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class BraveTraceContextBuilder implements TraceContext.Builder { + + brave.propagation.TraceContext.Builder delegate = brave.propagation.TraceContext.newBuilder(); + + @Override + public TraceContext.Builder traceId(String traceId) { + long[] fromString = EncodingUtils.fromString(traceId); + if (fromString.length == 2) { + this.delegate.traceIdHigh(fromString[0]); + this.delegate.traceId(fromString[1]); + } + else { + this.delegate.traceId(fromString[0]); + } + return this; + } + + @Override + public TraceContext.Builder parentId(String traceId) { + long[] fromString = EncodingUtils.fromString(traceId); + this.delegate.parentId(fromString[fromString.length == 2 ? 1 : 0]); + return this; + } + + @Override + public TraceContext.Builder spanId(String spanId) { + long[] fromString = EncodingUtils.fromString(spanId); + this.delegate.spanId(fromString[fromString.length == 2 ? 1 : 0]); + return this; + } + + @Override + public TraceContext.Builder sampled(Boolean sampled) { + this.delegate.sampled(sampled); + return this; + } + + @Override + public TraceContext build() { + brave.propagation.TraceContext context = this.delegate.build(); + return BraveTraceContext.fromBrave(context); + } + +} diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java index 5eaa63958..258a5c53b 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java @@ -90,6 +90,11 @@ public class BraveTracer implements Tracer { return new BraveSpanBuilder(this.tracer); } + @Override + public TraceContext.Builder traceContextBuilder() { + return new BraveTraceContextBuilder(); + } + @Override public Map getAllBaggage() { return this.braveBaggageManager.getAllBaggage(); diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java index 0931b52e2..4f8a87207 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.sleuth.BaggageInScope; +import org.springframework.cloud.sleuth.internal.EncodingUtils; import static java.util.Collections.singletonList; @@ -219,12 +220,12 @@ class W3CPropagation extends Propagation.Factory implements Propagation private static boolean isTraceIdValid(CharSequence traceId) { return (traceId.length() == TRACE_ID_HEX_SIZE) && !INVALID_TRACE_ID.contentEquals(traceId) - && BigendianEncoding.isValidBase16String(traceId); + && EncodingUtils.isValidBase16String(traceId); } private static boolean isSpanIdValid(String spanId) { return (spanId.length() == SPAN_ID_HEX_SIZE) && !INVALID_SPAN_ID.equals(spanId) - && BigendianEncoding.isValidBase16String(spanId); + && EncodingUtils.isValidBase16String(spanId); } private static TraceContext extractContextFromTraceParent(String traceparent) { @@ -257,10 +258,10 @@ class W3CPropagation extends Propagation.Factory implements Propagation String traceIdLow = traceId.substring(traceId.length() / 2); byte isSampled = TraceFlags.byteFromHex(traceparent, TRACE_OPTION_OFFSET); return TraceContext.newBuilder().shared(true) - .traceIdHigh(BigendianEncoding.longFromBase16String(traceIdHigh)) - .traceId(BigendianEncoding.longFromBase16String(traceIdLow)) - .spanId(BigendianEncoding.longFromBase16String(spanId)) - .sampled(isSampled == TraceFlags.IS_SAMPLED).build(); + .traceIdHigh(EncodingUtils.longFromBase16String(traceIdHigh)) + .traceId(EncodingUtils.longFromBase16String(traceIdLow)) + .spanId(EncodingUtils.longFromBase16String(spanId)).sampled(isSampled == TraceFlags.IS_SAMPLED) + .build(); } return null; } @@ -421,41 +422,6 @@ final class TemporaryBuffers { } -/** - * Taken from OpenTelemetry API. - */ -final class Utils { - - private Utils() { - - } - - /** - * Throws an {@link IllegalArgumentException} if the argument is false. This method is - * similar to {@code Preconditions.checkArgument(boolean, Object)} from Guava. - * @param isValid whether the argument check passed. - * @param errorMessage the message to use for the exception. - */ - static void checkArgument(boolean isValid, String errorMessage) { - if (!isValid) { - throw new IllegalArgumentException(errorMessage); - } - } - - /** - * Throws an {@link IllegalStateException} if the argument is false. This method is - * similar to {@code Preconditions.checkState(boolean, Object)} from Guava. - * @param isValid whether the state check passed. - * @param errorMessage the message to use for the exception. - */ - static void checkState(boolean isValid, String errorMessage) { - if (!isValid) { - throw new IllegalStateException(String.valueOf(errorMessage)); - } - } - -} - /** * Taken from OpenTelemetry API. */ @@ -469,131 +435,7 @@ final class TraceFlags { /** Extract the byte representation of the flags from a hex-representation. */ static byte byteFromHex(CharSequence src, int srcOffset) { - return BigendianEncoding.byteFromBase16String(src, srcOffset); - } - -} - -/** - * Taken from OpenTelemetry API. - */ -final class BigendianEncoding { - - private BigendianEncoding() { - } - - static final int LONG_BYTES = Long.SIZE / Byte.SIZE; - - static final int BYTE_BASE16 = 2; - - static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES; - - private static final String ALPHABET = "0123456789abcdef"; - - private static final int ASCII_CHARACTERS = 128; - - private static final char[] ENCODING = buildEncodingArray(); - - private static final byte[] DECODING = buildDecodingArray(); - - private static char[] buildEncodingArray() { - char[] encoding = new char[512]; - for (int i = 0; i < 256; ++i) { - encoding[i] = ALPHABET.charAt(i >>> 4); - encoding[i | 0x100] = ALPHABET.charAt(i & 0xF); - } - return encoding; - } - - private static byte[] buildDecodingArray() { - byte[] decoding = new byte[ASCII_CHARACTERS]; - Arrays.fill(decoding, (byte) -1); - for (int i = 0; i < ALPHABET.length(); i++) { - char c = ALPHABET.charAt(i); - decoding[c] = (byte) i; - } - return decoding; - } - - /** - * Returns the {@code long} value whose base16 representation is stored in the first - * 16 chars of {@code chars} starting from the {@code offset}. - * @param chars the base16 representation of the {@code long}. - */ - static long longFromBase16String(CharSequence chars) { - return longFromBase16String(chars, 0); - } - - /** - * Returns the {@code long} value whose base16 representation is stored in the first - * 16 chars of {@code chars} starting from the {@code offset}. - * @param chars the base16 representation of the {@code long}. - */ - static long longFromBase16String(CharSequence chars, int offset) { - Utils.checkArgument(chars.length() >= offset + LONG_BASE16, "chars too small"); - return (decodeByte(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56 - | (decodeByte(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48 - | (decodeByte(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40 - | (decodeByte(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32 - | (decodeByte(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24 - | (decodeByte(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16 - | (decodeByte(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8 - | (decodeByte(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL); - } - - /** - * Decodes the specified two character sequence, and returns the resulting - * {@code byte}. - * @param chars the character sequence to be decoded. - * @param offset the starting offset in the {@code CharSequence}. - * @return the resulting {@code byte} - * @throws IllegalArgumentException if the input is not a valid encoded string - * according to this encoding. - */ - static byte byteFromBase16String(CharSequence chars, int offset) { - Utils.checkArgument(chars.length() >= offset + 2, "chars too small"); - return decodeByte(chars.charAt(offset), chars.charAt(offset + 1)); - } - - private static byte decodeByte(char hi, char lo) { - Utils.checkArgument(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "invalid character " + lo); - Utils.checkArgument(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "invalid character " + hi); - int decoded = DECODING[hi] << 4 | DECODING[lo]; - return (byte) decoded; - } - - /** - * Returns the {@code long} value whose big-endian representation is stored in the - * first 8 bytes of {@code bytes} starting from the {@code offset}. - * @param bytes the byte array representation of the {@code long}. - * @param offset the starting offset in the byte array. - * @return the {@code long} value whose big-endian representation is given. - * @throws IllegalArgumentException if {@code bytes} has fewer than 8 elements. - */ - static long longFromByteArray(byte[] bytes, int offset) { - Utils.checkArgument(bytes.length >= offset + LONG_BYTES, "array too small"); - return (bytes[offset] & 0xFFL) << 56 | (bytes[offset + 1] & 0xFFL) << 48 | (bytes[offset + 2] & 0xFFL) << 40 - | (bytes[offset + 3] & 0xFFL) << 32 | (bytes[offset + 4] & 0xFFL) << 24 - | (bytes[offset + 5] & 0xFFL) << 16 | (bytes[offset + 6] & 0xFFL) << 8 | (bytes[offset + 7] & 0xFFL); - } - - static boolean isValidBase16String(CharSequence value) { - for (int i = 0; i < value.length(); i++) { - char b = value.charAt(i); - // 48..57 && 97..102 are valid - if (!isDigit(b) && !isLowercaseHexCharacter(b)) { - return false; - } - } - return true; - } - - private static boolean isLowercaseHexCharacter(char b) { - return 97 <= b && b <= 102; - } - - private static boolean isDigit(char b) { - return 48 <= b && b <= 57; + return EncodingUtils.byteFromBase16String(src, srcOffset); } } diff --git a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java new file mode 100644 index 000000000..570d6e70a --- /dev/null +++ b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java @@ -0,0 +1,54 @@ +/* + * 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.bridge; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.TraceContext; + +import static org.assertj.core.api.BDDAssertions.then; + +class BraveTraceContextBuilderTests { + + @Test + void should_set_trace_context_for_64_bit() { + BraveTraceContextBuilder builder = new BraveTraceContextBuilder(); + + TraceContext traceContext = builder.parentId("7c6239a5ad0a4287").spanId("caff89f7f0f229dd") + .traceId("596e1787feb11040").sampled(true).build(); + + then(traceContext.parentId()).isEqualTo("7c6239a5ad0a4287"); + then(traceContext.spanId()).isEqualTo("caff89f7f0f229dd"); + then(traceContext.traceId()).isEqualTo("596e1787feb11040"); + then(traceContext.sampled()).isTrue(); + } + + @Test + void should_set_trace_context_for_128_bit() { + BraveTraceContextBuilder builder = new BraveTraceContextBuilder(); + + TraceContext traceContext = builder.parentId("00000000000000007c6239a5ad0a4287") + .spanId("0000000000000000caff89f7f0f229dd").traceId("596e1787feb11040caff89f7f0f229dd").sampled(true) + .build(); + + then(traceContext.parentId()).isEqualTo("7c6239a5ad0a4287"); + then(traceContext.spanId()).isEqualTo("caff89f7f0f229dd"); + then(traceContext.traceId()).isEqualTo("596e1787feb11040caff89f7f0f229dd"); + then(traceContext.sampled()).isTrue(); + } + +} diff --git a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java index 265e0e76b..7d56613da 100644 --- a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java +++ b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java @@ -27,6 +27,8 @@ import brave.propagation.TraceContext; import brave.propagation.TraceContextOrSamplingFlags; import org.junit.jupiter.api.Test; +import org.springframework.cloud.sleuth.internal.EncodingUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.springframework.cloud.sleuth.brave.bridge.W3CPropagation.TRACE_PARENT; @@ -68,9 +70,9 @@ class W3CPropagationTest { private TraceContext.Builder sampledTraceContext(String traceIdHigh, String traceId, String spanId) { return TraceContext.newBuilder().sampled(SAMPLED_TRACE_OPTIONS) - .traceIdHigh(BigendianEncoding.longFromBase16String(traceIdHigh)) - .traceId(BigendianEncoding.longFromBase16String(traceId)) - .spanId(BigendianEncoding.longFromBase16String(spanId)); + .traceIdHigh(EncodingUtils.longFromBase16String(traceIdHigh)) + .traceId(EncodingUtils.longFromBase16String(traceId)) + .spanId(EncodingUtils.longFromBase16String(spanId)); } @Test diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 0dfdf55a4..3b2d48a01 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -42,6 +42,11 @@ spring-boot-starter-web true + + org.springframework.boot + spring-boot-starter-rsocket + true + io.micrometer micrometer-core @@ -52,6 +57,11 @@ reactor-core true + + io.rsocket + rsocket-core + true + io.projectreactor.kafka reactor-kafka diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java index 0d11b88f9..6be996c8d 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java @@ -62,6 +62,7 @@ public class TraceAsyncAspect { } span = span.name(spanName); try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { + // TODO: Make this less generic span.tag(CLASS_KEY, pjp.getTarget().getClass().getSimpleName()); span.tag(METHOD_KEY, pjp.getSignature().getName()); return pjp.proceed(); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java index 9c3a0cc91..52d02ffb3 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java @@ -191,12 +191,6 @@ class TraceMessageHandler { } } - private void addTags(Span result, String destinationName) { - if (StringUtils.hasText(destinationName)) { - result.tag("channel", SpanNameUtil.shorten(destinationName)); - } - } - /** * Called either when message got received and processed or message got sent. * @param span - span that corresponds to the given operation diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java new file mode 100644 index 000000000..fda6d71bd --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java @@ -0,0 +1,100 @@ +/* + * 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.messaging; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.internal.SpanNameUtil; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.util.ReflectionUtils; + +/** + * Aspect that wraps {@link MessageMapping} annotated methods in a tracing representation. + * + * TODO: Document that for client side responders declare them as beans + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@SuppressWarnings("ArgNamesWarningsInspection") +@Aspect +public class TraceMessagingAspect { + + private static final Log log = org.apache.commons.logging.LogFactory.getLog(TraceMessagingAspect.class); + + static final String MESSAGING_CONTROLLER_CLASS_KEY = "messaging.controller.class"; + + static final String MESSAGING_CONTROLLER_METHOD_KEY = "messaging.controller.method"; + + private final Tracer tracer; + + private final SpanNamer spanNamer; + + public TraceMessagingAspect(Tracer tracer, SpanNamer spanNamer) { + this.tracer = tracer; + this.spanNamer = spanNamer; + } + + @Pointcut("@within(org.springframework.messaging.handler.annotation.MessageMapping)") + private void anyMessageMappingAnnotated() { + } // NOSONAR + + @Around("anyMessageMappingAnnotated()") + @SuppressWarnings("unchecked") + public Object addTags(ProceedingJoinPoint pjp) throws Throwable { + Object object = pjp.proceed(); + String methodName = pjp.getSignature().getName(); + String className = pjp.getTarget().getClass().getName(); + Span currentSpan = currentSpan(pjp); + currentSpan.tag(MESSAGING_CONTROLLER_CLASS_KEY, className); + currentSpan.tag(MESSAGING_CONTROLLER_METHOD_KEY, methodName); + return object; + } + + private Span currentSpan(ProceedingJoinPoint pjp) { + Span currentSpan = this.tracer.currentSpan(); + if (currentSpan == null) { + if (log.isDebugEnabled()) { + log.debug("No span found - will create a new one"); + } + currentSpan = this.tracer.nextSpan().name(name(pjp)).start(); + } + return currentSpan; + } + + private String name(ProceedingJoinPoint pjp) { + return this.spanNamer.name(getMethod(pjp, pjp.getTarget()), + SpanNameUtil.toLowerHyphen(pjp.getSignature().getName())); + } + + private Method getMethod(ProceedingJoinPoint pjp, Object object) { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + return ReflectionUtils.findMethod(object.getClass(), method.getName(), method.getParameterTypes()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java index ed9d3780d..9ee309081 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java @@ -26,9 +26,9 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; import org.springframework.cloud.sleuth.ThreadLocalSpan; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.WithThreadLocalSpan; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.cloud.stream.binder.BinderType; import org.springframework.cloud.stream.binder.BinderTypeRegistry; @@ -58,7 +58,7 @@ import org.springframework.util.StringUtils; * @since 3.0.0 */ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter - implements ExecutorChannelInterceptor, ApplicationContextAware { + implements ExecutorChannelInterceptor, ApplicationContextAware, WithThreadLocalSpan { /** * Name of the class in Spring Cloud Stream that is a direct channel. @@ -163,13 +163,6 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter return outputMessage; } - private void setSpanInScope(Span span) { - this.threadLocalSpan.set(span); - if (log.isDebugEnabled()) { - log.debug("Put span in scope " + span); - } - } - private String toRemoteServiceName(MessageHeaderAccessor headers) { for (String key : headers.getMessageHeaders().keySet()) { String remoteServiceName = this.remoteServiceNameMapper.apply(key); @@ -357,41 +350,9 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter finishSpan(ex); } - void finishSpan(Exception error) { - SpanAndScope spanAndScope = getSpanFromThreadLocal(); - if (spanAndScope == null) { - return; - } - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - if (span.isNoop()) { - if (log.isDebugEnabled()) { - log.debug("Span " + span + " is noop - will stope the scope"); - } - scope.close(); - return; - } - if (error != null) { // an error occurred, adding error to span - String message = error.getMessage(); - if (message == null) { - message = error.getClass().getSimpleName(); - } - span.tag("error", message); - } - if (log.isDebugEnabled()) { - log.debug("Will finish the and its corresponding scope " + span); - } - span.end(); - scope.close(); - } - - private SpanAndScope getSpanFromThreadLocal() { - SpanAndScope span = this.threadLocalSpan.get(); - if (log.isDebugEnabled()) { - log.debug("Took span [" + span + "] from thread local"); - } - this.threadLocalSpan.remove(); - return span; + @Override + public ThreadLocalSpan getThreadLocalSpan() { + return this.threadLocalSpan; } private MessageHeaderAccessor mutableHeaderAccessor(Message message) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index dabadd1a3..02d870027 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -334,15 +334,26 @@ public abstract class ReactorSleuth { public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, @NonNull Consumer spanCustomizer) { + return runMonoSupplierInScope(supplier, spanCustomizer).contextWrite( + context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + private static Mono runMonoSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { return Mono.deferContextual(contextView -> { Span span = contextView.get(Span.class); spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return supplier.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + // @formatter:off + return supplier.get() + // TODO: Fix me when this is resolved in Reactor +// .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); } /** @@ -361,6 +372,20 @@ public abstract class ReactorSleuth { }); } + /** + * Wraps the given Mono in a trace representation. Puts the provided span to context. + * @param tracer - Tracer bean + * @param span - span to put in context + * @param supplier - supplier of a {@link Mono} to be wrapped in tracing + * @param - type returned by the Mono + * @return traced Mono + */ + public static Mono tracedMono(@NonNull Tracer tracer, @NonNull Span span, + @NonNull Supplier> supplier) { + return runMonoSupplierInScope(supplier, span1 -> { + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); + } + /** * Wraps the given Flux in a trace representation. Retrieves the span from context, * creates a child span with the given name. @@ -375,15 +400,41 @@ public abstract class ReactorSleuth { public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, @NonNull Consumer spanCustomizer) { + return runFluxSupplierInScope(supplier, spanCustomizer).contextWrite( + context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + /** + * Wraps the given Flux in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param span - span to put in context + * @param supplier - supplier of a {@link Flux} to be wrapped in tracing + * @param - type returned by the Flux + * @return traced Flux + */ + public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull Span span, + @NonNull Supplier> supplier) { + return runFluxSupplierInScope(supplier, span1 -> { + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); + } + + private static Flux runFluxSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { return Flux.deferContextual(contextView -> { Span span = contextView.get(Span.class); spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return supplier.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + // @formatter:off + return supplier.get() + // TODO: Fix me when this is resolved in Reactor +// .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); } /** @@ -442,6 +493,10 @@ public abstract class ReactorSleuth { private static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, reactor.util.context.Context context, String childSpanName) { Span span = spanFromContext(tracer, currentTraceContext, context, childSpanName); + return putSpanInScope(tracer, context, span); + } + + private static Context putSpanInScope(Tracer tracer, Context context, Span span) { return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, tracer.withSpan(span)); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java new file mode 100644 index 000000000..167af1865 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java @@ -0,0 +1,38 @@ +/* + * 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.rsocket; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +class ByteBufGetter implements Propagator.Getter { + + @Override + public String get(ByteBuf carrier, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(carrier, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + if (key.equals(entry.getMimeType())) { + return entry.getContent().toString(CharsetUtil.UTF_8); + } + } + return null; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java new file mode 100644 index 000000000..e44a77de7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java @@ -0,0 +1,35 @@ +/* + * 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.rsocket; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadataCodec; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +class ByteBufSetter implements Propagator.Setter { + + @Override + public void set(CompositeByteBuf carrier, String key, String value) { + final ByteBufAllocator alloc = carrier.alloc(); + CompositeMetadataCodec.encodeAndAddMetadataWithCompression(carrier, alloc, key, + ByteBufUtil.writeUtf8(alloc, value)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java new file mode 100644 index 000000000..5bc960a3e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java @@ -0,0 +1,42 @@ +/* + * 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.rsocket; + +import io.netty.buffer.ByteBuf; +import io.rsocket.metadata.CompositeMetadata; + +import org.springframework.lang.Nullable; + +final class CompositeMetadataUtils { + + private CompositeMetadataUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + @Nullable + static ByteBuf extract(ByteBuf metadata, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + final String entryKey = entry.getMimeType(); + if (key.equals(entryKey)) { + return entry.getContent(); + } + } + return null; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java new file mode 100644 index 000000000..17363acc4 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java @@ -0,0 +1,68 @@ +/* + * 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.rsocket; + +import java.util.HashSet; +import java.util.Set; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; + +final class PayloadUtils { + + private PayloadUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static Payload cleanTracingMetadata(Payload payload, Set fields) { + Set fieldsWithDefaultZipkin = new HashSet<>(fields); + fieldsWithDefaultZipkin.add(WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + final CompositeMetadata entries = new CompositeMetadata(payload.metadata(), true); + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + for (Entry entry : entries) { + if (!fieldsWithDefaultZipkin.contains(entry.getMimeType())) { + CompositeMetadataCodec.encodeAndAddMetadataWithCompression(metadata, ByteBufAllocator.DEFAULT, + entry.getMimeType(), entry.getContent()); + } + } + return payload(payload, metadata); + } + + private static Payload payload(Payload payload, CompositeByteBuf metadata) { + final Payload newPayload; + try { + if (payload instanceof ByteBufPayload) { + newPayload = ByteBufPayload.create(payload.data().retain(), metadata.retain()); + } + else { + newPayload = DefaultPayload.create(payload.data().retain(), metadata.retain()); + } + } + finally { + payload.release(); + } + return newPayload; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java new file mode 100644 index 000000000..b172c0640 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java @@ -0,0 +1,49 @@ +/* + * 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.rsocket; + +import io.rsocket.core.RSocketConnector; +import io.rsocket.plugins.RSocketInterceptor; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; + +public class TracingRSocketConnectorConfigurer implements RSocketConnectorConfigurer { + + private final Propagator propagator; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, boolean isZipkinPropagationEnabled) { + this.propagator = propagator; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void configure(RSocketConnector rSocketConnector) { + rSocketConnector.interceptors(ir -> ir + .forResponder((RSocketInterceptor) rSocket -> new TracingResponderRSocketProxy(rSocket, this.propagator, + new ByteBufGetter(), this.tracer, this.isZipkinPropagationEnabled)) + .forRequester((RSocketInterceptor) rSocket -> new TracingRequesterRSocketProxy(rSocket, this.propagator, + new ByteBufSetter(), this.tracer, this.isZipkinPropagationEnabled))); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java new file mode 100644 index 000000000..809671eec --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java @@ -0,0 +1,49 @@ +/* + * 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.rsocket; + +import io.rsocket.core.RSocketServer; +import io.rsocket.plugins.RSocketInterceptor; + +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; + +public class TracingRSocketServerCustomizer implements RSocketServerCustomizer { + + private final Propagator propagator; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, boolean isZipkinPropagationEnabled) { + this.propagator = propagator; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void customize(RSocketServer rSocketServer) { + rSocketServer.interceptors(ir -> ir + .forResponder((RSocketInterceptor) rSocket -> new TracingResponderRSocketProxy(rSocket, propagator, + new ByteBufGetter(), this.tracer, this.isZipkinPropagationEnabled)) + .forRequester((RSocketInterceptor) rSocket -> new TracingRequesterRSocketProxy(rSocket, propagator, + new ByteBufSetter(), tracer, isZipkinPropagationEnabled))); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java new file mode 100644 index 000000000..31d0b9a96 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java @@ -0,0 +1,168 @@ +/* + * 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.rsocket; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.function.Function; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.internal.EncodingUtils; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Tracing representation of a {@link RSocketProxy} for the requester. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class TracingRequesterRSocketProxy extends RSocketProxy { + + private static final Log log = LogFactory.getLog(TracingRequesterRSocketProxy.class); + + private final Propagator propagator; + + private final Propagator.Setter setter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRequesterRSocketProxy(RSocket source, Propagator propagator, + Propagator.Setter setter, Tracer tracer, boolean isZipkinPropagationEnabled) { + super(source); + this.propagator = propagator; + this.setter = setter; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public Mono fireAndForget(Payload payload) { + return setSpan(super::fireAndForget, payload, FrameType.REQUEST_FNF); + } + + @Override + public Mono requestResponse(Payload payload) { + return setSpan(super::requestResponse, payload, FrameType.REQUEST_RESPONSE); + } + + Mono setSpan(Function> input, Payload payload, FrameType frameType) { + return Mono.deferContextual(contextView -> { + Span.Builder spanBuilder = spanBuilder(contextView); + ByteBuf extracted = CompositeMetadataUtils.extract(payload.sliceMetadata(), + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + // TODO: do sth about extracted == null, log that tracing can't be used or sth + final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); + final Iterator iterator = routingMetadata.iterator(); + String route = iterator.next(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(frameType.name() + " " + route).start(); + span.tag("rsocket.route", route); + span.tag("rsocket.request-type", frameType.name()); + if (log.isDebugEnabled()) { + log.debug("Extracted result from context or thread local " + span); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + TraceContext traceContext = span.context(); + if (this.isZipkinPropagationEnabled) { + injectDefaultZipkinRSocketHeaders(newPayload, traceContext); + } + this.propagator.inject(traceContext, (CompositeByteBuf) newPayload.metadata(), this.setter); + return input.apply(newPayload).doOnError(span::error).doFinally(signalType -> span.end()); + }); + } + + private void injectDefaultZipkinRSocketHeaders(Payload newPayload, TraceContext traceContext) { + TracingMetadataCodec.Flags flags = traceContext.sampled() == null ? TracingMetadataCodec.Flags.UNDECIDED + : traceContext.sampled() ? TracingMetadataCodec.Flags.SAMPLE : TracingMetadataCodec.Flags.NOT_SAMPLE; + String traceId = traceContext.traceId(); + long[] traceIds = EncodingUtils.fromString(traceId); + long[] spanId = EncodingUtils.fromString(traceContext.spanId()); + long[] parentSpanId = EncodingUtils.fromString(traceContext.parentId()); + boolean isTraceId128Bit = traceIds.length == 2; + if (isTraceId128Bit) { + TracingMetadataCodec.encode128(newPayload.metadata().alloc(), traceIds[0], traceIds[1], spanId[0], + EncodingUtils.fromString(traceContext.parentId())[0], flags); + } + else { + TracingMetadataCodec.encode64(newPayload.metadata().alloc(), traceIds[0], spanId[0], parentSpanId[0], + flags); + } + } + + private Span.Builder spanBuilder(ContextView contextView) { + Span.Builder spanBuilder = this.tracer.spanBuilder(); + if (contextView.hasKey(TraceContext.class)) { + spanBuilder = spanBuilder.setParent(contextView.get(TraceContext.class)); + } + else if (this.tracer.currentSpan() != null) { + spanBuilder = spanBuilder.setParent(this.tracer.currentSpan().context()); + } + return spanBuilder; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.deferContextual(contextView -> setSpan(super::requestStream, payload, contextView)); + } + + @Override + public Flux requestChannel(Publisher inbound) { + return Flux.from(inbound).switchOnFirst((firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + return setSpan(p -> super.requestChannel(flux.skip(1).startWith(p)), firstPayload, + firstSignal.getContextView()); + } + return flux; + }); + } + + Flux setSpan(Function> input, Payload payload, ContextView contextView) { + Span.Builder spanBuilder = spanBuilder(contextView); + final RoutingMetadata routingMetadata = new RoutingMetadata(CompositeMetadataUtils + .extract(payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())); + final Iterator iterator = routingMetadata.iterator(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(iterator.next()).start(); + if (log.isDebugEnabled()) { + log.debug("Extracted result from context or thread local " + span); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + this.propagator.inject(span.context(), (CompositeByteBuf) newPayload.metadata(), this.setter); + return input.apply(newPayload).doOnError(span::error).doFinally(signalType -> span.end()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java new file mode 100644 index 000000000..b75a4aff9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -0,0 +1,170 @@ +/* + * 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.rsocket; + +import java.util.HashSet; +import java.util.Iterator; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.WithThreadLocalSpan; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.cloud.sleuth.internal.EncodingUtils; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class TracingResponderRSocketProxy extends RSocketProxy implements WithThreadLocalSpan { + + private static final Log log = LogFactory.getLog(TracingResponderRSocketProxy.class); + + private final Propagator propagator; + + private final Propagator.Getter getter; + + private final Tracer tracer; + + private final ThreadLocalSpan threadLocalSpan; + + private final boolean isZipkinPropagationEnabled; + + public TracingResponderRSocketProxy(RSocket source, Propagator propagator, Propagator.Getter getter, + Tracer tracer, boolean isZipkinPropagationEnabled) { + super(source); + this.propagator = propagator; + this.getter = getter; + this.tracer = tracer; + this.threadLocalSpan = new ThreadLocalSpan(tracer); + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public Mono fireAndForget(Payload payload) { + // called on Netty EventLoop + // there can't be trace context in thread local here + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_FNF); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedMono(this.tracer, handle, () -> super.fireAndForget(newPayload)); + } + + @Override + public Mono requestResponse(Payload payload) { + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_RESPONSE); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedMono(this.tracer, handle, () -> super.requestResponse(newPayload)); + } + + @Override + public Flux requestStream(Payload payload) { + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_STREAM); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedFlux(this.tracer, handle, () -> super.requestStream(newPayload)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).switchOnFirst((firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + Span handle = consumerSpanBuilder(firstPayload.sliceMetadata(), FrameType.REQUEST_CHANNEL); + if (handle == null) { + return super.requestChannel(flux); + } + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(firstPayload, + new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedFlux(this.tracer, handle, + () -> super.requestChannel(flux.skip(1).startWith(newPayload))); + } + return flux; + }); + } + + private Span consumerSpanBuilder(ByteBuf headers, FrameType requestType) { + Span.Builder consumerSpanBuilder = consumerSpanBuilder(headers); + if (log.isDebugEnabled()) { + log.debug("Extracted result from headers " + consumerSpanBuilder); + } + final ByteBuf extract = CompositeMetadataUtils.extract(headers, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + String name = "handle"; + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + name = requestType.name() + " " + iterator.next(); + } + return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + } + + private Span.Builder consumerSpanBuilder(ByteBuf headers) { + if (this.isZipkinPropagationEnabled) { + ByteBuf extract = CompositeMetadataUtils.extract(headers, + WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + if (extract != null) { + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); + Span.Builder builder = this.tracer.spanBuilder(); + TraceContext.Builder parentBuilder = this.tracer.traceContextBuilder() + .sampled(tracingMetadata.isSampled()).traceId(EncodingUtils.fromLong(tracingMetadata.traceId())) + .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); + return builder.setParent(parentBuilder.build()); + } + else { + return this.propagator.extract(headers, this.getter); + } + } + return this.propagator.extract(headers, this.getter); + } + + @Override + public ThreadLocalSpan getThreadLocalSpan() { + return this.threadLocalSpan; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java new file mode 100644 index 000000000..f1fcf6f82 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java @@ -0,0 +1,322 @@ +/* + * 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.internal; + +import java.util.Arrays; + +import org.springframework.util.Assert; + +/** + * Adopted from OpenTelemetry API. + * + * @since 3.1.0 + */ +public final class EncodingUtils { + + private static final ThreadLocal charBuffer = new ThreadLocal(); + + private EncodingUtils() { + } + + static final int LONG_BYTES = Long.SIZE / Byte.SIZE; + + static final int BYTE_BASE16 = 2; + + static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES; + + private static final String ALPHABET = "0123456789abcdef"; + + private static final int ASCII_CHARACTERS = 128; + + private static final char[] ENCODING = buildEncodingArray(); + + private static final byte[] DECODING = buildDecodingArray(); + + private static char[] buildEncodingArray() { + char[] encoding = new char[512]; + for (int i = 0; i < 256; ++i) { + encoding[i] = ALPHABET.charAt(i >>> 4); + encoding[i | 0x100] = ALPHABET.charAt(i & 0xF); + } + return encoding; + } + + private static byte[] buildDecodingArray() { + byte[] decoding = new byte[ASCII_CHARACTERS]; + Arrays.fill(decoding, (byte) -1); + for (int i = 0; i < ALPHABET.length(); i++) { + char c = ALPHABET.charAt(i); + decoding[c] = (byte) i; + } + return decoding; + } + + /** + * Returns the {@code long} value. + * @param chars the base8 or base16 representation of the {@code long}. + * @return long array from string. Either contains high and low or just low trace id + */ + public static long[] fromString(CharSequence chars) { + if (chars == null || chars.length() == 0) { + return new long[] { 0 }; + } + if (chars.length() == 32) { + long high = HexCodec.lenientLowerHexToUnsignedLong(chars, 0, 16); + long low = HexCodec.lenientLowerHexToUnsignedLong(chars, 16, 32); + return new long[] { high, low }; + } + return new long[] { HexCodec.lenientLowerHexToUnsignedLong(chars, 0, 16) }; + } + + /** + * Returns the {@code long} value whose base16 representation is stored in the first + * 16 chars of {@code chars} starting from the {@code offset}. + * @param chars the base16 representation of the {@code long}. + * @return long value from string + */ + public static long longFromBase16String(CharSequence chars) { + return longFromBase16String(chars, 0); + } + + /** + * Returns the {@code long} value whose base16 representation is stored in the first + * 16 chars of {@code chars} starting from the {@code offset}. + * @param chars the base16 representation of the {@code long}. + */ + static long longFromBase16String(CharSequence chars, int offset) { + Assert.isTrue(chars.length() >= offset + LONG_BASE16, "chars too small"); + return (decodeByte(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56 + | (decodeByte(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48 + | (decodeByte(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40 + | (decodeByte(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32 + | (decodeByte(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24 + | (decodeByte(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16 + | (decodeByte(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8 + | (decodeByte(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL); + } + + /** + * Decodes the specified two character sequence, and returns the resulting + * {@code byte}. + * @param chars the character sequence to be decoded. + * @param offset the starting offset in the {@code CharSequence}. + * @return the resulting {@code byte} + * @throws IllegalArgumentException if the input is not a valid encoded string + * according to this encoding. + */ + public static byte byteFromBase16String(CharSequence chars, int offset) { + Assert.isTrue(chars.length() >= offset + 2, "chars too small"); + return decodeByte(chars.charAt(offset), chars.charAt(offset + 1)); + } + + private static byte decodeByte(char hi, char lo) { + Assert.isTrue(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "invalid character " + lo); + Assert.isTrue(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "invalid character " + hi); + int decoded = DECODING[hi] << 4 | DECODING[lo]; + return (byte) decoded; + } + + /** + * Checks if string is valid base16. + * @param value to check + * @return {@code true} if valid base16 string + */ + public static boolean isValidBase16String(CharSequence value) { + for (int i = 0; i < value.length(); i++) { + char b = value.charAt(i); + // 48..57 && 97..102 are valid + if (!isDigit(b) && !isLowercaseHexCharacter(b)) { + return false; + } + } + return true; + } + + /** + * Converts long into string. + * @param id 64 bit + * @return string representation of the long + */ + public static String fromLong(long id) { + return fromLongs(0, id); + } + + /** + * Converts longs into string. + * @param idHigh - trace id high part + * @param idLow - trace id low part + * @return string representation of the long + */ + public static String fromLongs(long idHigh, long idLow) { + if (idHigh == 0L) { + return HexCodec.toLowerHex(idLow); + } + else { + char[] chars = getTemporaryBuffer(); + longToBase16String(idHigh, chars, 0); + longToBase16String(idLow, chars, 16); + return new String(chars); + } + } + + public static void longToBase16String(long value, char[] dest, int destOffset) { + byteToBase16((byte) ((int) (value >> 56 & 255L)), dest, destOffset); + byteToBase16((byte) ((int) (value >> 48 & 255L)), dest, destOffset + 2); + byteToBase16((byte) ((int) (value >> 40 & 255L)), dest, destOffset + 4); + byteToBase16((byte) ((int) (value >> 32 & 255L)), dest, destOffset + 6); + byteToBase16((byte) ((int) (value >> 24 & 255L)), dest, destOffset + 8); + byteToBase16((byte) ((int) (value >> 16 & 255L)), dest, destOffset + 10); + byteToBase16((byte) ((int) (value >> 8 & 255L)), dest, destOffset + 12); + byteToBase16((byte) ((int) (value & 255L)), dest, destOffset + 14); + } + + public static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 255; + dest[destOffset] = ENCODING[b]; + dest[destOffset + 1] = ENCODING[b | 256]; + } + + private static char[] getTemporaryBuffer() { + char[] chars = charBuffer.get(); + if (chars == null) { + chars = new char[32]; + charBuffer.set(chars); + } + return chars; + } + + private static boolean isLowercaseHexCharacter(char b) { + return 97 <= b && b <= 102; + } + + private static boolean isDigit(char b) { + return 48 <= b && b <= 57; + } + +} + +// taken from brave.internal.codec.HexCodec +final class HexCodec { + + private HexCodec() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + /** + * Parses a 16 character lower-hex string with no prefix into an unsigned long, + * starting at the specified index. + * + * This reads a trace context a sequence potentially larger than the format. The + * use-case is reducing garbage, by re-using the input {@code value} across multiple + * parse operations. + * @param value the sequence that contains a lower-hex encoded unsigned long. + * @param beginIndex the inclusive begin index: {@linkplain CharSequence#charAt(int) + * index} of the first lower-hex character representing the unsigned long. + */ + static long lowerHexToUnsignedLong(CharSequence value, int beginIndex) { + int endIndex = Math.min(beginIndex + 16, value.length()); + long result = lenientLowerHexToUnsignedLong(value, beginIndex, endIndex); + if (result == 0) { + throw isntLowerHexLong(value); + } + return result; + } + + /** + * Like {@link #lowerHexToUnsignedLong(CharSequence, int)}, but returns zero on + * invalid input. + * @param value the sequence that contains a lower-hex encoded unsigned long. + * @param beginIndex the inclusive begin index: {@linkplain CharSequence#charAt(int) + * index} of the first lower-hex character representing the unsigned long. + * @param endIndex the exclusive end index: {@linkplain CharSequence#charAt(int) + * index} after the last lower-hex character representing the unsigned long. + */ + static long lenientLowerHexToUnsignedLong(CharSequence value, int beginIndex, int endIndex) { + long result = 0; + int pos = beginIndex; + while (pos < endIndex) { + char c = value.charAt(pos++); + result <<= 4; + if (c >= '0' && c <= '9') { + result |= c - '0'; + } + else if (c >= 'a' && c <= 'f') { + result |= c - 'a' + 10; + } + else { + return 0; + } + } + return result; + } + + static NumberFormatException isntLowerHexLong(CharSequence lowerHex) { + throw new NumberFormatException(lowerHex + " should be a 1 to 32 character lower-hex string with no prefix"); + } + + /** Inspired by {@code okio.Buffer.writeLong}. */ + static String toLowerHex(long v) { + char[] data = RecyclableBuffers.parseBuffer(); + writeHexLong(data, 0, v); + return new String(data, 0, 16); + } + + /** Inspired by {@code okio.Buffer.writeLong}. */ + static void writeHexLong(char[] data, int pos, long v) { + writeHexByte(data, pos + 0, (byte) ((v >>> 56L) & 0xff)); + writeHexByte(data, pos + 2, (byte) ((v >>> 48L) & 0xff)); + writeHexByte(data, pos + 4, (byte) ((v >>> 40L) & 0xff)); + writeHexByte(data, pos + 6, (byte) ((v >>> 32L) & 0xff)); + writeHexByte(data, pos + 8, (byte) ((v >>> 24L) & 0xff)); + writeHexByte(data, pos + 10, (byte) ((v >>> 16L) & 0xff)); + writeHexByte(data, pos + 12, (byte) ((v >>> 8L) & 0xff)); + writeHexByte(data, pos + 14, (byte) (v & 0xff)); + } + + static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + static void writeHexByte(char[] data, int pos, byte b) { + data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf]; + data[pos + 1] = HEX_DIGITS[b & 0xf]; + } + +} + +// taken from brave +final class RecyclableBuffers { + + private RecyclableBuffers() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + private static final ThreadLocal PARSE_BUFFER = new ThreadLocal<>(); + + /** + * Returns a {@link ThreadLocal} reused {@code char[]} for use when decoding bytes + * into an ID hex string. The buffer should be immediately copied into a + * {@link String} after decoding within the same method. + */ + static char[] parseBuffer() { + char[] idBuffer = PARSE_BUFFER.get(); + if (idBuffer == null) { + idBuffer = new char[32 + 1 + 16 + 3 + 16]; // traceid128-spanid-1-parentid + PARSE_BUFFER.set(idBuffer); + } + return idBuffer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java index 295d2288d..0b56fc984 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java @@ -85,6 +85,11 @@ class SimpleTracer implements Tracer { return null; } + @Override + public TraceContext.Builder traceContextBuilder() { + return null; + } + @Override public Map getAllBaggage() { return new HashMap<>(); diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java new file mode 100644 index 000000000..3472e237f --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java @@ -0,0 +1,43 @@ +/* + * 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.internal; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class EncodingUtilsTests { + + @Test + void should_convert_back_and_forth_with_64bits() { + long[] fromString = EncodingUtils.fromString("7c6239a5ad0a4287"); + BDDAssertions.then(fromString).hasSize(1); + + String fromLong = EncodingUtils.fromLong(fromString[0]); + + BDDAssertions.then(fromLong).isEqualTo("7c6239a5ad0a4287"); + } + + @Test + void should_convert_back_and_forth_with_128bits() { + long[] fromString = EncodingUtils.fromString("596e1787feb110407c6239a5ad0a4287"); + BDDAssertions.then(fromString).hasSize(2); + String fromLong = EncodingUtils.fromLongs(fromString[0], fromString[1]); + + BDDAssertions.then(fromLong).isEqualTo("596e1787feb110407c6239a5ad0a4287"); + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 621d49fe6..b742ec657 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -54,6 +54,7 @@ spring-cloud-sleuth-instrumentation-scheduling-tests spring-cloud-sleuth-instrumentation-task-tests spring-cloud-sleuth-instrumentation-webflux-tests + spring-cloud-sleuth-instrumentation-rsocket-tests spring-cloud-sleuth-zipkin-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml new file mode 100644 index 000000000..5df34188c --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-rsocket-tests + jar + Spring Cloud Sleuth Brave RSocket Instrumentation Tests + Spring Cloud Sleuth Brave RSocket Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + ${project.groupId} + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-rsocket + + + 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 + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java new file mode 100644 index 000000000..4cd869a93 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java @@ -0,0 +1,53 @@ +/* + * 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.instrument.rsocket; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TraceRSocketTests extends org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests { + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + org.springframework.cloud.sleuth.test.TestSpanHandler testSpanHandlerSupplier( + brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml new file mode 100644 index 000000000..c5c54bb21 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 16cb95b52..7c1fb98de 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -1,230 +1,235 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-tests-common - jar - Spring Cloud Sleuth Tests Common - Spring Cloud Sleuth Tests Common - - - org.springframework.cloud - spring-cloud-sleuth-tests - 3.1.0-SNAPSHOT - .. - - - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - - - org.springframework.boot - spring-boot-starter-test - compile - - - org.awaitility - awaitility - compile - - - com.squareup.okhttp3 - mockwebserver - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.boot - spring-boot-starter-websocket - true - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-starter-web - true - - - org.springframework.boot - spring-boot-starter-webflux - true - - - org.springframework.cloud - spring-cloud-starter-gateway - true - - - org.springframework.cloud - spring-cloud-starter-task - true - - - org.springframework.cloud - spring-cloud-starter-circuitbreaker-resilience4j - true - - - org.springframework.cloud - spring-cloud-starter-circuitbreaker-reactor-resilience4j - true - - - org.springframework.boot - spring-boot-starter-quartz - true - - - org.springframework.cloud - spring-cloud-starter-openfeign - true - - - io.github.openfeign - feign-okhttp - true - - - org.springframework.cloud - spring-cloud-starter-loadbalancer - true - - - org.springframework.cloud - spring-cloud-config-server - true - - - org.apache.httpcomponents - httpclient - true - - - org.springframework.cloud - spring-cloud-sleuth-autoconfigure - true - - - org.springframework.cloud - spring-cloud-sleuth-brave - true - - - io.zipkin.brave - brave-tests - true - - - io.projectreactor.kafka - reactor-kafka - true - - - org.testcontainers - testcontainers - true - - - org.testcontainers - junit-jupiter - true - - - org.testcontainers - kafka - true - - - org.springframework.cloud - spring-cloud-sleuth-zipkin - true - - - io.zipkin.zipkin2 - zipkin - true - - - io.zipkin.reporter2 - zipkin-reporter - true - - - io.zipkin.reporter2 - zipkin-reporter-brave - true - - - io.zipkin.reporter2 - zipkin-sender-kafka - true - - - - org.apache.kafka - kafka-clients - - - - - io.zipkin.reporter2 - zipkin-sender-activemq-client - true - - - org.apache.activemq - activemq-client - - - - - org.apache.activemq - activemq-client - true - - - io.zipkin.reporter2 - zipkin-sender-amqp-client - true - - - - com.rabbitmq - amqp-client - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-tests-common + jar + Spring Cloud Sleuth Tests Common + Spring Cloud Sleuth Tests Common + + + org.springframework.cloud + spring-cloud-sleuth-tests + 3.1.0-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-sleuth-instrumentation + + + org.springframework.boot + spring-boot-starter-test + compile + + + org.awaitility + awaitility + compile + + + com.squareup.okhttp3 + mockwebserver + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-web + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + org.springframework.cloud + spring-cloud-starter-task + true + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-resilience4j + true + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + true + + + org.springframework.boot + spring-boot-starter-quartz + true + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + io.github.openfeign + feign-okhttp + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + + + org.springframework.cloud + spring-cloud-config-server + true + + + org.apache.httpcomponents + httpclient + true + + + org.springframework.cloud + spring-cloud-sleuth-autoconfigure + true + + + org.springframework.cloud + spring-cloud-sleuth-brave + true + + + io.zipkin.brave + brave-tests + true + + + io.projectreactor.kafka + reactor-kafka + true + + + org.testcontainers + testcontainers + true + + + org.testcontainers + junit-jupiter + true + + + org.testcontainers + kafka + true + + + org.springframework.boot + spring-boot-starter-rsocket + true + + + org.springframework.cloud + spring-cloud-sleuth-zipkin + true + + + io.zipkin.zipkin2 + zipkin + true + + + io.zipkin.reporter2 + zipkin-reporter + true + + + io.zipkin.reporter2 + zipkin-reporter-brave + true + + + io.zipkin.reporter2 + zipkin-sender-kafka + true + + + + org.apache.kafka + kafka-clients + + + + + io.zipkin.reporter2 + zipkin-sender-activemq-client + true + + + org.apache.activemq + activemq-client + + + + + org.apache.activemq + activemq-client + true + + + io.zipkin.reporter2 + zipkin-sender-amqp-client + true + + + + com.rabbitmq + amqp-client + + + + + + + diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java index e27a43140..8a954219e 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java @@ -88,7 +88,7 @@ public abstract class ConfigServerIntegrationTests { void call(int port) { log.info("Sending request"); - String result = new RestTemplate().getForObject("http://localhost:" + port + "/master/application.yml", + String result = new RestTemplate().getForObject("http://localhost:" + port + "/foo/default/main", String.class); log.info("Got [\n" + result + "\n]"); } diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java new file mode 100644 index 000000000..556b4ff54 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -0,0 +1,363 @@ +/* + * 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.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import brave.Span; +import brave.Tracer; +import brave.test.TestSpanHandler; +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + then(spans).hasSize(1); + // TODO: Preferred option would be : [api.c2.{name}] + then(spans.get(0).name()).isEqualTo(frameType.name() + " " + path); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceIdString()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +}