Adding jdbc instrumentation (#1959)

fixes gh-1930
This commit is contained in:
Marcin Grzejszczak
2021-05-26 10:00:14 +02:00
committed by GitHub
parent b591e0f474
commit 190a15de57
51 changed files with 5293 additions and 89 deletions

View File

@@ -5,10 +5,10 @@
|spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads.
|spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation.
|spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts.
|spring.sleuth.baggage.correlation-fields | |
|spring.sleuth.baggage.local-fields | |
|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire.
|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire.
|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 | |
|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags.
|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation.
|spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation.
|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation.
@@ -23,6 +23,27 @@
|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation.
|spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names.
|spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query | `false` | Creates listener to count queries. @see ProxyDataSourceBuilder#countQuery() @see QueryCountHolder
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson()
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging | | Logging to use for logging queries.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline()
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.logger-name | | Name of query logger.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.logger-name | | Name of slow query logger.
|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow.
|spring.sleuth.jdbc.decorator.datasource.enabled | `true` | Enables data source decorating.
|spring.sleuth.jdbc.decorator.datasource.excluded-beans | | Beans that won't be decorated.
|spring.sleuth.jdbc.decorator.datasource.includes | | Which types of tracing we would like to include.
|spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}.
|spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging | `true` | Enables logging JDBC events.
|spring.sleuth.jdbc.decorator.datasource.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file).
|spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged.
|spring.sleuth.jdbc.decorator.datasource.p6spy.log-format | | Custom log format.
|spring.sleuth.jdbc.decorator.datasource.p6spy.logging | | Logging to use for logging queries.
|spring.sleuth.jdbc.decorator.datasource.p6spy.multiline | `true` | Enables multiline output.
|spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems. <p> NOTE this setting does not affect the logging message.
|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients.
|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled.
|spring.sleuth.messaging.enabled | `false` | Should messaging be turned on.

View File

