diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 3425dd37b..5f575c7e0 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -10,6 +10,7 @@ |spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. |spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. |spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. +|spring.sleuth.cassandra.enabled | `true` | Enable Cassandra instrumentation. |spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. |spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. |spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc index 2ded3abf3..bca12b758 100644 --- a/docs/src/main/asciidoc/_spans.adoc +++ b/docs/src/main/asciidoc/_spans.adoc @@ -85,6 +85,31 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |batch.step.type|Type of the Spring Batch job. |=== +=== Cassandra Span + +> Span created around CqlSession executions. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan` + +IMPORTANT: All tags and events must be prefixed with `cassandra.` prefix! + +.Tag Keys +|=== +|Name | Description +|cassandra.cql|A tag containing Cassandra CQL. +|cassandra.keyspace|Name of the Cassandra keyspace. +|cassandra.node[%s].error|A tag containing error that occurred for the given node. (since the name contains `%s` the final value will be resolved at runtime) +|=== + +.Event Values +|=== +|Name | Description +|cassandra.node.error|Set whenever an error occurred for the given node. +|cassandra.node.success|Set when a success occurred for the session processing. +|=== + === Circuit Breaker Function Span > Span created when we wrap a Function passed to the CircuitBreaker. as fallback. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index a0d24f277..9e1cb711d 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -665,6 +665,13 @@ This feature is available for all tracer implementations. We're adding an instrumented Tomcat's `Valve` that originates the span. In order to disable this instrumentation set `spring.sleuth.web.tomcat.enabled` to `false`. +[[sleuth-cassandra-integration]] +== Spring Data Cassandra + +This feature is available for all tracer implementations. + +We're instrumenting Casandra's `CqlSession` and `ReactiveSession` interfaces and we're providing our own implementation of the `RequestTracker`. +In order to disable this instrumentation set `spring.sleuth.cassandra.enabled` to `false`. [[sleuth-jdbc-integration]] == Spring JDBC diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java index d4ad3b7ff..0f9f47f23 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java @@ -132,6 +132,12 @@ public interface AssertingSpan extends Span { return this; } + @Override + default Span remoteIpAndPort(String ip, int port) { + getDelegate().remoteIpAndPort(ip, port); + return this; + } + /** * @param documentedSpan span configuration * @param span span to wrap in assertions diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java index 0363928d2..de951bdc4 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java @@ -93,6 +93,12 @@ public interface AssertingSpanBuilder extends Span.Builder { return this; } + @Override + default Span.Builder remoteIpAndPort(String ip, int port) { + getDelegate().remoteIpAndPort(ip, port); + return this; + } + @Override default AssertingSpanBuilder setParent(TraceContext context) { getDelegate().setParent(context); diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index de8c4d1b1..cecaa8ace 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -123,6 +123,16 @@ spring-cloud-starter-task true + + org.springframework.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + true + org.springframework.cloud spring-cloud-deployer-spi diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java new file mode 100644 index 000000000..ca89e870a --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * 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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSessionBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Cassandra. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.cassandra.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore(CassandraAutoConfiguration.class) +@ConditionalOnClass(CqlSession.class) +public class TraceCassandraAutoConfiguration { + + @Bean + static TraceCqlSessionBeanPostProcessor traceCqlSessionBeanPostProcessor(BeanFactory beanFactory) { + return new TraceCqlSessionBeanPostProcessor(beanFactory); + } + + @Bean + TraceCqlSessionBuilderCustomizer traceCqlSessionBuilderCustomizer() { + return new TraceCqlSessionBuilderCustomizer(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java new file mode 100644 index 000000000..331fe53b4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.cassandra; + +import reactor.core.publisher.Flux; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Cassandra. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnClass({ ReactiveSession.class, Flux.class }) +@ConditionalOnProperty(value = "spring.sleuth.cassandra.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ CassandraAutoConfiguration.class, CassandraReactiveDataAutoConfiguration.class }) +public class TraceCassandraReactiveAutoConfiguration { + + @Bean + static TraceReactiveSessionBeanPostProcessor traceReactiveSessionBeanPostProcessor(BeanFactory beanFactory) { + return new TraceReactiveSessionBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java new file mode 100644 index 000000000..26cb74abc --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java @@ -0,0 +1,58 @@ +/* + * 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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSession; + +/** + * {@link BeanPostProcessor} to wrap a {@link CqlSession} instance into its trace + * representation. + * + * @author Marcin Grzejszczak + * @author Mark Paluch + * @since 3.1.0 + */ +public class TraceCqlSessionBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceCqlSessionBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof CqlSession) { + return create((CqlSession) bean); + } + return bean; + } + + private CqlSession create(CqlSession session) { + return TraceCqlSession.create(session, this.beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java new file mode 100644 index 000000000..ddf6e74d0 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.cassandra; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceReactiveSession; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * {@link BeanPostProcessor} to wrap a + * {@link org.springframework.data.cassandra.ReactiveSession} instance into its trace + * representation. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveSessionBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceReactiveSessionBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof ReactiveSession) { + return create((ReactiveSession) bean); + } + return bean; + } + + private ReactiveSession create(ReactiveSession session) { + return TraceReactiveSession.create(session, this.beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1cfec09f1..fac014bc6 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -191,6 +191,12 @@ "description": "Enable Spring Session instrumentation.", "defaultValue": true }, + { + "name": "spring.sleuth.cassandra.enabled", + "type": "java.lang.Boolean", + "description": "Enable Cassandra instrumentation.", + "defaultValue": true + }, { "name": "spring.sleuth.vault.enabled", "type": "java.lang.Boolean", 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 f859d669d..13decf094 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 @@ -6,6 +6,8 @@ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfi org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.cassandra.TraceCassandraAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.cassandra.TraceCassandraReactiveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java index 730c7a278..b245cfcaa 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -63,7 +64,7 @@ public class BraveRpcAutoConfigurationIntegrationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) @Configuration(proxyBeanMethods = false) public static class Config { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java index c5fcf7908..5d07ef4ad 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; @@ -152,7 +153,7 @@ public class WebClientTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) + CassandraAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) @DisableSecurity public static class TestConfiguration { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java index d96ce17c4..ca1dce937 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -54,7 +55,7 @@ public class TraceAsyncDefaultAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java new file mode 100644 index 000000000..bcf141da1 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * 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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSessionBuilderCustomizer; + +class TraceCassandraAutoConfigurationTests { + + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( + AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceCassandraAutoConfiguration.class)); + + @Test + void should_register_cassandra_tracing_beans() { + runner.run(context -> BDDAssertions.then(context).hasSingleBean(TraceCqlSessionBeanPostProcessor.class) + .hasSingleBean(TraceCqlSessionBuilderCustomizer.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_not_present() { + runner.withClassLoader(new FilteredClassLoader(CqlSession.class)) + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TraceCqlSessionBeanPostProcessor.class) + .doesNotHaveBean(TraceCqlSessionBuilderCustomizer.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_tracing_disabled() { + runner.withPropertyValues("spring.sleuth.cassandra.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TraceCqlSessionBeanPostProcessor.class) + .doesNotHaveBean(TraceCqlSessionBuilderCustomizer.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java new file mode 100644 index 000000000..68b77b955 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * 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.cassandra; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.data.cassandra.ReactiveSession; + +class TraceCassandraReactiveAutoConfigurationTests { + + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration(AutoConfigurations + .of(TraceNoOpAutoConfiguration.class, TraceCassandraReactiveAutoConfiguration.class)); + + @Test + void should_register_cassandra_tracing_beans() { + runner.run(context -> BDDAssertions.then(context).hasSingleBean(TraceReactiveSessionBeanPostProcessor.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_not_present() { + runner.withClassLoader(new FilteredClassLoader(ReactiveSession.class)).run( + context -> BDDAssertions.then(context).doesNotHaveBean(TraceReactiveSessionBeanPostProcessor.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_tracing_disabled() { + runner.withPropertyValues("spring.sleuth.cassandra.enabled=false").run( + context -> BDDAssertions.then(context).doesNotHaveBean(TraceReactiveSessionBeanPostProcessor.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java index 8ce3abc66..7265b4e5c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.autoconfigure.security.servlet.Managemen import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -120,7 +121,7 @@ public class TraceQuartzAutoConfigurationTest { exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }, excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") public static class EnableAutoConfig { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java index 016442269..2e0c9f37a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -124,7 +125,7 @@ public class BraveWebClientAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { // custom builder diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java index 0801a617d..900b3c447 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -58,7 +59,7 @@ public class GH846Tests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class App { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java index 07c6b8e7b..35c3d536a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -47,7 +48,7 @@ public class TraceWebClientDisabledTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) public static class Config { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java index b3061be84..34103f05a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -58,7 +59,7 @@ public class ZipkinSamplerTests { exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }, excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") static class TestConfig { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml index e5b38daa4..151a645e7 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml +++ b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml @@ -1,3 +1,3 @@ logging.level.org.springframework.cloud: DEBUG -spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration, org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index fd7a5aa4d..4af8bedbc 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -167,6 +167,16 @@ spring-cloud-starter-task true + + org.springframework.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + true + org.springframework.cloud spring-cloud-deployer-spi diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java new file mode 100644 index 000000000..6d7b98d14 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018-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.cassandra; + +/** + * Provides additional customization for a Cassandra span. Used internally - do not + * implement. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface CassandraSpanCustomizer { + + /** + * Provides additional customization for a Cassandra span. + * @param defaultName default name of the span + */ + void customizeSpan(String defaultName); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java new file mode 100644 index 000000000..ab5115a78 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018-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.cassandra; + +import org.springframework.cloud.sleuth.Span; + +/** + * Returns the Cassandra span. + * + * Used internally - do not implement. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface CassandraSpanSupplier { + + /** + * @return span + */ + Span getSpan(); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java new file mode 100644 index 000000000..d0752df51 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java @@ -0,0 +1,108 @@ +/* + * 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.cassandra; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthCassandraSpan implements DocumentedSpan { + + /** + * Span created around CqlSession executions. + */ + CASSANDRA_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + + @Override + public String prefix() { + return "cassandra."; + } + }; + + enum Tags implements TagKey { + + /** + * Name of the Cassandra keyspace. + */ + KEYSPACE_NAME { + @Override + public String getKey() { + return "cassandra.keyspace"; + } + }, + + /** + * A tag containing error that occurred for the given node. + */ + NODE_ERROR_TAG { + @Override + public String getKey() { + return "cassandra.node[%s].error"; + } + }, + + /** + * A tag containing Cassandra CQL. + */ + CQL_TAG { + @Override + public String getKey() { + return "cassandra.cql"; + } + }, + + } + + enum Events implements EventValue { + + /** + * Set whenever an error occurred for the given node. + */ + NODE_ERROR { + @Override + public String getValue() { + return "cassandra.node.error"; + } + }, + + /** + * Set when a success occurred for the session processing. + */ + NODE_SUCCESS { + @Override + public String getValue() { + return "cassandra.node.success"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java new file mode 100644 index 000000000..0c1f0f48a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018-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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; + +/** + * Factory to create a {@link TraceCqlSession}. + * + * @author Mark Paluch + * @since 3.1.0 + */ +public final class TraceCqlSession { + + private TraceCqlSession() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + public static CqlSession create(CqlSession session, BeanFactory beanFactory) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(session); + proxyFactory.addAdvice(new TraceCqlSessionInterceptor(session, beanFactory)); + proxyFactory.addInterface(CqlSession.class); + return (CqlSession) proxyFactory.getProxy(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java new file mode 100644 index 000000000..2420d27c8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018-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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSessionBuilder; + +import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer; + +/** + * Adds tracing mechanism to the {@link CqlSessionBuilder}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceCqlSessionBuilderCustomizer implements CqlSessionBuilderCustomizer { + + @Override + public void customize(CqlSessionBuilder cqlSessionBuilder) { + cqlSessionBuilder.withRequestTracker(TraceRequestTracker.INSTANCE); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java new file mode 100644 index 000000000..d06972e37 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018-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.cassandra; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.cloud.sleuth.internal.ContextUtil; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * A {@link MethodInterceptor} that wraps calls around {@link CqlSession} in a trace + * representation. This interceptor wraps statements for {@code execute} and + * {@code prepare} (including their asynchronous variants) only. Graph and reactive + * {@link CqlSession} method remain called as-is. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class TraceCqlSessionInterceptor implements MethodInterceptor { + + private static final Log log = LogFactory.getLog(TraceCqlSessionInterceptor.class); + + private final CqlSession delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + TraceCqlSessionInterceptor(CqlSession delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @Nullable + @Override + public Object invoke(@NonNull MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + Object[] args = invocation.getArguments(); + if (isContextUnusable()) { + return invocation.proceed(); + } + if (method.getName().equals("execute")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "execute", this.delegate::execute); + } + } + if (method.getName().equals("executeAsync")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "executeAsync", this.delegate::executeAsync); + } + } + if (method.getName().equals("prepare")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "prepare", + statement -> this.delegate.prepare((SimpleStatement) statement)); + } + } + if (method.getName().equals("prepareAsync")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "prepareAsync", + statement -> this.delegate.prepareAsync((SimpleStatement) statement)); + } + } + return invocation.proceed(); + } + + private static Statement createStatement(Object[] args) { + if (args[0] instanceof Statement) { + return (Statement) (args[0]); + } + else if (args[0] instanceof String && args.length == 1) { + return SimpleStatement.newInstance((String) args[0]); + } + else if (args[0] instanceof String && args.length == 2) { + String query = (String) args[0]; + return args[1] instanceof Map ? SimpleStatement.newInstance(query, (Map) args[1]) + : SimpleStatement.newInstance(query, (Object[]) args[1]); + } + throw new IllegalArgumentException(String.format("Unsupported arguments %s", Arrays.toString(args))); + } + + boolean isContextUnusable() { + return ContextUtil.isContextUnusable(this.beanFactory); + } + + private Object tracedCall(Statement statement, String defaultSpanName, Function, Object> function) { + Span span = cassandraClientSpan(); + Statement proxied = TraceStatement.isTraceStatement(statement) ? statement + : TraceStatement.createProxy(span, statement); + ((CassandraSpanCustomizer) proxied).customizeSpan(defaultSpanName); + try (CurrentTraceContext.Scope ws = currentTraceContext().maybeScope(span.context())) { + log.debug("Will execute statement"); + return function.apply(proxied); + } + } + + private Span cassandraClientSpan() { + return cassandraClientSpan(tracer().spanBuilder(), getSessionName(), getKeyspace()); + } + + static Span cassandraClientSpan(Span.Builder builder, String sessionName, Optional keyspace) { + return AssertingSpanBuilder.of(SleuthCassandraSpan.CASSANDRA_SPAN, builder).kind(Span.Kind.CLIENT) + .remoteServiceName("cassandra-" + sessionName) + .tag(SleuthCassandraSpan.Tags.KEYSPACE_NAME, keyspace.map(CqlIdentifier::asInternal).orElse("unknown")) + .start(); + } + + String getSessionName() { + return this.delegate.getContext().getSessionName(); + } + + Optional getKeyspace() { + return this.delegate.getKeyspace(); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java new file mode 100644 index 000000000..dd94e0ec3 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018-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.cassandra; + +import java.util.Map; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.data.cassandra.ReactiveResultSet; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * Tracing variant of {@link ReactiveSession}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveSession implements ReactiveSession { + + private final ReactiveSession delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + TraceReactiveSession(ReactiveSession delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + /** + * Factory method for creation of a {@link TraceReactiveSession}. + * @param session reactive session + * @param beanFactory bean factory + * @return traced representation of a {@link ReactiveSession}. + */ + public static ReactiveSession create(ReactiveSession session, BeanFactory beanFactory) { + return new TraceReactiveSession(session, beanFactory); + } + + @Override + public boolean isClosed() { + return this.delegate.isClosed(); + } + + @Override + public DriverContext getContext() { + return this.delegate.getContext(); + } + + @Override + public Mono execute(String cql) { + return execute(SimpleStatement.newInstance(cql)); + } + + @Override + public Mono execute(String cql, Object... objects) { + return execute(SimpleStatement.newInstance(cql, objects)); + } + + @Override + public Mono execute(String cql, Map map) { + return execute(SimpleStatement.newInstance(cql, map)); + } + + @Override + public Mono execute(Statement statement) { + return Mono.deferContextual(contextView -> { + Span span = ReactorSleuth.spanFromContext(tracer(), currentTraceContext(), contextView); + return this.delegate.execute(proxiedStatement(span, statement, "execute")); + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer(), context, createSpan(context))); + } + + @Override + public Mono prepare(String cql) { + return prepare(SimpleStatement.newInstance(cql)); + } + + @Override + public Mono prepare(SimpleStatement statement) { + return Mono.deferContextual(contextView -> { + Span span = ReactorSleuth.spanFromContext(tracer(), currentTraceContext(), contextView); + return this.delegate.prepare((SimpleStatement) proxiedStatement(span, statement, "prepare")); + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer(), context, createSpan(context))); + } + + private Statement proxiedStatement(Span span, Statement statement, String defaultName) { + Statement proxied = TraceStatement.createProxy(span, statement); + ((CassandraSpanCustomizer) proxied).customizeSpan(defaultName); + return proxied; + } + + @Override + public void close() { + this.delegate.close(); + } + + private Span createSpan(ContextView contextView) { + return TraceCqlSessionInterceptor.cassandraClientSpan(spanBuilder(contextView), getContext().getSessionName(), + Optional.empty() /* todo @since 3.2.2 */); + } + + private Span.Builder spanBuilder(ContextView contextView) { + Span.Builder spanBuilder = tracer().spanBuilder(); + if (contextView.hasKey(TraceContext.class)) { + return spanBuilder.setParent(contextView.get(TraceContext.class)); + } + else if (contextView.hasKey(Span.class)) { + return spanBuilder.setParent(contextView.get(Span.class).context()); + } + Span span = tracer().currentSpan(); + if (span != null) { + return spanBuilder.setParent(span.context()); + } + return spanBuilder; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java new file mode 100644 index 000000000..5b32056ce --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java @@ -0,0 +1,135 @@ +/* + * Copyright 2018-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.cassandra; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.CASSANDRA_SPAN; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Events.NODE_ERROR; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Events.NODE_SUCCESS; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Tags.NODE_ERROR_TAG; + +/** + * Trace implementation of the {@link RequestTracker}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +enum TraceRequestTracker implements RequestTracker { + + INSTANCE; + + private static final Log log = LogFactory.getLog(TraceRequestTracker.class); + + @Override + public void onSuccess(@NonNull Request request, long latencyNanos, @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + Span span = ((CassandraSpanSupplier) request).getSpan(); + if (log.isDebugEnabled()) { + log.debug("Closing span [" + span + "]"); + } + span.end(); + } + } + + @Override + public void onError(@NonNull Request request, @NonNull Throwable error, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @Nullable Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + Span span = ((CassandraSpanSupplier) request).getSpan(); + span.error(error); + if (log.isDebugEnabled()) { + log.debug("Closing span [" + span + "]"); + } + span.end(); + } + } + + @Override + public void onNodeError(@NonNull Request request, @NonNull Throwable error, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + AssertingSpan span = AssertingSpan.of(CASSANDRA_SPAN, ((CassandraSpanSupplier) request).getSpan()); + span.event(NODE_ERROR); + span.tag(String.format(NODE_ERROR_TAG.getKey(), node.getEndPoint()), error.toString()); + tryAddingRemoteIpAndPort(node, span); + if (log.isDebugEnabled()) { + log.debug("Marking node error for [" + span + "]"); + } + } + } + + @Override + public void onNodeSuccess(@NonNull Request request, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + AssertingSpan span = AssertingSpan.of(CASSANDRA_SPAN, ((CassandraSpanSupplier) request).getSpan()); + span.event(NODE_SUCCESS); + tryAddingRemoteIpAndPort(node, span); + if (log.isDebugEnabled()) { + log.debug("Marking node success for [" + span + "]"); + } + } + } + + @Override + public void onSessionReady(@NonNull Session session) { + } + + @Override + public void close() throws Exception { + + } + + private void tryAddingRemoteIpAndPort(Node node, Span span) { + try { + SocketAddress socketAddress = node.getEndPoint().resolve(); + String host; + int port; + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + host = inetSocketAddress.getHostString(); + port = inetSocketAddress.getPort(); + } + else { + host = socketAddress.toString(); + port = 0; + } + span.remoteIpAndPort(host, port); + } + catch (Exception e) { + log.debug("Exception occurred while trying to set ip and port", e); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java new file mode 100644 index 000000000..91a036878 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018-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.cassandra; + +import java.util.StringJoiner; + +import javax.annotation.Nonnull; + +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Trace implementation of a {@link Statement}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +final class TraceStatement implements MethodInterceptor { + + private final Span span; + + private Statement delegate; + + private TraceStatement(Span span, Statement delegate) { + this.span = span; + this.delegate = delegate; + } + + /** + * Creates a proxy with {@link TraceStatement} attached to it. + * @param span current span + * @param target target to proxy + * @param type of target + * @return proxied object with trace advice + */ + @SuppressWarnings("unchecked") + public static T createProxy(Span span, T target) { + ProxyFactory factory = new ProxyFactory(ClassUtils.getAllInterfaces(target)); + factory.addInterface(CassandraSpanCustomizer.class); + factory.addInterface(CassandraSpanSupplier.class); + factory.setTarget(target); + factory.addAdvice(new TraceStatement(span, (Statement) target)); + return (T) factory.getProxy(); + } + + /** + * @param statement target statement to inspect + * @return whether the given {@code statement} is a traced statement wrapper + */ + public static boolean isTraceStatement(Statement statement) { + return statement instanceof CassandraSpanCustomizer && statement instanceof CassandraSpanSupplier; + } + + /** + * @return stored span + */ + public Span getSpan() { + return this.span; + } + + /** + * Tries to parse the CQL query or provides the default name. + * @param defaultName if there's not query + * @return span name + */ + public String getSpanName(String defaultName) { + String query = getCql(); + if (query.indexOf(' ') > -1) { + return query.substring(0, query.indexOf(' ')); + } + return defaultName; + } + + private String getCql() { + String query = ""; + if (this.delegate instanceof SimpleStatement) { + query = getQuery(this.delegate); + } + else if (this.delegate instanceof BoundStatement) { + query = getQuery(this.delegate); + } + else if (this.delegate instanceof BatchStatement) { + StringJoiner joiner = new StringJoiner(";"); + for (BatchableStatement bs : (BatchStatement) this.delegate) { + joiner.add(getQuery(bs)); + } + query = joiner.toString(); + } + return query; + } + + private static String getQuery(Statement statement) { + if (statement instanceof SimpleStatement) { + return ((SimpleStatement) statement).getQuery(); + } + else if (statement instanceof BoundStatement) { + return ((BoundStatement) statement).getPreparedStatement().getQuery(); + } + return ""; + } + + @Nullable + @Override + public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable { + if (invocation.getMethod().getName().equals("getSpan")) { + return this.span; + } + if (invocation.getMethod().getName().equals("customizeSpan")) { + AssertingSpan.of(SleuthCassandraSpan.CASSANDRA_SPAN, this.span) + .name(getSpanName((String) invocation.getArguments()[0])) + .tag(SleuthCassandraSpan.Tags.CQL_TAG, getCql()); + return null; + } + Object result = invocation.proceed(); + if (result instanceof Statement) { + this.delegate = (Statement) result; + } + return result; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java index 52fe0ae6b..4e592d673 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java @@ -30,6 +30,7 @@ public interface TraceListenerStrategySpanCustomizer /** * Customizes the client database span. + * @param dataSource data source for which we're building the span * @param spanBuilder span builder */ void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); 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 8b5e99e3f..3ed8f2faf 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 @@ -35,6 +35,7 @@ import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.util.context.Context; +import reactor.util.context.ContextView; import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.Span; @@ -544,6 +545,7 @@ public abstract class ReactorSleuth { * Updates the Reactor context with tracing information. Creates a new span if there * is no current span. Creates a child span if there was an entry in the context * already. + * @param tracer tracer * @param currentTraceContext current trace context * @param context Reactor context * @param childSpanName child span name when there is no span in context @@ -572,11 +574,49 @@ public abstract class ReactorSleuth { return putSpanInScope(tracer, context, span); } - private static Context putSpanInScope(Tracer tracer, Context context, Span span) { + /** + * Puts the provided span in scope and in Reactor context. + * @param tracer tracer + * @param context Reactor context + * @param span span to put in Reactor context + * @return mutated context + */ + public 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)); } + /** + * Retrieves span from Reactor context. + * @param tracer tracer + * @param currentTraceContext current trace context + * @param context context view + * @return span from Reactor context or creates a new one if missing + */ + public static Span spanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, ContextView context) { + Span span = context.getOrDefault(Span.class, null); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + return span; + } + TraceContext traceContext = context.getOrDefault(TraceContext.class, null); + if (traceContext != null) { + try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { + if (log.isDebugEnabled()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + return tracer.currentSpan(); + } + } + Span newSpan = tracer.nextSpan().start(); + if (log.isDebugEnabled()) { + log.debug("No span was found - will create a new one [" + newSpan + "]"); + } + return newSpan; + } + } class SleuthContextOperator implements Subscription, CoreSubscriber, Scannable { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java index 8fe1bc235..a692c6d5f 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java @@ -33,7 +33,11 @@ import org.springframework.lang.Nullable; */ public class HttpServletRequestWrapper implements HttpServerRequest { - /** @since 5.10 */ + /** + * Wraps the request in a tracing representation. + * @param request http request + * @return wrapped request + */ public static HttpServerRequest create(HttpServletRequest request) { return new HttpServletRequestWrapper(request); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java index 3d70cbd80..de4e30b86 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java @@ -36,8 +36,9 @@ public class HttpServletResponseWrapper implements HttpServerResponse { // not final for inner // subtype /** + * Returns the trace representation of a response. * @param caught an exception caught serving the request. - * @since 5.10 + * @return wrapped response */ public static HttpServerResponse create(@Nullable HttpServletRequest request, HttpServletResponse response, @Nullable Throwable caught) { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java new file mode 100644 index 000000000..ab5f3725a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.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.cassandra; + +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +class TraceCqlSessionBuilderCustomizerTests { + + @Test + void should_register_trace_request_tracker() { + TraceCqlSessionBuilderCustomizer customizer = new TraceCqlSessionBuilderCustomizer(); + CqlSessionBuilder builder = mock(CqlSessionBuilder.class); + + customizer.customize(builder); + + then(builder).should().withRequestTracker(isA(TraceRequestTracker.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java new file mode 100644 index 000000000..fcba0a9b0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java @@ -0,0 +1,108 @@ +/* + * 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.cassandra; + +import java.util.Optional; +import java.util.function.BiConsumer; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.AsyncCqlSession; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SyncCqlSession; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class TraceCqlSessionInterceptorTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + SimpleCurrentTraceContext simpleCurrentTraceContext = SimpleCurrentTraceContext.withTracer(this.simpleTracer); + + BeanFactory beanFactory = beanFactory(); + + @Test + void should_register_a_span_for_execute() { + assertThatTracingWorks(SyncCqlSession::execute); + } + + @Test + void should_register_a_span_for_executeAsync() { + assertThatTracingWorks(AsyncCqlSession::executeAsync); + } + + @Test + void should_register_a_span_for_prepare() { + assertThatTracingWorks(SyncCqlSession::prepare); + } + + private void assertThatTracingWorks(BiConsumer consumer) { + CqlSession session = proxied(mock(CqlSession.class)); + SimpleStatement statement = mock(SimpleStatement.class); + given(statement.getQuery()) + .willReturn("Insert into University.Student(RollNo,Name,dept,Semester) values(2,'Michael','CS', 2);"); + + consumer.accept(session, statement); + + SimpleSpan span = this.simpleTracer.getLastSpan(); + then(span).isNotNull(); + then(span.tags).containsKeys(SleuthCassandraSpan.Tags.CQL_TAG.getKey(), + SleuthCassandraSpan.Tags.KEYSPACE_NAME.getKey()); + then(span.remoteServiceName).isNotBlank(); + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("currentTraceContext", this.simpleCurrentTraceContext); + return beanFactory; + } + + private CqlSession proxied(CqlSession session) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(session); + proxyFactory.addAdvice(new TraceCqlSessionInterceptor(session, this.beanFactory) { + @Override + boolean isContextUnusable() { + return false; + } + + @Override + String getSessionName() { + return "test"; + } + + @Override + Optional getKeyspace() { + return Optional.empty(); + } + }); + proxyFactory.addInterface(CqlSession.class); + return (CqlSession) proxyFactory.getProxy(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java new file mode 100644 index 000000000..20f6f44f1 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java @@ -0,0 +1,199 @@ +/* + * 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.cassandra; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; +import org.springframework.data.cassandra.ReactiveSession; +import org.springframework.lang.NonNull; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class TraceReactiveSessionTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + SimpleCurrentTraceContext simpleCurrentTraceContext = SimpleCurrentTraceContext.withTracer(this.simpleTracer); + + @Test + void should_register_a_span_for_execute() { + ReactiveSession session = mock(ReactiveSession.class); + given(session.execute(any(Statement.class))).willReturn(Mono.empty()); + SimpleStatement statement = mock(SimpleStatement.class); + given(statement.getQuery()) + .willReturn("Insert into University.Student(RollNo,Name,dept,Semester) values(2,'Michael','CS', 2);"); + + new TraceReactiveSession(session, beanFactory()) { + @Override + public DriverContext getContext() { + return driverContext(); + } + }.execute(statement).block(Duration.ofMillis(10)); + + SimpleSpan span = this.simpleTracer.getLastSpan(); + then(span).isNotNull(); + then(span.tags).containsKeys(SleuthCassandraSpan.Tags.CQL_TAG.getKey(), + SleuthCassandraSpan.Tags.KEYSPACE_NAME.getKey()); + then(span.remoteServiceName).isNotBlank(); + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("currentTraceContext", this.simpleCurrentTraceContext); + return beanFactory; + } + + private DriverContext driverContext() { + return new DriverContext() { + @NonNull + @Override + public String getSessionName() { + return "session"; + } + + @NonNull + @Override + public DriverConfig getConfig() { + return null; + } + + @NonNull + @Override + public DriverConfigLoader getConfigLoader() { + return null; + } + + @NonNull + @Override + public Map getLoadBalancingPolicies() { + return null; + } + + @NonNull + @Override + public Map getRetryPolicies() { + return null; + } + + @NonNull + @Override + public Map getSpeculativeExecutionPolicies() { + return null; + } + + @NonNull + @Override + public TimestampGenerator getTimestampGenerator() { + return null; + } + + @NonNull + @Override + public ReconnectionPolicy getReconnectionPolicy() { + return null; + } + + @NonNull + @Override + public AddressTranslator getAddressTranslator() { + return null; + } + + @NonNull + @Override + public Optional getAuthProvider() { + return Optional.empty(); + } + + @NonNull + @Override + public Optional getSslEngineFactory() { + return Optional.empty(); + } + + @NonNull + @Override + public RequestTracker getRequestTracker() { + return null; + } + + @NonNull + @Override + public RequestThrottler getRequestThrottler() { + return null; + } + + @NonNull + @Override + public NodeStateListener getNodeStateListener() { + return null; + } + + @NonNull + @Override + public SchemaChangeListener getSchemaChangeListener() { + return null; + } + + @NonNull + @Override + public ProtocolVersion getProtocolVersion() { + return null; + } + + @NonNull + @Override + public CodecRegistry getCodecRegistry() { + return null; + } + }; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java new file mode 100644 index 000000000..ada2dcc3c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java @@ -0,0 +1,194 @@ +/* + * 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.cassandra; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.session.Request; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceRequestTrackerTests { + + @Test + void should_end_span_on_success() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + + traceRequestTracker.onSuccess(new MyRequest(span), 1L, BDDMockito.mock(DriverExecutionProfile.class), + BDDMockito.mock(Node.class), ""); + + then(span.ended).isTrue(); + } + + @Test + void should_end_span_on_error() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + + traceRequestTracker.onError(new MyRequest(span), new IllegalStateException("Foo"), 1L, + BDDMockito.mock(DriverExecutionProfile.class), BDDMockito.mock(Node.class), ""); + + then(span.ended).isTrue(); + then(span.throwable).isNotNull(); + } + + @Test + void should_customize_span_on_node_error() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + Node node = BDDMockito.mock(Node.class); + BDDMockito.given(node.getEndPoint()).willReturn(endpoint()); + + traceRequestTracker.onNodeError(new MyRequest(span), new IllegalStateException("Foo"), 1L, + BDDMockito.mock(DriverExecutionProfile.class), node, ""); + + then(span.events).contains(SleuthCassandraSpan.Events.NODE_ERROR.getValue()); + then(span.tags).containsEntry("cassandra.node[localhost/127.0.0.1:1234].error", + "java.lang.IllegalStateException: Foo"); + then(span.ip).isNotEmpty(); + then(span.port).isPositive(); + } + + @Test + void should_customize_span_on_node_success() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + Node node = BDDMockito.mock(Node.class); + BDDMockito.given(node.getEndPoint()).willReturn(endpoint()); + + traceRequestTracker.onNodeSuccess(new MyRequest(span), 1L, BDDMockito.mock(DriverExecutionProfile.class), node, + ""); + + then(span.events).contains(SleuthCassandraSpan.Events.NODE_SUCCESS.getValue()); + then(span.ip).isNotEmpty(); + then(span.port).isPositive(); + } + + private EndPoint endpoint() { + return new EndPoint() { + @NonNull + @Override + public SocketAddress resolve() { + return new InetSocketAddress("localhost", 1234); + } + + @NonNull + @Override + public String asMetricPrefix() { + return ""; + } + + @Override + public String toString() { + return resolve().toString(); + } + }; + } + +} + +class MyRequest implements Request, CassandraSpanSupplier { + + private final Span span; + + MyRequest(Span span) { + this.span = span; + } + + @Nullable + @Override + public String getExecutionProfileName() { + return null; + } + + @Nullable + @Override + public DriverExecutionProfile getExecutionProfile() { + return null; + } + + @Nullable + @Override + public CqlIdentifier getKeyspace() { + return null; + } + + @Nullable + @Override + public CqlIdentifier getRoutingKeyspace() { + return null; + } + + @Nullable + @Override + public ByteBuffer getRoutingKey() { + return null; + } + + @Nullable + @Override + public Token getRoutingToken() { + return null; + } + + @NonNull + @Override + public Map getCustomPayload() { + return null; + } + + @Nullable + @Override + public Boolean isIdempotent() { + return null; + } + + @Nullable + @Override + public Duration getTimeout() { + return null; + } + + @Nullable + @Override + public Node getNode() { + return null; + } + + @Override + public Span getSpan() { + return this.span; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java index 6c171a1d2..0c3acd5f9 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java @@ -72,4 +72,13 @@ public class SimpleCurrentTraceContext implements CurrentTraceContext { return delegate; } + public static SimpleCurrentTraceContext withTracer(SimpleTracer simpleTracer) { + return new SimpleCurrentTraceContext() { + @Override + public TraceContext context() { + return simpleTracer.currentSpan() != null ? simpleTracer.currentSpan().context() : null; + } + }; + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java index c4213fd4b..7df1f7035 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java @@ -48,6 +48,10 @@ public class SimpleSpan implements Span { public String name; + public String ip; + + public int port; + @Override public boolean isNoop() { return true; @@ -88,6 +92,13 @@ public class SimpleSpan implements Span { return this; } + @Override + public Span remoteIpAndPort(String ip, int port) { + this.ip = ip; + this.port = port; + return this; + } + @Override public void end() { this.ended = true; diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java index 300ff616a..8292a235f 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java @@ -21,6 +21,7 @@ import brave.baggage.BaggagePropagationConfig; import brave.sampler.Sampler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; @@ -75,9 +76,9 @@ public class MultipleHopsIntegrationTests } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration( - exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, JmxAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + @EnableAutoConfiguration(exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, + JmxAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, + RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { @Bean diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java index f4a41ed5c..d6912f647 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; @@ -169,9 +170,9 @@ public class JmsTracingConfigurationTest { } @Configuration(proxyBeanMethods = false) -@EnableAutoConfiguration( - exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) +@EnableAutoConfiguration(exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, + QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, + RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) class JmsTestTracingConfiguration { } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java index a51599cfd..5e449776b 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java @@ -22,6 +22,7 @@ import brave.test.TestSpanHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @@ -34,7 +35,8 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, + CassandraAutoConfiguration.class }) @ImportResource("classpath:beans/applicationContext.xml") @EnableIntegration @EnableAsync