JDBC tracing context not leaking

without this change there were cases (that we didn't test) where the tracing context would leak and pollute other parts of the code (including tests)
with this change we ensure that in case of errors we don't allow any dangling tracing context
This commit is contained in:
Marcin Grzejszczak
2021-05-26 18:05:43 +02:00
parent feeb251f09
commit 12737adb67
11 changed files with 1088 additions and 929 deletions

View File

@@ -18,6 +18,9 @@ package org.springframework.cloud.sleuth;
import java.io.Closeable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Container object for {@link Span} and its corresponding {@link Tracer.SpanInScope}.
*
@@ -26,6 +29,8 @@ import java.io.Closeable;
*/
public class SpanAndScope implements Closeable {
private static final Log log = LogFactory.getLog(SpanAndScope.class);
private final Span span;
private final Tracer.SpanInScope scope;
@@ -50,6 +55,9 @@ public class SpanAndScope implements Closeable {
@Override
public void close() {
if (log.isTraceEnabled()) {
log.trace("Closing span [" + this.span + "]");
}
this.scope.close();
this.span.end();
}

View File

@@ -130,6 +130,11 @@ public interface AssertingSpanBuilder extends Span.Builder {
public boolean isStarted() {
return true;
}
@Override
public String toString() {
return getDelegate().toString();
}
};
}

View File

@@ -1,277 +1,281 @@
/*
* 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;
}
}
}
/*
* 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.Tracer;
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);
// order matters!
connection2.close();
assertThat(closingCountingListener.connectionCount).isEqualTo(1);
// order matters!
connection1.close();
assertThat(closingCountingListener.connectionCount).isEqualTo(2);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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

@@ -1,238 +1,246 @@
/*
* 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");
}
};
}
}
}
/*
* 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.Tracer;
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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@Test
void testGlobalConnectionIdManagerByDefault() {
contextRunner.run(context -> {
DataSource dataSource = context.getBean(DataSource.class);
ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource)
.getDecoratedDataSource();
assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@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

@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon
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.Tracer;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener;
@@ -50,6 +51,7 @@ class SleuthP6SpyListenerAutoConfigurationTests {
.createJdbcEventListener();
assertThat(jdbcEventListener.getEventListeners()).extracting("class")
.contains(TraceJdbcEventListener.class);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}

View File

@@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon
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.Tracer;
import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration;
import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener;
@@ -53,6 +54,7 @@ class SleuthProxyDataSourceListenerAutoConfigurationTests {
.getDecoratedDataSource();
ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener();
assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class);
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}

View File

@@ -36,22 +36,16 @@ import static org.assertj.core.api.Assertions.assertThat;
class TracingJdbcEventListenerTests extends TracingListenerStrategyTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
private static 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")
"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")));
super(contextRunner);
}
@Test

View File

@@ -35,6 +35,7 @@ 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.Tracer;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener;
import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener;
import org.springframework.context.annotation.Bean;
@@ -71,6 +72,7 @@ abstract class TracingListenerStrategyTests {
assertThat(connectionSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -92,6 +94,7 @@ abstract class TracingListenerStrategyTests {
assertThat(connectionSpan.remoteServiceName()).isEqualTo("aaaabbbb");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit");
assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -112,6 +115,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(statementSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -134,6 +138,7 @@ abstract class TracingListenerStrategyTests {
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");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -156,6 +161,7 @@ abstract class TracingListenerStrategyTests {
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");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -185,6 +191,7 @@ abstract class TracingListenerStrategyTests {
if (isP6Spy(context)) {
assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2");
}
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -212,6 +219,7 @@ abstract class TracingListenerStrategyTests {
if (isP6Spy(context)) {
assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1");
}
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -236,6 +244,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -259,6 +268,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -284,6 +294,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -312,6 +323,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -330,6 +342,7 @@ abstract class TracingListenerStrategyTests {
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -345,6 +358,7 @@ abstract class TracingListenerStrategyTests {
statement.executeQuery("SELECT NOW()");
}).isInstanceOf(SQLException.class);
connection.close();
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -362,6 +376,7 @@ abstract class TracingListenerStrategyTests {
}).isInstanceOf(SQLException.class);
statement.close();
connection.close();
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -373,14 +388,15 @@ abstract class TracingListenerStrategyTests {
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
connection1.close();
connection2.close();
connection1.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");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -406,6 +422,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -428,6 +445,7 @@ abstract class TracingListenerStrategyTests {
assertThat(listener).extracting("strategy").extracting("openConnections")
.isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty());
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -466,6 +484,7 @@ abstract class TracingListenerStrategyTests {
assertThat(spanReporter.spans()).hasSize(1 + 2 * 5);
assertThat(spanReporter.spans()).extracting("name").contains("select", "result-set", "connection");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -487,6 +506,7 @@ abstract class TracingListenerStrategyTests {
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -507,6 +527,7 @@ abstract class TracingListenerStrategyTests {
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -527,6 +548,7 @@ abstract class TracingListenerStrategyTests {
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan resultSetSpan = spanReporter.spans().get(0);
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -550,6 +572,7 @@ abstract class TracingListenerStrategyTests {
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -573,6 +596,7 @@ abstract class TracingListenerStrategyTests {
MutableSpan resultSetSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -596,6 +620,7 @@ abstract class TracingListenerStrategyTests {
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -608,12 +633,14 @@ abstract class TracingListenerStrategyTests {
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT NOW()");
connection.close();
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class);
assertThat(spanReporter.spans()).hasSize(1);
MutableSpan connectionSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -634,6 +661,7 @@ abstract class TracingListenerStrategyTests {
MutableSpan statementSpan = spanReporter.spans().get(0);
assertThat(connectionSpan.name()).isEqualTo("connection");
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}
@@ -659,6 +687,7 @@ abstract class TracingListenerStrategyTests {
assertThat(statementSpan.name()).isEqualTo("select");
assertThat(resultSetSpan.name()).isEqualTo("result-set");
assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()");
assertThat(context.getBean(Tracer.class).currentSpan()).isNull();
});
}

View File

@@ -1,402 +1,506 @@
/*
* 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);
}
}
}
/*
* 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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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 static final Log log = LogFactory.getLog(TraceListenerStrategy.class);
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) {
if (log.isTraceEnabled()) {
log.trace("Before get connection key [" + connectionKey + "] - current span is [" + tracer.currentSpan()
+ "]");
}
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));
if (log.isTraceEnabled()) {
log.trace("Started client span before connection [" + connectionSpan + "] - current span is ["
+ tracer.currentSpan() + "]");
}
}
ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope);
this.openConnections.put(connectionKey, connectionInfo);
}
void afterGetConnection(CON connectionKey, Connection connection, Throwable t) {
if (log.isTraceEnabled()) {
log.trace("After get connection [" + connectionKey + "]. Current span is [" + tracer.currentSpan() + "]");
}
this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> {
parseServerIpAndPort(connection, spanAndScope.getSpan());
});
if (t != null) {
ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey);
connectionInfo.getSpan().ifPresent(connectionSpan -> {
parseServerIpAndPort(connection, connectionSpan.getSpan());
if (log.isTraceEnabled()) {
log.trace("Closing client span due to exception [" + connectionSpan.getSpan()
+ "] - current span is [" + tracer.currentSpan() + "]");
}
connectionSpan.getSpan().error(t);
connectionSpan.close();
if (log.isTraceEnabled()) {
log.trace("Current span [" + tracer.currentSpan() + "]");
}
});
}
}
void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) {
if (log.isTraceEnabled()) {
log.trace("Before query - connection [" + connectionKey + "] and current span [" + tracer.currentSpan()
+ "]");
}
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
if (log.isTraceEnabled()) {
log.trace("Connection may be closed after statement preparation, but before statement execution");
}
return;
}
SpanAndScope spanAndScope = null;
if (traceTypes.contains(TraceType.QUERY)) {
Span.Builder statementSpanBuilder = AssertingSpanBuilder
.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder())
.name(String.format(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName(), "query"));
statementSpanBuilder.remoteServiceName(dataSourceName);
parseServerIpAndPort(connection, statementSpanBuilder);
statementSpanBuilder.kind(Span.Kind.CLIENT);
Span statementSpan = statementSpanBuilder.start();
spanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan));
if (log.isTraceEnabled()) {
log.trace("Started client span before query [" + statementSpan + "] - current span is ["
+ tracer.currentSpan() + "]");
}
}
StatementInfo statementInfo = new StatementInfo(spanAndScope);
connectionInfo.getNestedStatements().put(statementKey, statementInfo);
}
void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) {
if (log.isTraceEnabled()) {
log.trace("Add query row count for connection key [" + connectionKey + "]");
}
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
if (log.isTraceEnabled()) {
log.trace("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) {
if (log.isTraceEnabled()) {
log.trace("After query for connection key [" + connectionKey + "]");
}
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
if (connectionInfo == null) {
if (log.isTraceEnabled()) {
log.trace(
"Connection may be closed after statement preparation, but before statement execution. Current span is ["
+ tracer.currentSpan() + "]");
}
return;
}
StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey);
statementInfo.getSpan().ifPresent(statementSpan -> {
updateQuerySpan(sql, t, statementSpan);
statementSpan.close();
if (log.isTraceEnabled()) {
log.trace("Current span [" + tracer.currentSpan() + "]");
}
});
}
private void updateQuerySpan(String sql, Throwable t, SpanAndScope statementSpan) {
AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan())
.tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql));
if (t != null) {
statementSpan.getSpan().error(t);
}
if (log.isTraceEnabled()) {
log.trace(
"Closing statement span [" + statementSpan + "] - current span is [" + tracer.currentSpan() + "]");
}
}
void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey,
String dataSourceName) {
if (log.isTraceEnabled()) {
log.trace("Before result set next");
}
if (!traceTypes.contains(TraceType.FETCH)) {
return;
}
ConnectionInfo connectionInfo = openConnections.get(connectionKey);
// ConnectionInfo may be null if Connection was closed before ResultSet
if (connectionInfo == null) {
if (log.isTraceEnabled()) {
log.trace("No connection info, skipping");
}
return;
}
if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) {
if (log.isTraceEnabled()) {
log.trace("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));
if (log.isTraceEnabled()) {
log.trace("Started client result set span [" + resultSetSpan + "] - current span is ["
+ tracer.currentSpan() + "]");
}
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) {
if (log.isTraceEnabled()) {
log.trace("After result set close");
}
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);
}
if (log.isTraceEnabled()) {
log.trace("Closing client result set span [" + resultSetSpan + "] - current span is ["
+ tracer.currentSpan() + "]");
}
resultSetSpan.close();
if (log.isTraceEnabled()) {
log.trace("Current span [" + tracer.currentSpan() + "]");
}
}
void afterStatementClose(CON connectionKey, STMT statementKey) {
if (log.isTraceEnabled()) {
log.trace("After statement close");
}
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);
if (log.isTraceEnabled()) {
log.trace("Closing span after statement close [" + span.getSpan() + "] - current span is ["
+ tracer.currentSpan() + "]");
}
span.close();
if (log.isTraceEnabled()) {
log.trace("Current span [" + tracer.currentSpan() + "]");
}
});
statementInfo.getNestedResultSetSpans().clear();
}
}
void afterCommit(CON connectionKey, Throwable t) {
if (log.isTraceEnabled()) {
log.trace("After commit");
}
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) {
if (log.isTraceEnabled()) {
log.trace("After rollback");
}
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) {
if (log.isTraceEnabled()) {
log.trace("After connection close with key [" + connectionKey + "]");
}
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));
if (log.isTraceEnabled()) {
log.trace("Current span after closing statements [" + tracer.currentSpan() + "]");
}
connectionInfo.getSpan().ifPresent(connectionSpan -> {
if (t != null) {
connectionSpan.getSpan().error(t);
}
if (log.isTraceEnabled()) {
log.trace("Closing span after connection close [" + connectionSpan.getSpan() + "] - current span is ["
+ tracer.currentSpan() + "]");
}
connectionSpan.close();
if (log.isTraceEnabled()) {
log.trace("Current span [" + tracer.currentSpan() + "]");
}
});
}
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

@@ -121,6 +121,9 @@ public class TraceQueryExecutionListener implements QueryExecutionListener, Meth
try {
DataSource source = targetDataSource instanceof ProxyDataSource
? (targetDataSource).unwrap(DataSource.class) : targetDataSource;
if (source == null) {
return null;
}
return source.getConnection();
}
catch (Exception ex) {

View File

@@ -81,8 +81,8 @@ public class SleuthContextListener implements SmartApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent || event instanceof ContextClosedEvent) {
if (log.isDebugEnabled()) {
log.debug("Context refreshed or closed [" + event + "]");
if (log.isTraceEnabled()) {
log.trace("Context refreshed or closed [" + event + "]");
}
ApplicationContextEvent contextEvent = (ApplicationContextEvent) event;
ApplicationContext context = contextEvent.getApplicationContext();