@@ -292,6 +292,105 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in
|deployer.start|When deployer started deploying the application.
|===
=== Jdbc Connection Span
> Span created when a JDBC connection takes place.
**Span name** `connection`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.jdbc.SleuthJdbcSpan`
IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix!
.Tag Keys
|===
|Name | Description
|jdbc.datasource.driver|Name of the JDBC datasource driver.
|jdbc.datasource.pool|Name of the JDBC datasource pool.
|===
=== Jdbc Query Span
> Span created when a JDBC query gets executed.
**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.jdbc.SleuthJdbcSpan`
IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix!
.Tag Keys
|===
|Name | Description
|jdbc.query|The SQL query value.
|jdbc.row-count|Number of SQL rows.
|===
.Event Values
|===
|Name | Description
|jdbc.commit|When the transaction gets commited.
|jdbc.rollback|When the transaction gets rolled back.
|===
=== Jdbc Result Set Span
> Span created when working with JDBC result set.
**Span name** `result-set`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.jdbc.SleuthJdbcSpan`
IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix!
.Tag Keys
|===
|Name | Description
|jdbc.query|The SQL query value.
|jdbc.row-count|Number of SQL rows.
|===
.Event Values
|===
|Name | Description
|jdbc.commit|When the transaction gets commited.
|jdbc.rollback|When the transaction gets rolled back.
|===
=== Kafka Consumer Span
> Span created on the Kafka consumer side.
**Span name** `kafka.consume`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.kafka.SleuthKafkaSpan`
IMPORTANT: All tags and events must be prefixed with `kafka.` prefix!
.Tag Keys
|===
|Name | Description
|kafka.offset|Kafka offset number.
|kafka.partition|Kafka partition number.
|kafka.topic|Name of the Kafka topic.
|===
=== Kafka Producer Span
> Span created on the Kafka consumer side.
**Span name** `kafka.produce`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.kafka.SleuthKafkaSpan`
IMPORTANT: All tags and events must be prefixed with `kafka.` prefix!
.Tag Keys
|===
|Name | Description
|kafka.topic|Name of the Kafka topic.
|===
=== Messaging Span
> Span created when message is sent or received.
@@ -336,6 +435,41 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in
|quartz.trigger|Name of the trigger.
|===
=== R2dbc Query Span
> Span created on the Kafka consumer side.
**Span name** `query`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.r2dbc.SleuthR2dbcSpan`
IMPORTANT: All tags and events must be prefixed with `r2dbc.` prefix!
.Tag Keys
|===
|Name | Description
|r2dbc.connection|Name of the R2DBC connection.
|r2dbc.query[%s]|Name of the R2DBC query. (since the name contains `%s` the final value will be resolved at runtime)
|r2dbc.thread|Name of the R2DBC thread.
|===
=== Rsocket Requester Span
> Span created on the RSocket responder side.
**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.rsocket.SleuthRSocketSpan`
IMPORTANT: All tags and events must be prefixed with `rsocket.` prefix!
.Tag Keys
|===
|Name | Description
|rsocket.request-type|Name of the R2DBC thread.
|rsocket.route|Name of the RSocket route.
|===
=== Rsocket Responder Span
> Span created on the RSocket responder side.
@@ -389,6 +523,27 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.task.SleuthTaskSpan`
=== Tx Span
> Span created when there was no previous transaction. If there was one, we will continue it unless propagation is required.
**Span name** `tx`.
Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.tx.SleuthTxSpan`
IMPORTANT: All tags and events must be prefixed with `tx.` prefix!
.Tag Keys
|===
|Name | Description
|tx.isolation-level|Transaction isolation level.
|tx.name|Transaction name.
|tx.propagation-level|Transaction propagation level.
|tx.read-only|Whether the transaction is read-only.
|tx.timeout|Transaction timeout.
|tx.transaction-manager|Name of the TransactionManager.
|===
=== Web Filter Span
> Span around a WebFilter. Will continue the current span or create a new one and tag it

View File

@@ -1,22 +1,22 @@
:numbered!:
[[appendix]]
[[sleuth-spans]]
== Spring Cloud Sleuth Spans
Below you can find a list of all the spans that are created by Spring Cloud Sleuth.
include::_spans.adoc[]
[[common-application-properties]]
== Common application properties
include::_attributes.adoc[]
Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches.
This appendix provides a list of common {project-full-name} properties and references to the underlying classes that consume them.
NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list.
Also, you can define your own properties.
include::_configprops.adoc[]
:numbered!:
[[appendix]]
[[common-application-properties]]
== Common application properties
include::_attributes.adoc[]
Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches.
This appendix provides a list of common {project-full-name} properties and references to the underlying classes that consume them.
NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list.
Also, you can define your own properties.
include::_configprops.adoc[]
[[sleuth-spans]]
== Spring Cloud Sleuth Spans
Below you can find a list of all the spans that are created by Spring Cloud Sleuth.
include::_spans.adoc[]

View File

@@ -3,7 +3,7 @@
include::_attributes.adoc[]
In this section, we describe how to customize various parts of Spring Cloud Sleuth.
In this section, we describe how to customize various parts of Spring Cloud Sleuth. Please check the <<appendix.adoc#sleuth-spans,appendix>> for the list of spans, tags and events.
[[sleuth-kafka-integration]]
== Apache Kafka
@@ -650,3 +650,53 @@ 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-jdbc-integration]]
== Spring JDBC
This feature is available for all tracer implementations. It has been ported from the https://github.com/gavlyukovskiy/spring-boot-data-source-decorator/[spring-boot-datasource-decorator] project.
We're decorating `DataSource`s in a trace representation. We delegate actual proxying to either https://github.com/p6spy/p6spy[p6spy] or https://github.com/ttddyy/datasource-proxy[datasource-proxy]. In order to use this feature you need to have them on the classpath.
====
[source,xml,indent=0,subs="verbatim,attributes",role="primary"]
.P6Spy Maven
----
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${p6spy.version}</version>
<scope>runtime</scope>
</dependency>
----
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
.P6Spy Gradle
----
runtimeOnly "p6spy:p6spy:${p6spyVersion}"
----
[source,xml,indent=0,subs="verbatim,attributes",role="secondary"]
.Datasource Proxy Maven
----
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<version>${datasource-proxy.version}</version>
<scope>runtime</scope>
</dependency>
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Datasource Proxy Gradle
----
runtimeOnly "net.ttddyy:datasource-proxy:${datasourceProxyVersion}"
----
====
Please check the <<appendix.adoc#appendix,appendix>> page under `spring.sleuth.jdbc.decorator.datasource.p6spy` for all p6spy configuration options and `spring.sleuth.jdbc.decorator.datasource.datasource-proxy` for all datasource proxy configuration options.
You can configure P6Spy manually using one of available configuration methods. For more information please refer to the http://p6spy.readthedocs.io/en/latest/configandusage.html[P6Spy Configuration Guide].
In order to disable this instrumentation set `spring.sleuth.jdbc.decorator.datasource.enabled` to `false`.

View File

@@ -1,18 +1,18 @@
[[spring-cloud-sleuth-reference-documentation]]
= Spring Cloud Sleuth Reference Documentation
Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant
:docinfo: shared
include::_attributes.adoc[]
The reference documentation consists of the following sections:
[horizontal]
<<legal.adoc#legal,Legal>> :: Legal information.
<<documentation-overview.adoc#sleuth-documentation-about,Documentation Overview>> :: About the Documentation, Getting Help, First Steps, and more.
<<getting-started.adoc#getting-started,Getting Started>> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application
<<using.adoc#using,Using {project-full-name}>> :: {project-full-name} usage examples and workflows.
<<project-features.adoc#features,{project-full-name} Features>> :: Span creation, context propagation, and more.
<<howto.adoc#howto,"`How-to`" Guides>> :: Add sampling, propagate remote tags, and more.
<<integrations.adoc#sleuth-integration,{project-full-name} Integrations>> :: Instrumentation configuration, context propagation, and more.
<<appendix.adoc#appendix,Appendices>> :: Configuration properties.
[[spring-cloud-sleuth-reference-documentation]]
= Spring Cloud Sleuth Reference Documentation
Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant
:docinfo: shared
include::_attributes.adoc[]
The reference documentation consists of the following sections:
[horizontal]
<<legal.adoc#legal,Legal>> :: Legal information.
<<documentation-overview.adoc#sleuth-documentation-about,Documentation Overview>> :: About the Documentation, Getting Help, First Steps, and more.
<<getting-started.adoc#getting-started,Getting Started>> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application
<<using.adoc#using,Using {project-full-name}>> :: {project-full-name} usage examples and workflows.
<<project-features.adoc#features,{project-full-name} Features>> :: Span creation, context propagation, and more.
<<howto.adoc#howto,"`How-to`" Guides>> :: Add sampling, propagate remote tags, and more.
<<integrations.adoc#sleuth-integration,{project-full-name} Integrations>> :: Instrumentation configuration, context propagation, and more.
<<appendix.adoc#appendix,Appendices>> :: Span definitions and configuration properties.

View File

@@ -35,11 +35,13 @@ class SpanEntry implements Comparable<SpanEntry> {
final String description;
final String prefix;
final Collection<KeyValueEntry> tagKeys;
final Collection<KeyValueEntry> events;
SpanEntry(String name, String enclosingClass, String enumName, String description,
SpanEntry(String name, String enclosingClass, String enumName, String description, String prefix,
Collection<KeyValueEntry> tagKeys, Collection<KeyValueEntry> events) {
Assert.isTrue(StringUtils.hasText(name), "Span name must not be empty");
Assert.isTrue(StringUtils.hasText(description), "Span description must not be empty");
@@ -47,6 +49,7 @@ class SpanEntry implements Comparable<SpanEntry> {
this.enclosingClass = enclosingClass;
this.enumName = enumName;
this.description = description;
this.prefix = prefix;
this.tagKeys = tagKeys;
this.events = events;
}
@@ -89,6 +92,9 @@ class SpanEntry implements Comparable<SpanEntry> {
text.append(".");
}
text.append("\n\n").append("Fully qualified name of the enclosing class `").append(this.enclosingClass).append("`");
if (StringUtils.hasText(prefix)) {
text.append("\n\nIMPORTANT: All tags and events must be prefixed with `").append(this.prefix).append("` prefix!");
}
if (!tagKeys.isEmpty()) {
text.append("\n\n.Tag Keys\n|===\n|Name | Description\n").append(this.tagKeys.stream().map(KeyValueEntry::toString).collect(Collectors.joining("\n")))
.append("\n|===");

View File

@@ -99,6 +99,7 @@ class SpanSearchingFileVisitor extends SimpleFileVisitor<Path> {
}
String name = "";
String description = enumConstant.getJavaDoc().getText();
String prefix = "";
Collection<KeyValueEntry> tags = new TreeSet<>();
Collection<KeyValueEntry> events = new TreeSet<>();
for (MemberSource<EnumConstantSource.Body, ?> member : members) {
@@ -117,11 +118,12 @@ class SpanSearchingFileVisitor extends SimpleFileVisitor<Path> {
else if ("getEvents".equals(methodName)) {
events.addAll(keyValueEntries(myEnum, methodDeclaration, EventValue.class));
}
else {
return null;
else if ("prefix".equals(methodName)) {
prefix = readStringReturnValue(methodDeclaration);
}
}
return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, tags, events);
return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, prefix, tags,
events);
}
private Collection<KeyValueEntry> keyValueEntries(JavaEnumImpl myEnum, MethodDeclaration methodDeclaration,

24
pom.xml
View File

@@ -84,6 +84,10 @@
<cglib-nodep.version>3.3.0</cglib-nodep.version>
<objenesis.version>3.0.1</objenesis.version>
<spring-security-oauth2.version>2.2.0.RELEASE</spring-security-oauth2.version>
<p6spy.version>3.9.1</p6spy.version>
<datasource-proxy.version>1.7</datasource-proxy.version>
<tomcat-jdbc.version>10.0.6</tomcat-jdbc.version>
<commons-dbcp2.version>2.8.0</commons-dbcp2.version>
<!-- Until we switch it to true in sc-build -->
<javadoc.failOnError>true</javadoc.failOnError>
@@ -293,6 +297,26 @@
<artifactId>brave-propagation-aws</artifactId>
<version>${brave-propagation-aws.version}</version>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${p6spy.version}</version>
</dependency>
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<version>${datasource-proxy.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>${commons-dbcp2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>${tomcat-jdbc.version}</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>

View File

@@ -99,6 +99,17 @@ public interface Span extends SpanCustomizer {
return this;
}
/**
* Sets the remote url on the span.
* @param ip remote ip
* @param port remote port
* @return this span
* @since 3.1.0
*/
default Span remoteIpAndPort(String ip, int port) {
return this;
}
/**
* Type of span. Can be used to specify additional relationships between spans in
* addition to a parent/child relationship.
@@ -204,10 +215,11 @@ public interface Span extends SpanCustomizer {
/**
* Sets the remote URL for the span.
* @param remoteUrl remote service name
* @param ip remote service ip
* @param port remote service port
* @return this
*/
default Builder remoteUrl(String remoteUrl) {
default Builder remoteIpAndPort(String ip, int port) {
return this;
}

View File

@@ -16,13 +16,15 @@
package org.springframework.cloud.sleuth;
import java.io.Closeable;
/**
* Container object for {@link Span} and its corresponding {@link Tracer.SpanInScope}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class SpanAndScope {
public class SpanAndScope implements Closeable {
private final Span span;
@@ -46,4 +48,10 @@ public class SpanAndScope {
return "SpanAndScope{" + "span=" + this.span + '}';
}
@Override
public void close() {
this.scope.close();
this.span.end();
}
}

View File

@@ -188,7 +188,31 @@
<artifactId>spring-vault-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<optional>true</optional>
</dependency>
<!-- BRAVE -->
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

@@ -36,19 +36,24 @@ public class SleuthBaggageProperties {
private boolean correlationEnabled = true;
/**
* List of fields that should be propagated over the wire.
*/
private List<String> correlationFields = new ArrayList<>();
/**
* List of fields that should be accessible within the JVM process but not propagated
* over the wire.
*/
private List<String> localFields = new ArrayList<>();
/**
* 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.
*
*/
private List<String> remoteFields = new ArrayList<>();
/**
* List of fields that should automatically become tags.
*/
private List<String> tagFields = new ArrayList<>();

View File

@@ -0,0 +1,120 @@
/*
* 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.jdbc;
import java.util.ArrayList;
import java.util.List;
import net.ttddyy.dsproxy.listener.MethodExecutionListener;
import net.ttddyy.dsproxy.listener.QueryCountStrategy;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager;
import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory;
import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import net.ttddyy.dsproxy.transform.ParameterTransformer;
import net.ttddyy.dsproxy.transform.QueryTransformer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener;
import org.springframework.context.annotation.Bean;
/**
* Configuration for integration with datasource-proxy, allows to use define custom
* {@link QueryExecutionListener}, {@link ParameterTransformer} and
* {@link QueryTransformer}.
*
* @author Arthur Gavlyukovskiy
*/
@ConditionalOnClass(ProxyDataSource.class)
class DataSourceProxyConfiguration {
@Bean
@ConditionalOnMissingBean
DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() {
return GlobalConnectionIdManager::new;
}
@Bean
DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer(
ObjectProvider<QueryCountStrategy> queryCountStrategy,
ObjectProvider<List<QueryExecutionListener>> listeners,
ObjectProvider<List<MethodExecutionListener>> methodExecutionListeners,
ObjectProvider<ParameterTransformer> parameterTransformer,
ObjectProvider<QueryTransformer> queryTransformer,
ObjectProvider<ResultSetProxyLogicFactory> resultSetProxyLogicFactory,
ObjectProvider<DataSourceProxyConnectionIdManagerProvider> dataSourceProxyConnectionIdManagerProvider,
TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) {
return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null),
listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null),
parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null),
resultSetProxyLogicFactory.getIfAvailable(() -> null),
dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null),
props(dataSourceDecoratorProperties));
}
private DataSourceProxyProperties props(TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) {
TraceDataSourceDecoratorProperties.DataSourceProxyProperties originalProxy = dataSourceDecoratorProperties
.getDatasourceProxy();
DataSourceProxyProperties props = new DataSourceProxyProperties();
BeanUtils.copyProperties(originalProxy, props);
props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name()));
return props;
}
@Bean
DataSourceProxyDataSourceDecorator proxyDataSourceDecorator(
DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer,
DataSourceNameResolver dataSourceNameResolver) {
return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query",
havingValue = "true")
QueryCountStrategy queryCountStrategy() {
return new SingleQueryCountHolder();
}
@Bean
TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer,
TraceDataSourceDecoratorProperties dataSourceDecoratorProperties,
ObjectProvider<List<TraceListenerStrategySpanCustomizer>> customizers) {
return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(),
customizers.getIfAvailable(ArrayList::new));
}
@Bean
@ConditionalOnMissingBean
ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() {
return new SimpleResultSetProxyLogicFactory();
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.jdbc;
import java.util.ArrayList;
import java.util.List;
import com.p6spy.engine.event.JdbcEventListener;
import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import com.p6spy.engine.spy.P6DataSource;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver;
import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory;
import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
/**
* Configuration for integration with p6spy, allows to define custom
* {@link JdbcEventListener}.
*
* @author Arthur Gavlyukovskiy
*/
@ConditionalOnClass(P6DataSource.class)
class P6SpyConfiguration {
@Bean
static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) {
return new P6SpyPropertiesSetter(context);
}
@Bean
@ConditionalOnMissingBean
JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider<List<JdbcEventListener>> listeners) {
JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory();
List<JdbcEventListener> listenerList = listeners.getIfAvailable(() -> null);
return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList)
: jdbcEventListenerFactory;
}
@Bean
P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) {
return new P6SpyDataSourceDecorator(jdbcEventListenerFactory);
}
@Bean
TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver,
TraceDataSourceDecoratorProperties dataSourceDecoratorProperties,
ObjectProvider<List<TraceListenerStrategySpanCustomizer>> customizers) {
return new TraceJdbcEventListener(tracer, dataSourceNameResolver, dataSourceDecoratorProperties.getIncludes(),
dataSourceDecoratorProperties.getP6spy().getTracing().isIncludeParameterValues(),
customizers.getIfAvailable(ArrayList::new));
}
}

View File

@@ -0,0 +1,189 @@
/*
* 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.jdbc;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.p6spy.engine.logging.P6LogFactory;
import com.p6spy.engine.spy.P6ModuleManager;
import com.p6spy.engine.spy.P6SpyFactory;
import com.p6spy.engine.spy.option.EnvironmentVariables;
import com.p6spy.engine.spy.option.P6OptionsSource;
import com.p6spy.engine.spy.option.SpyDotProperties;
import com.p6spy.engine.spy.option.SystemProperties;
import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Sets p6spy properties to / from system properties.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable {
private static final Logger log = getLogger(P6SpyPropertiesSetter.class);
private final ConfigurableApplicationContext context;
private final Map<String, String> initialP6SpyOptions;
P6SpyPropertiesSetter(ConfigurableApplicationContext context) {
this.context = context;
this.initialP6SpyOptions = findDefinedOptions();
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
ConfigurableEnvironment environment = this.context.getEnvironment();
String customModuleList = initialP6SpyOptions.get("modulelist");
boolean isEnableLogging = environment
.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging", Boolean.class, true);
if (customModuleList != null) {
log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied");
}
else {
List<String> moduleList = new ArrayList<>();
// default factory, holds P6Spy configuration
moduleList.add(P6SpyFactory.class.getName());
if (isEnableLogging) {
moduleList.add(P6LogFactory.class.getName());
}
System.setProperty("p6spy.config.modulelist", String.join(",", moduleList));
}
if (!initialP6SpyOptions.containsKey("logMessageFormat")) {
String logFormat = logFormat(environment);
boolean isMultiline = multiLine(environment);
if (logFormat != null) {
System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat");
System.setProperty("p6spy.config.customLogMessageFormat", logFormat);
}
else if (isMultiline) {
System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat");
}
}
if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) {
TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging logging = TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging
.valueOf(environment
.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.logging", String.class,
TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString())
.toUpperCase());
switch (logging) {
case SYSOUT:
System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger");
break;
case SLF4J:
System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger");
break;
case FILE:
System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger");
break;
case CUSTOM:
String customAppender = environment.getProperty(
"spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class", String.class, "");
System.setProperty("p6spy.config.appender", customAppender);
break;
}
}
if (!initialP6SpyOptions.containsKey("logfile")) {
String logFile = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-file",
String.class, "spy.log");
System.setProperty("p6spy.config.logfile", logFile);
}
String pattern = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern",
String.class);
if (pattern != null) {
System.setProperty("p6spy.config.filter", "true");
System.setProperty("p6spy.config.sqlexpression", pattern);
}
// If factories were loaded before this method is initialized changing properties
// will not be applied
// Changes done in this method could not override anything user specified,
// therefore it is safe to call reload
P6ModuleManager.getInstance().reload();
}
private Boolean multiLine(ConfigurableEnvironment environment) {
return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline", Boolean.class, true);
}
private String logFormat(ConfigurableEnvironment environment) {
return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format", String.class);
}
@Override
public void close() throws IOException {
if (!initialP6SpyOptions.containsKey("modulelist")) {
System.clearProperty("p6spy.config.modulelist");
}
if (!initialP6SpyOptions.containsKey("logMessageFormat")) {
ConfigurableEnvironment environment = this.context.getEnvironment();
String logFormat = logFormat(environment);
boolean isMultiline = multiLine(environment);
if (logFormat != null) {
System.clearProperty("p6spy.config.logMessageFormat");
System.clearProperty("p6spy.config.customLogMessageFormat");
}
else if (isMultiline) {
System.clearProperty("p6spy.config.logMessageFormat");
}
}
if (!initialP6SpyOptions.containsKey("appender")) {
System.clearProperty("p6spy.config.appender");
}
if (!initialP6SpyOptions.containsKey("logfile")) {
System.clearProperty("p6spy.config.logfile");
}
P6ModuleManager.getInstance().reload();
}
private Map<String, String> findDefinedOptions() {
SpyDotProperties spyDotProperties = null;
try {
spyDotProperties = new SpyDotProperties();
}
catch (IOException ignored) {
}
return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull)
.map(P6OptionsSource::getOptions).filter(Objects::nonNull)
.flatMap(options -> options.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
// always using value from the first P6OptionsSource
(value1, value2) -> value1));
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc;
import javax.sql.DataSource;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource.
*
* @author Arthur Gavlyukovskiy
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TraceDataSourceDecoratorProperties.class)
@ConditionalOnProperty(name = "spring.sleuth.jdbc.decorator.datasource.enabled", havingValue = "true",
matchIfMissing = true)
@ConditionalOnBean({ DataSource.class, Tracer.class })
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class })
public class TraceDataSourceDecoratorAutoConfiguration {
@Bean
@ConditionalOnBean(DataSourceDecorator.class)
static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor(
TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) {
return new TraceDataSourceDecoratorBeanPostProcessor(dataSourceDecoratorProperties.getExcludedBeans());
}
@Bean
@ConditionalOnMissingBean
DataSourceNameResolver traceDataSourceNameResolver() {
return new DataSourceNameResolver();
}
@Bean
@ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource")
TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() {
return new TraceHikariListenerStrategySpanCustomizer();
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.jdbc;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.ClassUtils;
/**
* {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource}
* proxies specified in property 'spring.datasource.type'.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware {
private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class);
private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource",
DataSourceNameResolver.class.getClassLoader());
private ApplicationContext applicationContext;
private DataSourceNameResolver dataSourceNameResolver;
private final Collection<String> excludedBeans;
public TraceDataSourceDecoratorBeanPostProcessor(Collection<String> excludedBeans) {
this.excludedBeans = excludedBeans;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName)
&& !this.excludedBeans.contains(beanName)) {
// TODO: This might be a problem
Map<String, DataSourceDecorator> decorators = this.applicationContext
.getBeansOfType(DataSourceDecorator.class).entrySet().stream()
.sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new));
return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators);
}
else {
return bean;
}
}
private String getDataSourceName(Object bean, String beanName) {
if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) bean;
if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) {
return hikariDataSource.getPoolName();
}
}
return beanName;
}
private DataSource decorate(DataSource dataSource, String name, Map<String, DataSourceDecorator> decorators) {
getDataSourceNameResolver().addDataSource(name, dataSource);
DataSource decoratedDataSource = dataSource;
for (Entry<String, DataSourceDecorator> decoratorEntry : decorators.entrySet()) {
String decoratorBeanName = decoratorEntry.getKey();
DataSourceDecorator decorator = decoratorEntry.getValue();
DataSource dataSourceBeforeDecorating = decoratedDataSource;
decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource),
"DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null");
if (dataSourceBeforeDecorating != decoratedDataSource) {
getDataSourceNameResolver().addDataSource(name, decoratedDataSource);
}
}
if (dataSource != decoratedDataSource) {
if (log.isDebugEnabled()) {
log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one ["
+ dataSource + "]");
}
decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource);
getDataSourceNameResolver().addDataSource(name, decoratedDataSource);
}
return decoratedDataSource;
}
private DataSourceNameResolver getDataSourceNameResolver() {
if (this.dataSourceNameResolver == null) {
this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class);
}
return this.dataSourceNameResolver;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View File

@@ -0,0 +1,502 @@
/*
* 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.jdbc;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import net.ttddyy.dsproxy.QueryCountHolder;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceType;
/**
* Properties for configuring proxy providers.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
@ConfigurationProperties(prefix = "spring.sleuth.jdbc.decorator.datasource")
public class TraceDataSourceDecoratorProperties {
/**
* Enables data source decorating.
*/
private boolean enabled = true;
/**
* Beans that won't be decorated.
*/
private Collection<String> excludedBeans = Collections.emptyList();
/**
* Which types of tracing we would like to include.
*/
private List<TraceType> includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH);
private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties();
private P6SpyProperties p6spy = new P6SpyProperties();
public boolean isEnabled() {
return enabled;
}
public Collection<String> getExcludedBeans() {
return excludedBeans;
}
public List<TraceType> getIncludes() {
return includes;
}
public void setIncludes(List<TraceType> includes) {
this.includes = includes;
}
public DataSourceProxyProperties getDatasourceProxy() {
return datasourceProxy;
}
public P6SpyProperties getP6spy() {
return p6spy;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setExcludedBeans(Collection<String> excludedBeans) {
this.excludedBeans = excludedBeans;
}
public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) {
this.datasourceProxy = datasourceProxy;
}
public void setP6spy(P6SpyProperties p6spy) {
this.p6spy = p6spy;
}
/**
* Properties for datasource-proxy.
*/
public static class DataSourceProxyProperties {
/**
* Logging to use for logging queries.
*/
private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J;
/**
* Query configuration.
*/
private Query query = new Query();
/**
* Slow query configuration.
*/
private SlowQuery slowQuery = new SlowQuery();
/**
* Use multiline output for logging query.
*
* @see ProxyDataSourceBuilder#multiline()
*/
private boolean multiline = true;
/**
* Use json output for logging query.
*
* @see ProxyDataSourceBuilder#asJson()
*/
private boolean jsonFormat = false;
/**
* Creates listener to count queries.
*
* @see ProxyDataSourceBuilder#countQuery()
* @see QueryCountHolder
*/
private boolean countQuery = false;
public DataSourceProxyLogging getLogging() {
return logging;
}
public void setLogging(DataSourceProxyLogging logging) {
this.logging = logging;
}
public Query getQuery() {
return query;
}
public void setQuery(Query query) {
this.query = query;
}
public SlowQuery getSlowQuery() {
return slowQuery;
}
public void setSlowQuery(SlowQuery slowQuery) {
this.slowQuery = slowQuery;
}
public boolean isMultiline() {
return multiline;
}
public void setMultiline(boolean multiline) {
this.multiline = multiline;
}
public boolean isJsonFormat() {
return jsonFormat;
}
public void setJsonFormat(boolean jsonFormat) {
this.jsonFormat = jsonFormat;
}
public boolean isCountQuery() {
return countQuery;
}
public void setCountQuery(boolean countQuery) {
this.countQuery = countQuery;
}
/**
* Properties to configure query logging listener.
*/
public static class Query {
/**
* Enable logging all queries to the log.
*/
private boolean enableLogging = true;
/**
* Name of query logger.
*/
private String loggerName;
/**
* Severity of query logger.
*/
private String logLevel = "DEBUG";
public boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = logLevel;
}
}
/**
* Properties to configure slow query logging listener.
*/
public static class SlowQuery {
/**
* Enable logging slow queries to the log.
*/
private boolean enableLogging = true;
/**
* Name of slow query logger.
*/
private String loggerName;
/**
* Severity of slow query logger.
*/
private String logLevel = "WARN";
/**
* Number of seconds to consider query as slow.
*/
private long threshold = 300;
boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = logLevel;
}
public long getThreshold() {
return threshold;
}
public void setThreshold(long threshold) {
this.threshold = threshold;
}
}
/**
* Query logging listener is the most used listener that logs executing query with
* actual parameters to. You can pick one of the following proxy logging
* mechanisms.
*/
public enum DataSourceProxyLogging {
/**
* Log using System.out.
*/
SYSOUT,
/**
* Log using SLF4J.
*/
SLF4J,
/**
* Log using Commons.
*/
COMMONS,
/**
* Log using Java Util Logging.
*/
JUL
}
}
/**
* Properties for configuring p6spy.
*/
public static class P6SpyProperties {
/**
* Enables logging JDBC events.
*/
private boolean enableLogging = true;
/**
* Enables multiline output.
*/
private boolean multiline = true;
/**
* Logging to use for logging queries.
*/
private P6SpyLogging logging = P6SpyLogging.SLF4J;
/**
* Name of log file to use (only with logging=file).
*/
private String logFile = "spy.log";
/**
* Custom log format.
*/
private String logFormat;
/**
* Tracing related properties.
*/
private P6SpyTracing tracing = new P6SpyTracing();
/**
* Class file to use (only with logging=custom). The class must implement
* {@link com.p6spy.engine.spy.appender.FormattedLogger}.
*/
private String customAppenderClass;
/**
* Log filtering related properties.
*/
private P6SpyLogFilter logFilter = new P6SpyLogFilter();
public boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public boolean isMultiline() {
return multiline;
}
public void setMultiline(boolean multiline) {
this.multiline = multiline;
}
public P6SpyLogging getLogging() {
return logging;
}
public void setLogging(P6SpyLogging logging) {
this.logging = logging;
}
public String getLogFile() {
return logFile;
}
public void setLogFile(String logFile) {
this.logFile = logFile;
}
public String getLogFormat() {
return logFormat;
}
public void setLogFormat(String logFormat) {
this.logFormat = logFormat;
}
public P6SpyTracing getTracing() {
return tracing;
}
public void setTracing(P6SpyTracing tracing) {
this.tracing = tracing;
}
public String getCustomAppenderClass() {
return customAppenderClass;
}
public void setCustomAppenderClass(String customAppenderClass) {
this.customAppenderClass = customAppenderClass;
}
public P6SpyLogFilter getLogFilter() {
return logFilter;
}
public void setLogFilter(P6SpyLogFilter logFilter) {
this.logFilter = logFilter;
}
/**
* P6Spy logging options.
*/
public enum P6SpyLogging {
/**
* Log using System.out.
*/
SYSOUT,
/**
* Log using SLF4J.
*/
SLF4J,
/**
* Log to file.
*/
FILE,
/**
* Custom logging.
*/
CUSTOM
}
public static class P6SpyTracing {
/**
* Report the effective sql string (with '?' replaced with real values) to
* tracing systems.
* <p>
* NOTE this setting does not affect the logging message.
*/
private boolean includeParameterValues = true;
public boolean isIncludeParameterValues() {
return includeParameterValues;
}
public void setIncludeParameterValues(boolean includeParameterValues) {
this.includeParameterValues = includeParameterValues;
}
}
public static class P6SpyLogFilter {
/**
* Use regex pattern to filter log messages. Only matched messages will be
* logged.
*/
private Pattern pattern;
public Pattern getPattern() {
return pattern;
}
public void setPattern(Pattern pattern) {
this.pattern = pattern;
}
}
}
}

View File

@@ -9,6 +9,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfi
org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceDataSourceDecoratorAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\
org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\

View File

@@ -0,0 +1,370 @@
/*
* 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.jdbc;
import java.io.PrintWriter;
import java.sql.Connection;
import java.util.concurrent.ThreadLocalRandom;
import java.util.logging.Logger;
import javax.sql.DataSource;
import com.p6spy.engine.spy.P6DataSource;
import com.zaxxer.hikari.HikariDataSource;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
class DataSourceDecoratorAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt());
@Test
void testDecoratingInDefaultOrder() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(DataSourceWrapper.class);
DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource;
assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class);
P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource();
DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource");
assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource;
});
}
@Test
void testNoDecoratingForExcludeBeans() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:dataSource");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(HikariDataSource.class);
});
}
@Test
void testDecoratingWhenDefaultProxyProviderNotAvailable() {
ApplicationContextRunner contextRunner = this.contextRunner;
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class);
DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource;
assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class);
P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource();
DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource");
assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource;
DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource,
"dataSource");
assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource());
});
}
@Test
void testDecoratedHikariSpecificPropertiesIsSet() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues(
"spring.datasource.type:" + HikariDataSource.class.getName(),
"spring.datasource.hikari.catalog:test_catalog");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isNotNull();
assertThat(dataSource).isInstanceOf(DataSourceWrapper.class);
DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource();
assertThat(realDataSource).isInstanceOf(HikariDataSource.class);
assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog");
});
}
@Test
void testCustomDataSourceIsDecorated() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(TestDataSourceConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(DataSourceWrapper.class);
DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource();
assertThat(realDataSource).isInstanceOf(BasicDataSource.class);
});
}
@Test
void testScopedDataSourceIsNotDecorated() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(TestScopedDataSourceConfiguration.class);
contextRunner.run(context -> {
assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource");
assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class);
assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class);
});
}
@Test
void testCustomDataSourceDecoratorApplied() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(TestDataSourceDecoratorConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isNotNull();
DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource();
assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class);
DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource();
assertThat(realDataSource).isInstanceOf(HikariDataSource.class);
assertThat(dataSource).isInstanceOf(DataSourceWrapper.class);
DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource;
assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class);
CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper
.getDecoratedDataSource();
assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class);
P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate;
DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource");
assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource;
});
}
@Test
void testDecoratingCanBeDisabled() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.enabled:false");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(HikariDataSource.class);
});
}
@Test
void testDecoratingCanBeDisabledForSpecificBeans() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:secondDataSource")
.withUserConfiguration(TestMultiDataSourceConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean("dataSource", DataSource.class);
assertThat(dataSource).isInstanceOf(DataSourceWrapper.class);
DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class);
assertThat(secondDataSource).isInstanceOf(BasicDataSource.class);
});
}
@Test
void testDecoratingChainBuiltCorrectly() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class);
assertThat(dataSource1).isNotNull();
DataSource p6DataSource = dataSource1.getDecoratedDataSource();
assertThat(p6DataSource).isNotNull();
assertThat(p6DataSource).isInstanceOf(P6DataSource.class);
DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource)
.getPropertyValue("realDataSource");
assertThat(proxyDataSource).isNotNull();
assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class);
});
}
@Test
void testDecorateDynamicallyRegisteredBeans() {
ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> {
GenericApplicationContext gac = (GenericApplicationContext) context;
gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource());
gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource());
});
contextRunner.run(context -> {
DataSource dataSource1 = context.getBean("ds1", DataSource.class);
assertThat(dataSource1).isNotNull();
assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class);
DataSource dataSource2 = context.getBean("ds2", DataSource.class);
assertThat(dataSource2).isNotNull();
assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class);
});
}
@Configuration
static class TestDataSourceConfiguration {
@Bean
public DataSource dataSource() {
BasicDataSource pool = new BasicDataSource();
pool.setDriverClassName("org.hsqldb.jdbcDriver");
pool.setUrl("jdbc:hsqldb:target/overridedb");
pool.setUsername("sa");
return pool;
}
}
@Configuration
static class TestDataSourceDecoratorConfiguration {
@Bean
public DataSourceDecorator customDataSourceDecorator() {
return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource);
}
}
@Configuration
static class TestMultiDataSourceConfiguration {
@Bean
@Primary
public DataSource dataSource() {
BasicDataSource pool = new BasicDataSource();
pool.setDriverClassName("org.hsqldb.jdbcDriver");
pool.setUrl("jdbc:hsqldb:target/db");
pool.setUsername("sa");
return pool;
}
@Bean
public DataSource secondDataSource() {
BasicDataSource pool = new BasicDataSource();
pool.setDriverClassName("org.hsqldb.jdbcDriver");
pool.setUrl("jdbc:hsqldb:target/db2");
pool.setUsername("sa");
return pool;
}
}
@Configuration
static class TestScopedDataSourceConfiguration {
@Bean
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public DataSource dataSource() {
BasicDataSource pool = new BasicDataSource();
pool.setDriverClassName("org.hsqldb.jdbcDriver");
pool.setUrl("jdbc:hsqldb:target/overridedb");
pool.setUsername("sa");
return pool;
}
}
/**
* Custom proxy data source for tests.
*
* @author Arthur Gavlyukovskiy
*/
static class CustomDataSourceProxy implements DataSource {
private final DataSource delegate;
CustomDataSourceProxy(DataSource delegate) {
this.delegate = delegate;
}
@Override
public Connection getConnection() {
return null;
}
@Override
public Connection getConnection(String username, String password) {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) {
return false;
}
@Override
public PrintWriter getLogWriter() {
return null;
}
@Override
public void setLogWriter(PrintWriter out) {
}
@Override
public void setLoginTimeout(int seconds) {
}
@Override
public int getLoginTimeout() {
return 0;
}
@Override
public Logger getParentLogger() {
return null;
}
}
}

View File

@@ -0,0 +1,277 @@
/*
* 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.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;
import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.common.P6LogQuery;
import com.p6spy.engine.event.CompoundJdbcEventListener;
import com.p6spy.engine.event.JdbcEventListener;
import com.p6spy.engine.logging.Category;
import com.p6spy.engine.logging.LoggingEventListener;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import com.p6spy.engine.spy.P6DataSource;
import com.p6spy.engine.spy.appender.CustomLineFormat;
import com.p6spy.engine.spy.appender.FormattedLogger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
class P6SpyConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt())
.withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy"));
@BeforeEach
@AfterEach
void resetLogAccumulator() {
LogAccumulator.reset();
}
@Test
void testCustomListeners() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(CustomListenerConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class);
GetCountingListener getCountingListener = context.getBean(GetCountingListener.class);
ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class);
P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource();
assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory);
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener);
assertThat(getCountingListener.connectionCount).isEqualTo(0);
Connection connection1 = p6DataSource.getConnection();
assertThat(getCountingListener.connectionCount).isEqualTo(1);
assertThat(closingCountingListener.connectionCount).isEqualTo(0);
Connection connection2 = p6DataSource.getConnection();
assertThat(getCountingListener.connectionCount).isEqualTo(2);
connection1.close();
assertThat(closingCountingListener.connectionCount).isEqualTo(1);
connection2.close();
assertThat(closingCountingListener.connectionCount).isEqualTo(2);
});
}
@Test
void testDoesNotRegisterLoggingListenerIfDisabled() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging:false");
contextRunner.run(context -> {
JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class);
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).extracting("class")
.doesNotContain(LoggingEventListener.class);
});
}
@Test
void testCanSetCustomLoggingFormat() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format:test %{connectionId}");
contextRunner.run(context -> {
JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class);
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class);
assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class")
.isEqualTo(CustomLineFormat.class);
});
}
@Test
void testMultilineShouldNotOverrideCustomProperties() {
System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat");
System.setProperty("p6spy.config.excludecategories", "debug");
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline:true");
contextRunner.run(context -> {
JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class);
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class);
assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class")
.isEqualTo(CustomLineFormat.class);
});
}
@Test
void testUseCustomLogger() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues(
"spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom",
"spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:"
+ LogAccumulator.class.getName());
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
dataSource.getConnection().close();
assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class);
});
}
@Test
void testLogFilterPattern() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues(
"spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom",
"spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(),
"spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*table1.*");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
try (Connection connection = dataSource.getConnection();
PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */");
PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) {
ps1.execute();
ps2.execute();
}
assertThat(LogAccumulator.MESSAGES).hasSize(1);
assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1"));
});
}
@Test
void testLogFilterPatternMatchAll() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues(
"spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom",
"spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(),
"spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
try (Connection connection = dataSource.getConnection();
PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */");
PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) {
ps1.execute();
ps2.execute();
}
assertThat(LogAccumulator.MESSAGES).hasSize(2);
});
}
@Configuration
static class CustomListenerConfiguration {
@Bean
public GetCountingListener wrappingCountingListener() {
return new GetCountingListener();
}
@Bean
public ClosingCountingListener closingCountingListener() {
return new ClosingCountingListener();
}
}
static class GetCountingListener extends JdbcEventListener {
int connectionCount = 0;
@Override
public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) {
connectionCount++;
}
}
static class ClosingCountingListener extends JdbcEventListener {
int connectionCount = 0;
@Override
public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) {
connectionCount++;
}
}
public static class LogAccumulator extends FormattedLogger {
static final List<String> MESSAGES = new ArrayList<>();
static final List<Exception> EXCEPTIONS = new ArrayList<>();
public static void reset() {
MESSAGES.clear();
EXCEPTIONS.clear();
}
@Override
public void logException(Exception e) {
EXCEPTIONS.add(e);
}
@Override
public void logText(String text) {
MESSAGES.add(text);
}
@Override
public boolean isCategoryEnabled(Category category) {
return true;
}
}
}

View File

@@ -0,0 +1,238 @@
/*
* 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.jdbc;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.listener.ChainListener;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener;
import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener;
import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener;
import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener;
import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener;
import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener;
import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener;
import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener;
import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager;
import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import net.ttddyy.dsproxy.transform.ParameterTransformer;
import net.ttddyy.dsproxy.transform.QueryTransformer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import static org.assertj.core.api.Assertions.assertThat;
class ProxyDataSourceConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt())
.withClassLoader(new FilteredClassLoader("com.p6spy"));
@Test
void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class);
assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class);
});
}
@Test
void testRegisterLogAndSlowQueryLogByUsingSlf4j() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:slf4j");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class);
assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class);
});
}
@Test
void testRegisterLogAndSlowQueryLogUsingSystemOut() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:sysout");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class);
assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class);
});
}
@Test
void testRegisterLogAndSlowQueryLogUsingJUL() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:jul");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class);
assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class);
});
}
@Test
void testRegisterLogAndSlowQueryLogUsingApacheCommons() {
ApplicationContextRunner contextRunner = this.contextRunner
.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:commons");
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class);
assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class);
});
}
@Test
void testCustomParameterAndQueryTransformer() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(CustomDataSourceProxyConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class);
QueryTransformer queryTransformer = context.getBean(QueryTransformer.class);
assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer);
assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer);
});
}
@Test
void testCustomListeners() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(CustomListenerConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class);
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).contains(queryExecutionListener);
});
}
@Test
void testGlobalConnectionIdManagerByDefault() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class);
});
}
@Test
void testCustomConnectionIdManager() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(CustomDataSourceProxyConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class);
});
}
@Configuration
static class CustomDataSourceProxyConfiguration {
@Bean
public ParameterTransformer parameterTransformer() {
return (replacer, transformInfo) -> {
};
}
@Bean
public QueryTransformer queryTransformer() {
return (transformInfo) -> "TestQuery";
}
@Bean
public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() {
return DefaultConnectionIdManager::new;
}
}
@Configuration
static class CustomListenerConfiguration {
@Bean
@Primary
public QueryExecutionListener queryExecutionListener() {
return new QueryExecutionListener() {
@Override
public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
System.out.println("beforeQuery");
}
@Override
public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
System.out.println("afterQuery");
}
};
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.jdbc;
import java.util.concurrent.ThreadLocalRandom;
import com.p6spy.engine.event.CompoundJdbcEventListener;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener;
import static org.assertj.core.api.Assertions.assertThat;
class SleuthP6SpyListenerAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt())
.withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy"));
@Test
void testAddsP6SpyListener() {
contextRunner.run(context -> {
JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class);
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).extracting("class")
.contains(TraceJdbcEventListener.class);
});
}
@Test
void testDoesNotAddP6SpyListenerIfNoTracer() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false");
contextRunner.run(context -> {
assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class);
});
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.listener.ChainListener;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener;
import static org.assertj.core.api.Assertions.assertThat;
class SleuthProxyDataSourceListenerAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt())
.withClassLoader(new FilteredClassLoader("com.p6spy"));
@Test
void testAddsDatasourceProxyListener() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class);
});
}
@Test
void testDoesNotAddDatasourceProxyListenerIfNoTracer() {
ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false");
contextRunner.run(context -> {
assertThat(context).doesNotHaveBean(DataSourceWrapper.class);
});
}
}

View File

@@ -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.autoconfig.instrument.jdbc;
import brave.sampler.Sampler;
import brave.test.TestSpanHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class TestSpanHandlerConfiguration {
@Bean
public TestSpanHandler spanHandler() {
return new TestSpanHandler();
}
@Bean
public Sampler sampler() {
return Sampler.ALWAYS_SAMPLE;
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import javax.sql.DataSource;
import brave.handler.MutableSpan;
import brave.test.TestSpanHandler;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
class TracingJdbcEventListenerTests extends TracingListenerStrategyTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test")
.withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy"));
protected TracingJdbcEventListenerTests() {
super(new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test")
.withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")));
}
@Test
void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() {
contextRunner
.withPropertyValues(
"spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values=false")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection
.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?");
preparedStatement.setString(1, "");
preparedStatement.setInt(2, 1);
preparedStatement.executeUpdate();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("update");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME,
"UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?");
assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0");
});
}
}

View File

@@ -0,0 +1,720 @@
/*
* 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.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.sql.DataSource;
import brave.handler.MutableSpan;
import brave.test.TestSpanHandler;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
abstract class TracingListenerStrategyTests {
public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query";
public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count";
protected final ApplicationContextRunner contextRunner;
protected TracingListenerStrategyTests(ApplicationContextRunner contextRunner) {
this.contextRunner = contextRunner;
}
@Test
void testShouldAddSpanForConnection() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
connection.commit();
connection.rollback();
connection.close();
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(connectionSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback");
});
}
@Test
void testShouldAddSpanForConnectionWithFixedRemoteServiceName() {
contextRunner.withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
connection.commit();
connection.rollback();
connection.close();
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(connectionSpan.remoteServiceName()).isEqualTo("aaaabbbb");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback");
});
}
@Test
void testShouldAddSpanForPreparedStatementExecute() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
connection.prepareStatement("SELECT NOW()").execute();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(statementSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldAddSpanForPreparedStatementExecuteUpdate() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1")
.executeUpdate();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("update");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME,
"UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1");
assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0");
});
}
@Test
void testShouldAddSpanForStatementExecuteUpdate() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
connection.createStatement()
.executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1");
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("update");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME,
"UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1");
assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0");
});
}
@Test
void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery();
resultSet.next();
resultSet.next();
resultSet.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME,
"SELECT NOW() UNION ALL select NOW()");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(resultSetSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ");
if (isP6Spy(context)) {
assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2");
}
});
}
@Test
void testShouldAddSpanForStatementAndResultSet() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()");
resultSet.next();
Thread.sleep(200L);
resultSet.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
if (isP6Spy(context)) {
assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1");
}
});
}
@Test
void testShouldNotFailWhenStatementIsClosedWihoutResultSet() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldNotFailWhenResultSetNextWasNotCalled() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldNotFailWhenResourceIsAlreadyClosed() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
resultSet.close();
resultSet.close();
statement.close();
statement.close();
connection.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldNotFailWhenResourceIsAlreadyClosed2() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
assertThatThrownBy(() -> {
connection.close();
connection.prepareStatement("SELECT NOW()");
}).isInstanceOf(SQLException.class);
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
});
}
@Test
void testShouldNotFailWhenResourceIsAlreadyClosed3() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
assertThatThrownBy(() -> {
statement.close();
statement.executeQuery("SELECT NOW()");
}).isInstanceOf(SQLException.class);
connection.close();
});
}
@Test
void testShouldNotFailWhenResourceIsAlreadyClosed4() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
assertThatThrownBy(() -> {
resultSet.close();
resultSet.next();
}).isInstanceOf(SQLException.class);
statement.close();
connection.close();
});
}
@Test
void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
connection1.close();
connection2.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connection1Span = spanReporter.spans().get(0);
MutableSpan connection2Span = spanReporter.spans().get(1);
assertThat(connection1Span.name()).isEqualTo("connection");
assertThat(connection2Span.name()).isEqualTo("connection");
});
}
@Test
void testShouldNotFailWhenClosedInReversedOrder() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
connection.close();
statement.close();
resultSet.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
@SuppressWarnings("unchecked")
void testShouldNotCauseMemoryLeakOnTomcatPool() {
contextRunner.withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class)
: context.getBean(TraceQueryExecutionListener.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(listener).extracting("strategy").extracting("openConnections")
.isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty());
});
}
private boolean isP6Spy(AssertableApplicationContext context) {
if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) {
return true;
}
else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) {
return false;
}
else {
throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context.");
}
}
@Test
void testSingleConnectionAcrossMultipleThreads() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> {
try {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.next();
statement.close();
resultSet.close();
}
catch (SQLException e) {
throw new IllegalStateException(e);
}
})).collect(Collectors.toList()).forEach(CompletableFuture::join);
connection.close();
assertThat(spanReporter.spans()).hasSize(1 + 2 * 5);
assertThat(spanReporter.spans()).extracting("name").contains("select", "result-set", "connection");
});
}
@Test
void testShouldIncludeOnlyConnectionTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
});
}
@Test
void testShouldIncludeOnlyQueryTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query").run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(statementSpan.name()).isEqualTo("select");
});
}
@Test
void testShouldIncludeOnlyFetchTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: fetch").run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan resultSetSpan = spanReporter.spans().get(0);
assertThat(resultSetSpan.name()).isEqualTo("result-set");
});
}
@Test
void testShouldIncludeOnlyConnectionAndQueryTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, query")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
});
}
@Test
void testShouldIncludeOnlyConnectionAndFetchTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, fetch")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan resultSetSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
});
}
@Test
void testShouldIncludeOnlyQueryAndFetchTraces() {
contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query, fetch")
.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select 1 FROM dual");
resultSet.next();
resultSet.close();
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
});
}
@Test
void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT NOW()");
connection.close();
assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class);
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
});
}
@Test
void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT NOW()");
statement.close();
assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class);
connection.close();
assertThat(spanReporter.spans()).hasSize(2);
MutableSpan connectionSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
});
}
@Test
void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class);
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT NOW()");
resultSet.close();
assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class);
statement.close();
connection.close();
assertThat(spanReporter.spans()).hasSize(3);
MutableSpan connectionSpan = spanReporter.spans().get(2);
MutableSpan resultSetSpan = spanReporter.spans().get(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
});
}
@Test
void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() {
ApplicationContextRunner contextRunner = this.contextRunner
.withUserConfiguration(MultiDataSourceConfiguration.class);
contextRunner.run(context -> {
DataSource dataSource1 = context.getBean("test1", DataSource.class);
DataSource dataSource2 = context.getBean("test2", DataSource.class);
dataSource1.getConnection().close();
dataSource2.getConnection().close();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
Connection connection1 = dataSource1.getConnection();
PreparedStatement statement = connection1.prepareStatement("SELECT NOW()");
ResultSet resultSet = statement.executeQuery();
Thread.sleep(200);
resultSet.close();
statement.close();
connection1.close();
}
catch (SQLException | InterruptedException e) {
throw new IllegalStateException(e);
}
});
Thread.sleep(100);
Connection connection2 = dataSource2.getConnection();
Thread.sleep(300);
connection2.close();
future.join();
});
}
private static class MultiDataSourceConfiguration {
@Bean
public HikariDataSource test1() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo");
dataSource.setPoolName("test1");
return dataSource;
}
@Bean
public HikariDataSource test2() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar");
dataSource.setPoolName("test2");
return dataSource;
}
}
}

View File

@@ -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.autoconfig.instrument.jdbc;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests {
TracingQueryExecutionListenerTests() {
super(new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class,
TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class,
TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class))
.withPropertyValues("spring.datasource.initialization-mode=never",
"spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test")
.withClassLoader(new FilteredClassLoader("com.p6spy")));
}
}

View File

@@ -26,7 +26,6 @@ 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.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration;
@@ -35,7 +34,6 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration;
import org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.instrument.quartz.TracingJobListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -47,8 +45,8 @@ import org.springframework.context.annotation.Primary;
public class TraceQuartzAutoConfigurationTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration(
AutoConfigurations.of(SchedulerConfig.class, TracingConfig.class, EnableAutoConfig.class));
.withPropertyValues("spring.sleuth.noop.enabled=true")
.withConfiguration(AutoConfigurations.of(SchedulerConfig.class, EnableAutoConfig.class));
@Test
public void should_create_job_listener_bean_when_all_conditions_are_met() {
@@ -94,9 +92,10 @@ public class TraceQuartzAutoConfigurationTest {
@Test
public void should_not_create_listener_when_scheduler_bean_is_not_present() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations
// given
.of(TracingConfig.class, TraceQuartzAutoConfiguration.class))
new ApplicationContextRunner().withPropertyValues("spring.sleuth.noop.enabled=true")
.withConfiguration(AutoConfigurations
// given
.of(TraceQuartzAutoConfiguration.class))
// when
.run(context -> {
@@ -138,16 +137,4 @@ public class TraceQuartzAutoConfigurationTest {
}
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(TraceQuartzAutoConfiguration.class)
@ConditionalOnProperty("spring.sleuth.noop.enabled")
public static class TracingConfig {
@Bean
public Tracer testTracer() {
return Mockito.mock(Tracer.class);
}
}
}

View File

@@ -97,6 +97,12 @@ public class BraveSpan implements Span {
return this;
}
@Override
public Span remoteIpAndPort(String ip, int port) {
this.delegate.remoteIpAndPort(ip, port);
return this;
}
@Override
public String toString() {
return this.delegate != null ? this.delegate.toString() : "null";

View File

@@ -16,12 +16,8 @@
package org.springframework.cloud.sleuth.brave.bridge;
import java.net.URI;
import brave.Tracer;
import brave.propagation.TraceContextOrSamplingFlags;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.TraceContext;
@@ -34,8 +30,6 @@ import org.springframework.cloud.sleuth.TraceContext;
*/
class BraveSpanBuilder implements Span.Builder {
private static final Log log = LogFactory.getLog(BraveSpanBuilder.class);
brave.Span delegate;
TraceContextOrSamplingFlags parentContext;
@@ -114,16 +108,8 @@ class BraveSpanBuilder implements Span.Builder {
}
@Override
public Span.Builder remoteUrl(String remoteUrl) {
try {
URI uri = URI.create(remoteUrl);
span().remoteIpAndPort(uri.getHost(), uri.getPort());
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Failed to parse url [" + remoteUrl + "]. Will not set the value", e);
}
}
public Span.Builder remoteIpAndPort(String ip, int port) {
span().remoteIpAndPort(ip, port);
return this;
}

View File

@@ -198,6 +198,17 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -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.jdbc;
import javax.sql.DataSource;
/**
* Decorator for context {@link DataSource} beans.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public interface DataSourceDecorator {
/**
* Decorates given {@link DataSource} instance. Should either return wrapped
* {@link DataSource} or same instance.
* @param beanName name of a bean
* @param dataSource bean instance
* @return decorated {@link DataSource} or given {@link DataSource} without changes.
*/
DataSource decorate(String beanName, DataSource dataSource);
}

View File

@@ -0,0 +1,41 @@
/*
* 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.jdbc;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.CommonDataSource;
/**
* {@link CommonDataSource} name resolver based on bean name.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class DataSourceNameResolver {
private final ConcurrentHashMap<CommonDataSource, String> cachedNames = new ConcurrentHashMap<>();
public void addDataSource(String name, CommonDataSource dataSource) {
this.cachedNames.put(dataSource, name);
}
public String resolveDataSourceName(CommonDataSource dataSource) {
return this.cachedNames.getOrDefault(dataSource, "dataSource");
}
}

View File

@@ -0,0 +1,202 @@
/*
* 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.jdbc;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.logging.Level;
import net.ttddyy.dsproxy.listener.MethodExecutionListener;
import net.ttddyy.dsproxy.listener.QueryCountStrategy;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.listener.logging.CommonsLogLevel;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import net.ttddyy.dsproxy.transform.ParameterTransformer;
import net.ttddyy.dsproxy.transform.QueryTransformer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
/**
* Configurer for {@link ProxyDataSourceBuilder} based on the application context.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
* @see ProxyDataSourceBuilder
*/
public class DataSourceProxyBuilderCustomizer {
private static final Log log = LogFactory.getLog(DataSourceProxyBuilderCustomizer.class);
private final QueryCountStrategy queryCountStrategy;
private final List<QueryExecutionListener> listeners;
private final List<MethodExecutionListener> methodExecutionListeners;
private final ParameterTransformer parameterTransformer;
private final QueryTransformer queryTransformer;
private final ResultSetProxyLogicFactory resultSetProxyLogicFactory;
private final DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider;
private final DataSourceProxyProperties datasourceProxy;
public DataSourceProxyBuilderCustomizer(@Nullable QueryCountStrategy queryCountStrategy,
@Nullable List<QueryExecutionListener> listeners,
@Nullable List<MethodExecutionListener> methodExecutionListeners,
@Nullable ParameterTransformer parameterTransformer, @Nullable QueryTransformer queryTransformer,
@Nullable ResultSetProxyLogicFactory resultSetProxyLogicFactory,
@Nullable DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider,
DataSourceProxyProperties datasourceProxy) {
this.queryCountStrategy = queryCountStrategy;
this.listeners = listeners;
this.methodExecutionListeners = methodExecutionListeners;
this.parameterTransformer = parameterTransformer;
this.queryTransformer = queryTransformer;
this.resultSetProxyLogicFactory = resultSetProxyLogicFactory;
this.dataSourceProxyConnectionIdManagerProvider = dataSourceProxyConnectionIdManagerProvider;
this.datasourceProxy = datasourceProxy;
}
public ProxyDataSourceBuilder customize(ProxyDataSourceBuilder proxyDataSourceBuilder) {
switch (this.datasourceProxy.getLogging()) {
case SLF4J:
if (this.datasourceProxy.getQuery().isEnableLogging()) {
proxyDataSourceBuilder.logQueryBySlf4j(toSlf4JLogLevel(this.datasourceProxy.getQuery().getLogLevel()),
this.datasourceProxy.getQuery().getLoggerName());
}
if (this.datasourceProxy.getSlowQuery().isEnableLogging()) {
proxyDataSourceBuilder.logSlowQueryBySlf4j(this.datasourceProxy.getSlowQuery().getThreshold(),
TimeUnit.SECONDS, toSlf4JLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()),
datasourceProxy.getSlowQuery().getLoggerName());
}
break;
case JUL:
if (this.datasourceProxy.getQuery().isEnableLogging()) {
proxyDataSourceBuilder.logQueryByJUL(toJULLogLevel(this.datasourceProxy.getQuery().getLogLevel()),
this.datasourceProxy.getQuery().getLoggerName());
}
if (this.datasourceProxy.getSlowQuery().isEnableLogging()) {
proxyDataSourceBuilder.logSlowQueryByJUL(this.datasourceProxy.getSlowQuery().getThreshold(),
TimeUnit.SECONDS, toJULLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()),
this.datasourceProxy.getSlowQuery().getLoggerName());
}
break;
case COMMONS:
if (this.datasourceProxy.getQuery().isEnableLogging()) {
proxyDataSourceBuilder.logQueryByCommons(
toCommonsLogLevel(this.datasourceProxy.getQuery().getLogLevel()),
datasourceProxy.getQuery().getLoggerName());
}
if (this.datasourceProxy.getSlowQuery().isEnableLogging()) {
proxyDataSourceBuilder.logSlowQueryByCommons(this.datasourceProxy.getSlowQuery().getThreshold(),
TimeUnit.SECONDS, toCommonsLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()),
this.datasourceProxy.getSlowQuery().getLoggerName());
}
break;
case SYSOUT:
if (this.datasourceProxy.getQuery().isEnableLogging()) {
proxyDataSourceBuilder.logQueryToSysOut();
}
if (this.datasourceProxy.getSlowQuery().isEnableLogging()) {
proxyDataSourceBuilder.logSlowQueryToSysOut(this.datasourceProxy.getSlowQuery().getThreshold(),
TimeUnit.SECONDS);
}
break;
}
if (this.datasourceProxy.isMultiline() && this.datasourceProxy.isJsonFormat()) {
log.warn(
"Found opposite multiline and json format, multiline will be used (may depend on library version)");
}
if (this.datasourceProxy.isMultiline()) {
proxyDataSourceBuilder.multiline();
}
if (this.datasourceProxy.isJsonFormat()) {
proxyDataSourceBuilder.asJson();
}
if (this.datasourceProxy.isCountQuery()) {
ifAvailable(this.queryCountStrategy, proxyDataSourceBuilder::countQuery);
}
ifAvailable(this.listeners, l -> l.forEach(proxyDataSourceBuilder::listener));
ifAvailable(this.methodExecutionListeners, m -> m.forEach(proxyDataSourceBuilder::methodListener));
ifAvailable(this.parameterTransformer, proxyDataSourceBuilder::parameterTransformer);
ifAvailable(this.queryTransformer, proxyDataSourceBuilder::queryTransformer);
ifAvailable(this.resultSetProxyLogicFactory, proxyDataSourceBuilder::proxyResultSet);
ifAvailable(this.dataSourceProxyConnectionIdManagerProvider,
d -> proxyDataSourceBuilder.connectionIdManager(d.get()));
return proxyDataSourceBuilder;
}
private <T> void ifAvailable(@Nullable T o, Consumer<T> consumer) {
if (o != null) {
consumer.accept(o);
}
}
private SLF4JLogLevel toSlf4JLogLevel(String logLevel) {
if (logLevel == null) {
return null;
}
for (SLF4JLogLevel slf4JLogLevel : SLF4JLogLevel.values()) {
if (slf4JLogLevel.name().equalsIgnoreCase(logLevel)) {
return slf4JLogLevel;
}
}
throw new IllegalArgumentException("Unresolved log level " + logLevel + " for slf4j logger, known levels: "
+ Arrays.toString(SLF4JLogLevel.values()));
}
private Level toJULLogLevel(String logLevel) {
if (logLevel == null) {
return null;
}
try {
return Level.parse(logLevel);
}
catch (IllegalArgumentException e) {
if (logLevel.equalsIgnoreCase("DEBUG")) {
return Level.FINE;
}
if (logLevel.equalsIgnoreCase("WARN")) {
return Level.WARNING;
}
throw new IllegalArgumentException("Unresolved log level " + logLevel + " for java.util.logging", e);
}
}
private CommonsLogLevel toCommonsLogLevel(String logLevel) {
if (logLevel == null) {
return null;
}
for (CommonsLogLevel commonsLogLevel : CommonsLogLevel.values()) {
if (commonsLogLevel.name().equalsIgnoreCase(logLevel)) {
return commonsLogLevel;
}
}
throw new IllegalArgumentException("Unresolved log level " + logLevel
+ " for apache commons logger, known levels " + Arrays.toString(CommonsLogLevel.values()));
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.jdbc;
import java.util.function.Supplier;
import net.ttddyy.dsproxy.ConnectionIdManager;
/**
* Supplier of a {@link ConnectionIdManager}.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public interface DataSourceProxyConnectionIdManagerProvider extends Supplier<ConnectionIdManager> {
}

View File

@@ -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.instrument.jdbc;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.springframework.core.Ordered;
/**
* {@link Ordered} decorator for {@link ProxyDataSource}.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class DataSourceProxyDataSourceDecorator implements DataSourceDecorator, Ordered {
private final DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer;
private final DataSourceNameResolver dataSourceNameResolver;
public DataSourceProxyDataSourceDecorator(DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer,
DataSourceNameResolver dataSourceNameResolver) {
this.dataSourceProxyBuilderCustomizer = dataSourceProxyBuilderCustomizer;
this.dataSourceNameResolver = dataSourceNameResolver;
}
@Override
public DataSource decorate(String beanName, DataSource dataSource) {
ProxyDataSourceBuilder proxyDataSourceBuilder = ProxyDataSourceBuilder.create();
proxyDataSourceBuilder = this.dataSourceProxyBuilderCustomizer.customize(proxyDataSourceBuilder);
String dataSourceName = this.dataSourceNameResolver.resolveDataSourceName(dataSource);
return proxyDataSourceBuilder.dataSource(dataSource).name(dataSourceName).build();
}
@Override
public int getOrder() {
return 20;
}
}

View File

@@ -0,0 +1,263 @@
/*
* 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.jdbc;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import net.ttddyy.dsproxy.QueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.CommonsLogLevel;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
/**
* Properties for datasource-proxy.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class DataSourceProxyProperties {
/**
* Logging to use for logging queries.
*/
private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J;
/**
* Query configuration.
*/
private Query query = new Query();
/**
* Slow query configuration.
*/
private SlowQuery slowQuery = new SlowQuery();
/**
* Use multiline output for logging query.
*
* @see ProxyDataSourceBuilder#multiline()
*/
private boolean multiline = true;
/**
* Use json output for logging query.
*
* @see ProxyDataSourceBuilder#asJson()
*/
private boolean jsonFormat = false;
/**
* Creates listener to count queries.
*
* @see ProxyDataSourceBuilder#countQuery()
* @see QueryCountHolder
*/
private boolean countQuery = false;
public DataSourceProxyLogging getLogging() {
return logging;
}
public void setLogging(DataSourceProxyLogging logging) {
this.logging = logging;
}
public Query getQuery() {
return query;
}
public void setQuery(Query query) {
this.query = query;
}
public SlowQuery getSlowQuery() {
return slowQuery;
}
public void setSlowQuery(SlowQuery slowQuery) {
this.slowQuery = slowQuery;
}
public boolean isMultiline() {
return multiline;
}
public void setMultiline(boolean multiline) {
this.multiline = multiline;
}
public boolean isJsonFormat() {
return jsonFormat;
}
public void setJsonFormat(boolean jsonFormat) {
this.jsonFormat = jsonFormat;
}
public boolean isCountQuery() {
return countQuery;
}
public void setCountQuery(boolean countQuery) {
this.countQuery = countQuery;
}
/**
* Properties to configure query logging listener.
*
* @see ProxyDataSourceBuilder#logQueryToSysOut()
* @see ProxyDataSourceBuilder#logQueryBySlf4j(SLF4JLogLevel, String)
* @see ProxyDataSourceBuilder#logQueryByCommons(CommonsLogLevel, String)
* @see ProxyDataSourceBuilder#logQueryByJUL(Level, String)
*/
public static class Query {
/**
* Enable logging all queries to the log.
*/
private boolean enableLogging = true;
/**
* Name of query logger.
*/
private String loggerName;
/**
* Severity of query logger.
*/
private String logLevel = "DEBUG";
public boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = logLevel;
}
}
/**
* Properties to configure slow query logging listener.
*
* @see ProxyDataSourceBuilder#logSlowQueryToSysOut(long, TimeUnit)
* @see ProxyDataSourceBuilder#logSlowQueryBySlf4j(long, TimeUnit)
* @see ProxyDataSourceBuilder#logSlowQueryByCommons(long, TimeUnit)
* @see ProxyDataSourceBuilder#logSlowQueryByJUL(long, TimeUnit)
*/
public static class SlowQuery {
/**
* Enable logging slow queries to the log.
*/
private boolean enableLogging = true;
/**
* Name of slow query logger.
*/
private String loggerName;
/**
* Severity of slow query logger.
*/
private String logLevel = "WARN";
/**
* Number of seconds to consider query as slow.
*/
private long threshold = 300;
boolean isEnableLogging() {
return enableLogging;
}
public void setEnableLogging(boolean enableLogging) {
this.enableLogging = enableLogging;
}
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLogLevel() {
return logLevel;
}
public void setLogLevel(String logLevel) {
this.logLevel = logLevel;
}
public long getThreshold() {
return threshold;
}
public void setThreshold(long threshold) {
this.threshold = threshold;
}
}
/**
* Query logging listener is the most used listener that logs executing query with
* actual parameters to. You can pick one of the following proxy logging mechanisms.
*/
public enum DataSourceProxyLogging {
/**
* Log using System.out.
*/
SYSOUT,
/**
* Log using SLF4J.
*/
SLF4J,
/**
* Log using Commons.
*/
COMMONS,
/**
* Log using Java Util Logging.
*/
JUL
}
}

View File

@@ -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.instrument.jdbc;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DelegatingDataSource;
/**
* Interface that implicitly added to the CGLIB proxy of {@link DataSource}.
*
* Returns link of both real {@link DataSource}, decorated {@link DataSource} and all
* decorating chain including decorator bean name, instance and result of decorating.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class DataSourceWrapper extends DelegatingDataSource {
private final DataSource originalDataSource;
public DataSourceWrapper(DataSource originalDataSource, DataSource decoratedDataSource) {
super(decoratedDataSource);
this.originalDataSource = originalDataSource;
}
/**
* Returns original data source, before applying any decorator.
* @return original data source
*/
public DataSource getOriginalDataSource() {
return originalDataSource;
}
/**
* Returns wrapped data source with the decorator applied.
* @return decorated data source
*/
public DataSource getDecoratedDataSource() {
return getTargetDataSource();
}
}

View File

@@ -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.instrument.jdbc;
import java.util.List;
import com.p6spy.engine.event.CompoundJdbcEventListener;
import com.p6spy.engine.event.JdbcEventListener;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import org.springframework.util.Assert;
/**
* Factory to support all defined {@link JdbcEventListener} in the context.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class P6SpyContextJdbcEventListenerFactory implements JdbcEventListenerFactory {
private final CompoundJdbcEventListener compoundJdbcEventListener;
public P6SpyContextJdbcEventListenerFactory(JdbcEventListenerFactory delegate, List<JdbcEventListener> listeners) {
Assert.notNull(delegate, "JdbcEventListenerFactory should not be null");
Assert.notEmpty(listeners, "Listeners should not be empty");
JdbcEventListener jdbcEventListener = delegate.createJdbcEventListener();
if (jdbcEventListener instanceof CompoundJdbcEventListener) {
this.compoundJdbcEventListener = (CompoundJdbcEventListener) jdbcEventListener;
}
else {
this.compoundJdbcEventListener = new CompoundJdbcEventListener();
this.compoundJdbcEventListener.addListener(jdbcEventListener);
}
listeners.forEach(this.compoundJdbcEventListener::addListener);
}
@Override
public JdbcEventListener createJdbcEventListener() {
return this.compoundJdbcEventListener;
}
}

View File

@@ -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.instrument.jdbc;
import javax.sql.DataSource;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import com.p6spy.engine.spy.P6DataSource;
import org.springframework.core.Ordered;
/**
* {@link Ordered} decorator for {@link P6DataSource}.
*
* @author Arthur Gavlyukovskiy
*/
public class P6SpyDataSourceDecorator implements DataSourceDecorator, Ordered {
/**
* Bean order.
*/
public static final int ORDER = 30;
private final JdbcEventListenerFactory jdbcEventListenerFactory;
public P6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) {
this.jdbcEventListenerFactory = jdbcEventListenerFactory;
}
@Override
public DataSource decorate(String beanName, DataSource dataSource) {
P6DataSource p6DataSource = new P6DataSource(dataSource);
p6DataSource.setJdbcEventListenerFactory(this.jdbcEventListenerFactory);
return p6DataSource;
}
@Override
public int getOrder() {
return P6SpyDataSourceDecorator.ORDER;
}
}

View File

@@ -0,0 +1,167 @@
/*
* 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.jdbc;
import org.springframework.cloud.sleuth.docs.DocumentedSpan;
import org.springframework.cloud.sleuth.docs.EventValue;
import org.springframework.cloud.sleuth.docs.TagKey;
enum SleuthJdbcSpan implements DocumentedSpan {
/**
* Span created when a JDBC query gets executed.
*/
JDBC_QUERY_SPAN {
@Override
public String getName() {
return "%s";
}
@Override
public TagKey[] getTagKeys() {
return QueryTags.values();
}
@Override
public EventValue[] getEvents() {
return QueryEvents.values();
}
@Override
public String prefix() {
return "jdbc.";
}
},
/**
* Span created when working with JDBC result set.
*/
JDBC_RESULT_SET_SPAN {
@Override
public String getName() {
return "result-set";
}
@Override
public TagKey[] getTagKeys() {
return QueryTags.values();
}
@Override
public EventValue[] getEvents() {
return QueryEvents.values();
}
@Override
public String prefix() {
return "jdbc.";
}
},
/**
* Span created when a JDBC connection takes place.
*/
JDBC_CONNECTION_SPAN {
@Override
public String getName() {
return "connection";
}
@Override
public TagKey[] getTagKeys() {
return ConnectionTags.values();
}
@Override
public String prefix() {
return "jdbc.";
}
};
enum ConnectionTags implements TagKey {
/**
* Name of the JDBC datasource driver.
*/
DATASOURCE_DRIVER {
@Override
public String getKey() {
return "jdbc.datasource.driver";
}
},
/**
* Name of the JDBC datasource pool.
*/
DATASOURCE_POOL {
@Override
public String getKey() {
return "jdbc.datasource.pool";
}
},
}
enum QueryTags implements TagKey {
/**
* The SQL query value.
*/
QUERY {
@Override
public String getKey() {
return "jdbc.query";
}
},
/**
* Number of SQL rows.
*/
ROW_COUNT {
@Override
public String getKey() {
return "jdbc.row-count";
}
}
}
enum QueryEvents implements EventValue {
/**
* When the transaction gets commited.
*/
COMMIT {
@Override
public String getValue() {
return "jdbc.commit";
}
},
/**
* When the transaction gets rolled back.
*/
ROLLBACK {
@Override
public String getValue() {
return "jdbc.rollback";
}
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.jdbc;
import javax.sql.CommonDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder;
import org.springframework.util.StringUtils;
/**
* Customizer for {@link TraceListenerStrategy} for a {@link HikariDataSource}.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public class TraceHikariListenerStrategySpanCustomizer
implements TraceListenerStrategySpanCustomizer<HikariDataSource> {
@Override
public void customizeConnectionSpan(HikariDataSource hikariDataSource, Span.Builder spanBuilder) {
AssertingSpanBuilder assertingSpanBuilder = AssertingSpanBuilder.of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN,
spanBuilder);
if (StringUtils.hasText(hikariDataSource.getDriverClassName())) {
assertingSpanBuilder.tag(SleuthJdbcSpan.ConnectionTags.DATASOURCE_DRIVER,
hikariDataSource.getDriverClassName());
}
if (StringUtils.hasText(hikariDataSource.getPoolName())) {
assertingSpanBuilder.tag(SleuthJdbcSpan.ConnectionTags.DATASOURCE_POOL, hikariDataSource.getPoolName());
}
}
@Override
public boolean isApplicable(CommonDataSource dataSource) {
return dataSource instanceof HikariDataSource;
}
}

View File

@@ -0,0 +1,152 @@
/*
* 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.jdbc;
import java.sql.SQLException;
import java.util.List;
import javax.sql.CommonDataSource;
import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.common.PreparedStatementInformation;
import com.p6spy.engine.common.ResultSetInformation;
import com.p6spy.engine.common.StatementInformation;
import com.p6spy.engine.event.SimpleJdbcEventListener;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.core.Ordered;
import org.springframework.util.StringUtils;
/**
* Listener to represent each connection and sql query as a span.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class TraceJdbcEventListener extends SimpleJdbcEventListener implements Ordered {
/**
* Bean order.
*/
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
private final DataSourceNameResolver dataSourceNameResolver;
private final TraceListenerStrategy<ConnectionInformation, StatementInformation, ResultSetInformation> strategy;
private final boolean includeParameterValues;
public TraceJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver,
List<TraceType> traceTypes, boolean includeParameterValues,
List<TraceListenerStrategySpanCustomizer> customizers) {
this.dataSourceNameResolver = dataSourceNameResolver;
this.includeParameterValues = includeParameterValues;
this.strategy = new TraceListenerStrategy<>(tracer, traceTypes, customizers);
}
@Override
public void onBeforeGetConnection(ConnectionInformation connectionInformation) {
CommonDataSource dataSource = connectionInformation.getDataSource();
String dataSourceName = this.dataSourceNameResolver.resolveDataSourceName(dataSource);
this.strategy.beforeGetConnection(connectionInformation, dataSource, dataSourceName);
}
@Override
public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) {
this.strategy.afterGetConnection(connectionInformation, connectionInformation.getConnection(), e);
}
@Override
public void onBeforeAnyExecute(StatementInformation statementInformation) {
String dataSourceName = this.dataSourceNameResolver
.resolveDataSourceName(statementInformation.getConnectionInformation().getDataSource());
this.strategy.beforeQuery(statementInformation.getConnectionInformation(),
statementInformation.getConnectionInformation().getConnection(), statementInformation, dataSourceName);
}
@Override
public void onAfterAnyExecute(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) {
this.strategy.afterQuery(statementInformation.getConnectionInformation(), statementInformation,
getSql(statementInformation), e);
}
@Override
public void onBeforeResultSetNext(ResultSetInformation resultSetInformation) {
String dataSourceName = this.dataSourceNameResolver
.resolveDataSourceName(resultSetInformation.getConnectionInformation().getDataSource());
this.strategy.beforeResultSetNext(resultSetInformation.getConnectionInformation(),
resultSetInformation.getConnectionInformation().getConnection(),
resultSetInformation.getStatementInformation(), resultSetInformation, dataSourceName);
}
@Override
public void onAfterExecuteUpdate(PreparedStatementInformation statementInformation, long timeElapsedNanos,
int rowCount, SQLException e) {
if (e == null) {
this.strategy.addQueryRowCount(statementInformation.getConnectionInformation(), statementInformation,
rowCount);
}
super.onAfterExecuteUpdate(statementInformation, timeElapsedNanos, rowCount, e);
}
@Override
public void onAfterExecuteUpdate(StatementInformation statementInformation, long timeElapsedNanos, String sql,
int rowCount, SQLException e) {
if (e == null) {
this.strategy.addQueryRowCount(statementInformation.getConnectionInformation(), statementInformation,
rowCount);
}
super.onAfterExecuteUpdate(statementInformation, timeElapsedNanos, sql, rowCount, e);
}
@Override
public void onAfterStatementClose(StatementInformation statementInformation, SQLException e) {
this.strategy.afterStatementClose(statementInformation.getConnectionInformation(), statementInformation);
}
@Override
public void onAfterResultSetClose(ResultSetInformation resultSetInformation, SQLException e) {
this.strategy.afterResultSetClose(resultSetInformation.getConnectionInformation(), resultSetInformation,
resultSetInformation.getCurrRow() + 1, e);
}
@Override
public void onAfterCommit(ConnectionInformation connectionInformation, long timeElapsedNanos, SQLException e) {
this.strategy.afterCommit(connectionInformation, e);
}
@Override
public void onAfterRollback(ConnectionInformation connectionInformation, long timeElapsedNanos, SQLException e) {
this.strategy.afterRollback(connectionInformation, e);
}
@Override
public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) {
this.strategy.afterConnectionClose(connectionInformation, e);
}
private String getSql(StatementInformation statementInformation) {
return this.includeParameterValues && StringUtils.hasText(statementInformation.getSqlWithValues())
? statementInformation.getSqlWithValues() : statementInformation.getSql();
}
@Override
public int getOrder() {
return ORDER;
}
}

View File

@@ -0,0 +1,402 @@
/*
* 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.jdbc;
import java.net.URI;
import java.sql.Connection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.CommonDataSource;
import org.springframework.cloud.sleuth.Span;
import org.springframework.cloud.sleuth.SpanAndScope;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.docs.AssertingSpan;
import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Partially taken from
* https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java.
*
* @param <CON> connection type
* @param <STMT> statement
* @param <RS> result set
*/
class TraceListenerStrategy<CON, STMT, RS> {
private final Map<CON, ConnectionInfo> openConnections = new ConcurrentHashMap<>();
// Captures all the characters between = and either the next & or the end of the
// string.
private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)");
private final Tracer tracer;
private final List<TraceType> traceTypes;
private final List<TraceListenerStrategySpanCustomizer> customizers;
TraceListenerStrategy(Tracer tracer, List<TraceType> traceTypes,
List<TraceListenerStrategySpanCustomizer> customizers) {
this.tracer = tracer;
this.traceTypes = traceTypes;
this.customizers = customizers;
}
void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) {
SpanAndScope spanAndScope = null;
if (this.traceTypes.contains(TraceType.CONNECTION)) {
AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder
.of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder())
.name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName());
connectionSpanBuilder.remoteServiceName(dataSourceName);
connectionSpanBuilder.kind(Span.Kind.CLIENT);
this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource))
.forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder));
Span connectionSpan = connectionSpanBuilder.start();
spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan));
}
ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope);
this.openConnections.put(connectionKey, connectionInfo);
}
void afterGetConnection(CON connectionKey, Connection connection, Throwable t) {
if (t != null) {
ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey);
connectionInfo.getSpan().ifPresent(connectionSpan -> {
parseServerIpAndPort(connection, connectionSpan.getSpan());
connectionSpan.getSpan().error(t);
connectionSpan.close();
});
return;
}
this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> {
parseServerIpAndPort(connection, spanAndScope.getSpan());
});
}
void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) {
SpanAndScope SpanAndScope = null;
if (traceTypes.contains(TraceType.QUERY)) {
Span.Builder statementSpanBuilder = AssertingSpanBuilder
.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder())
.name(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName());
statementSpanBuilder.remoteServiceName(dataSourceName);
parseServerIpAndPort(connection, statementSpanBuilder);
statementSpanBuilder.kind(Span.Kind.CLIENT);
Span statementSpan = statementSpanBuilder.start();
SpanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan));
}
StatementInfo statementInfo = new StatementInfo(SpanAndScope);
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
// Connection may be closed after statement preparation, but before statement
// execution.
return;
}
connectionInfo.getNestedStatements().put(statementKey, statementInfo);
}
void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
// Connection is already closed
return;
}
StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey);
statementInfo.getSpan()
.ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan())
.tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)));
}
void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
// Connection may be closed after statement preparation, but before statement
// execution.
return;
}
StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey);
statementInfo.getSpan().ifPresent(statementSpan -> {
AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan())
.tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql));
if (t != null) {
statementSpan.getSpan().error(t);
}
statementSpan.close();
});
}
void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey,
String dataSourceName) {
if (!traceTypes.contains(TraceType.FETCH)) {
return;
}
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
// ConnectionInfo may be null if Connection was closed before ResultSet
if (connectionInfo == null) {
return;
}
if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) {
// ResultSet span is already created
return;
}
AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder
.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder())
.name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName());
resultSetSpanBuilder.remoteServiceName(dataSourceName);
resultSetSpanBuilder.kind(Span.Kind.CLIENT);
parseServerIpAndPort(connection, resultSetSpanBuilder);
Span resultSetSpan = resultSetSpanBuilder.start();
SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan));
connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope);
StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey);
// StatementInfo may be null when Statement is proxied and instance returned from
// ResultSet is different from instance returned in query method
// in this case if Statement is closed before ResultSet span won't be finished
// immediately, but when Connection is closed
if (statementInfo != null) {
statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope);
}
}
void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
// ConnectionInfo may be null if Connection was closed before ResultSet
if (connectionInfo == null) {
return;
}
SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey);
// ResultSet span may be null if Statement or ResultSet were already closed
if (resultSetSpan == null) {
return;
}
if (rowCount != -1) {
AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan())
.tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount));
}
if (t != null) {
resultSetSpan.getSpan().error(t);
}
resultSetSpan.close();
}
void afterStatementClose(CON connectionKey, STMT statementKey) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
// ConnectionInfo may be null if Connection was closed before Statement
if (connectionInfo == null) {
return;
}
StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey);
if (statementInfo != null) {
statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> {
connectionInfo.getNestedResultSetSpans().remove(resultSetKey);
span.close();
});
statementInfo.getNestedResultSetSpans().clear();
}
}
void afterCommit(CON connectionKey, Throwable t) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
// Connection is already closed
return;
}
connectionInfo.getSpan().ifPresent(connectionSpan -> {
if (t != null) {
connectionSpan.getSpan().error(t);
}
AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan())
.event(SleuthJdbcSpan.QueryEvents.COMMIT);
});
}
void afterRollback(CON connectionKey, Throwable t) {
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
// Connection is already closed
return;
}
connectionInfo.getSpan().ifPresent(connectionSpan -> {
if (t != null) {
connectionSpan.getSpan().error(t);
}
else {
connectionSpan.getSpan().error(new JdbcException("Transaction rolled back"));
}
AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan())
.event(SleuthJdbcSpan.QueryEvents.ROLLBACK);
});
}
void afterConnectionClose(CON connectionKey, Throwable t) {
ConnectionInfo connectionInfo = openConnections.remove(connectionKey);
if (connectionInfo == null) {
// connection is already closed
return;
}
connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close);
connectionInfo.getNestedStatements().values()
.forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close));
connectionInfo.getSpan().ifPresent(connectionSpan -> {
if (t != null) {
connectionSpan.getSpan().error(t);
}
connectionSpan.close();
});
}
private String spanName(String sql) {
return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT);
}
private void parseServerIpAndPort(Connection connection, Span.Builder span) {
if (connection == null) {
return;
}
UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection);
span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName);
URI url = urlAndRemoteServiceName.url;
if (url != null) {
span.remoteIpAndPort(url.getHost(), url.getPort());
}
}
private void parseServerIpAndPort(Connection connection, Span span) {
if (connection == null) {
return;
}
UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection);
span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName);
URI url = urlAndRemoteServiceName.url;
if (url != null) {
span.remoteIpAndPort(url.getHost(), url.getPort());
}
}
/**
* This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from
* {@code
* jdbc:mysql://localhost:5555/mydatabase}.
*
* Taken from Brave.
*/
private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) {
String remoteServiceName = "";
try {
String urlAsString = connection.getMetaData().getURL().substring(5); // strip
// "jdbc:"
URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space
// according to RFC 2396
Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString());
if (matcher.find() && matcher.groupCount() == 1) {
String parsedServiceName = matcher.group(1);
if (parsedServiceName != null && !parsedServiceName.isEmpty()) {
remoteServiceName = parsedServiceName;
}
}
if (!StringUtils.hasText(remoteServiceName)) {
String databaseName = connection.getCatalog();
if (databaseName != null && !databaseName.isEmpty()) {
remoteServiceName = databaseName;
}
}
return new UrlAndRemoteServiceName(url, remoteServiceName);
}
catch (Exception e) {
// remote address is optional
return new UrlAndRemoteServiceName(null, remoteServiceName);
}
}
private final class ConnectionInfo {
private final SpanAndScope span;
private final Map<STMT, StatementInfo> nestedStatements = new ConcurrentHashMap<>();
private final Map<RS, SpanAndScope> nestedResultSetSpans = new ConcurrentHashMap<>();
private ConnectionInfo(@Nullable SpanAndScope span) {
this.span = span;
}
Optional<SpanAndScope> getSpan() {
return Optional.ofNullable(span);
}
Map<STMT, StatementInfo> getNestedStatements() {
return nestedStatements;
}
Map<RS, SpanAndScope> getNestedResultSetSpans() {
return nestedResultSetSpans;
}
}
private final class StatementInfo {
private final SpanAndScope span;
private final Map<RS, SpanAndScope> nestedResultSetSpans = new ConcurrentHashMap<>();
private StatementInfo(SpanAndScope span) {
this.span = span;
}
Optional<SpanAndScope> getSpan() {
return Optional.ofNullable(span);
}
Map<RS, SpanAndScope> getNestedResultSetSpans() {
return nestedResultSetSpans;
}
}
private final class UrlAndRemoteServiceName {
final URI url;
final String remoteServiceName;
private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) {
this.url = url;
this.remoteServiceName = remoteServiceName;
}
}
private static final class JdbcException extends RuntimeException {
JdbcException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.jdbc;
import javax.sql.CommonDataSource;
import org.springframework.cloud.sleuth.Span;
/**
* Customizer for {@link TraceListenerStrategy} client span.
*
* @author Marcin Grzejszczak
* @since 3.1.0
*/
public interface TraceListenerStrategySpanCustomizer<T extends CommonDataSource> {
/**
* Customizes the client database span.
* @param spanBuilder span builder
*/
void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder);
/**
* @param dataSource data source for which we're building the span
* @return {@code true} when this customizer can be applied
*/
boolean isApplicable(CommonDataSource dataSource);
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.stream.Collectors;
import javax.sql.CommonDataSource;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.ExecutionInfo;
import net.ttddyy.dsproxy.QueryInfo;
import net.ttddyy.dsproxy.listener.MethodExecutionContext;
import net.ttddyy.dsproxy.listener.MethodExecutionListener;
import net.ttddyy.dsproxy.listener.QueryExecutionListener;
import net.ttddyy.dsproxy.support.ProxyDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.core.Ordered;
/**
* Listener to represent each connection and sql query as a span.
*
* @author Arthur Gavlyukovskiy
* @since 3.1.0
*/
public class TraceQueryExecutionListener implements QueryExecutionListener, MethodExecutionListener, Ordered {
private static final Log log = LogFactory.getLog(TraceQueryExecutionListener.class);
private final TraceListenerStrategy<String, Statement, ResultSet> strategy;
public TraceQueryExecutionListener(Tracer tracer, List<TraceType> traceTypes,
List<TraceListenerStrategySpanCustomizer> customizers) {
this.strategy = new TraceListenerStrategy<>(tracer, traceTypes, customizers);
}
@Override
public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
this.strategy.beforeQuery(execInfo.getConnectionId(), connection(execInfo), execInfo.getStatement(),
execInfo.getDataSourceName());
}
private Connection connection(ExecutionInfo execInfo) {
try {
return execInfo.getStatement().getConnection();
}
catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug("Can't retrieve the connection - will return null", ex);
}
return null;
}
}
@Override
public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
if (execInfo.getMethod().getName().equals("executeUpdate") && execInfo.getThrowable() == null) {
this.strategy.addQueryRowCount(execInfo.getConnectionId(), execInfo.getStatement(),
(int) execInfo.getResult());
}
String sql = queryInfoList.stream().map(QueryInfo::getQuery).collect(Collectors.joining("\n"));
this.strategy.afterQuery(execInfo.getConnectionId(), execInfo.getStatement(), sql, execInfo.getThrowable());
}
@Override
public void beforeMethod(MethodExecutionContext executionContext) {
Object target = executionContext.getTarget();
String methodName = executionContext.getMethod().getName();
String dataSourceName = executionContext.getProxyConfig().getDataSourceName();
String connectionId = executionContext.getConnectionInfo().getConnectionId();
if (target instanceof DataSource && methodName.equals("getConnection")) {
this.strategy.beforeGetConnection(connectionId, toCommonDataSource(target), dataSourceName);
}
else if (target instanceof ResultSet) {
ResultSet resultSet = (ResultSet) target;
if (methodName.equals("next")) {
try {
this.strategy.beforeResultSetNext(connectionId, resultSet.getStatement().getConnection(),
resultSet.getStatement(), resultSet, dataSourceName);
}
catch (SQLException ignore) {
}
}
}
}
private CommonDataSource toCommonDataSource(Object target) {
try {
return ((DataSource) target).unwrap(CommonDataSource.class);
}
catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug("Failed to cast to common data source. Will return null", ex);
}
return null;
}
}
private Connection getConnection(DataSource targetDataSource) {
try {
DataSource source = targetDataSource instanceof ProxyDataSource
? (targetDataSource).unwrap(DataSource.class) : targetDataSource;
return source.getConnection();
}
catch (Exception ex) {
if (log.isDebugEnabled()) {
log.debug("Failed to retrieve connection. Will return null", ex);
}
return null;
}
}
@Override
public void afterMethod(MethodExecutionContext executionContext) {
Object target = executionContext.getTarget();
String methodName = executionContext.getMethod().getName();
String connectionId = executionContext.getConnectionInfo().getConnectionId();
Throwable t = executionContext.getThrown();
if (target instanceof DataSource && methodName.equals("getConnection")) {
this.strategy.afterGetConnection(connectionId, getConnection((DataSource) target), t);
}
else if (target instanceof Connection) {
switch (methodName) {
case "commit":
this.strategy.afterCommit(connectionId, t);
break;
case "rollback":
this.strategy.afterRollback(connectionId, t);
break;
case "close":
this.strategy.afterConnectionClose(connectionId, t);
break;
}
}
else if (target instanceof Statement && methodName.equals("close")) {
this.strategy.afterStatementClose(connectionId, (Statement) target);
}
else if (target instanceof ResultSet && methodName.equals("close")) {
ResultSet resultSet = (ResultSet) target;
this.strategy.afterResultSetClose(connectionId, resultSet, -1, t);
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.jdbc;
/**
* Trace types.
*/
public enum TraceType {
/**
* Connection.
*/
CONNECTION,
/**
* Query.
*/
QUERY,
/**
* Fetch.
*/
FETCH
}

View File

@@ -16,6 +16,8 @@
package org.springframework.cloud.sleuth.instrument.r2dbc;
import java.net.URI;
import io.r2dbc.proxy.core.QueryExecutionInfo;
import io.r2dbc.proxy.core.QueryInfo;
import io.r2dbc.proxy.listener.ProxyExecutionListener;
@@ -84,7 +86,15 @@ public class TraceProxyExecutionListener implements ProxyExecutionListener {
.tag(SleuthR2dbcSpan.Tags.THREAD, executionInfo.getThreadName());
// @formatter:on
if (StringUtils.hasText(url)) {
builder.remoteUrl(url);
try {
URI uri = URI.create(url);
builder.remoteIpAndPort(uri.getHost(), uri.getPort());
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Failed to parse the url [" + url + "]. Won't set this value as atag");
}
}
}
return builder.start();
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.sleuth.instrument.jdbc;
import java.util.Collections;
import com.p6spy.engine.event.CompoundJdbcEventListener;
import com.p6spy.engine.event.SimpleJdbcEventListener;
import com.p6spy.engine.spy.JdbcEventListenerFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
class ContextJdbcEventListenerFactoryTest {
private JdbcEventListenerFactory delegate;
@BeforeEach
void setUp() {
delegate = Mockito.mock(JdbcEventListenerFactory.class);
}
@Test
void shouldUseDelegateToCreateListener() {
SimpleJdbcEventListener listener1 = new SimpleJdbcEventListener() {
};
SimpleJdbcEventListener listener2 = new SimpleJdbcEventListener() {
};
Mockito.when(delegate.createJdbcEventListener()).thenReturn(listener1);
P6SpyContextJdbcEventListenerFactory contextJdbcEventListenerFactory = new P6SpyContextJdbcEventListenerFactory(
delegate, Collections.singletonList(listener2));
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) contextJdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).hasSize(2);
assertThat(jdbcEventListener.getEventListeners()).contains(listener1, listener2);
}
@Test
void shouldReuseCompoundListenerFromFactory() {
SimpleJdbcEventListener listener1 = new SimpleJdbcEventListener() {
};
Mockito.when(delegate.createJdbcEventListener()).thenReturn(new CompoundJdbcEventListener());
P6SpyContextJdbcEventListenerFactory contextJdbcEventListenerFactory = new P6SpyContextJdbcEventListenerFactory(
delegate, Collections.singletonList(listener1));
CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) contextJdbcEventListenerFactory
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).hasSize(1);
assertThat(jdbcEventListener.getEventListeners()).contains(listener1);
}
}

View File

@@ -140,6 +140,7 @@ public class TraceAsyncIntegrationTests {
private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) {
Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> {
then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull();
then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId())
.isEqualTo(span.context().traceId());
then(this.spans).hasSize(2);