diff --git a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts index 681fdcb45b..8ac5e0c572 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") library("org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion") library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion") + library("org.springframework.boot:spring-boot-starter-data-jdbc:$springBootVersion") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") implementation(project(":sdk-autoconfigure-support")) diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java index 2435fbc2ba..a1a615a31f 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java @@ -10,7 +10,14 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetry; import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.logging.Logger; import javax.sql.DataSource; +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.AdvisedSupport; import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -50,22 +57,28 @@ final class DataSourcePostProcessor implements BeanPostProcessor, Ordered { && !isRoutingDatasource(bean) && !ScopedProxyUtils.isScopedTarget(beanName)) { DataSource dataSource = (DataSource) bean; - return JdbcTelemetry.builder(openTelemetryProvider.getObject()) - .setStatementSanitizationEnabled( - InstrumentationConfigUtil.isStatementSanitizationEnabled( - configPropertiesProvider.getObject(), - "otel.instrumentation.jdbc.statement-sanitizer.enabled")) - .setCaptureQueryParameters( - configPropertiesProvider - .getObject() - .getBoolean( - "otel.instrumentation.jdbc.experimental.capture-query-parameters", false)) - .setTransactionInstrumenterEnabled( - configPropertiesProvider - .getObject() - .getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false)) - .build() - .wrap(dataSource); + DataSource otelDataSource = + JdbcTelemetry.builder(openTelemetryProvider.getObject()) + .setStatementSanitizationEnabled( + InstrumentationConfigUtil.isStatementSanitizationEnabled( + configPropertiesProvider.getObject(), + "otel.instrumentation.jdbc.statement-sanitizer.enabled")) + .setCaptureQueryParameters( + configPropertiesProvider + .getObject() + .getBoolean( + "otel.instrumentation.jdbc.experimental.capture-query-parameters", false)) + .setTransactionInstrumenterEnabled( + configPropertiesProvider + .getObject() + .getBoolean( + "otel.instrumentation.jdbc.experimental.transaction.enabled", false)) + .build() + .wrap(dataSource); + + // wrap instrumented data source into a proxy that unwraps to the original data source + // see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13512 + return new DataSource$$Wrapper(otelDataSource, dataSource); } return bean; } @@ -75,4 +88,65 @@ final class DataSourcePostProcessor implements BeanPostProcessor, Ordered { public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 20; } + + // Wrapper for DataSource that pretends to be a spring aop proxy. $$ in class name is commonly + // used by bytecode proxies and is tested by + // org.springframework.aop.support.AopUtils.isAopProxy(). This proxy can be unwrapped with + // ((Advised) dataSource).getTargetSource().getTarget() and it unwraps to the original data + // source. + @SuppressWarnings("checkstyle:TypeName") + private static class DataSource$$Wrapper extends AdvisedSupport + implements SpringProxy, DataSource { + private final DataSource delegate; + + DataSource$$Wrapper(DataSource delegate, DataSource original) { + this.delegate = delegate; + setTarget(original); + } + + @Override + public Connection getConnection() throws SQLException { + return delegate.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return delegate.getConnection(username, password); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return delegate.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + delegate.setLogWriter(out); + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + delegate.setLoginTimeout(seconds); + } + + @Override + public int getLoginTimeout() throws SQLException { + return delegate.getLoginTimeout(); + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return delegate.getParentLogger(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return delegate.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return delegate.isWrapperFor(iface); + } + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfigurationTest.java new file mode 100644 index 0000000000..e269ea8369 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/JdbcInstrumentationAutoConfigurationTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc; + +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import java.sql.Connection; +import java.sql.Statement; +import java.util.Collections; +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class JdbcInstrumentationAutoConfigurationTest { + + @RegisterExtension + static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private final ApplicationContextRunner runner = + new ApplicationContextRunner() + .withBean( + ConfigProperties.class, + () -> DefaultConfigProperties.createFromMap(Collections.emptyMap())) + .withConfiguration( + AutoConfigurations.of( + JdbcInstrumentationAutoConfiguration.class, DataSourceAutoConfiguration.class)) + .withBean("openTelemetry", OpenTelemetry.class, testing::getOpenTelemetry); + + @SuppressWarnings("deprecation") // using deprecated semconv + @Test + void statementSanitizerEnabledByDefault() { + runner.run( + context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(AopUtils.isAopProxy(dataSource)).isTrue(); + assertThat(dataSource.getClass().getSimpleName()).isNotEqualTo("HikariDataSource"); + // unwrap the instrumented data source to get the original data source + Object original = ((Advised) dataSource).getTargetSource().getTarget(); + assertThat(AopUtils.isAopProxy(original)).isFalse(); + assertThat(original.getClass().getSimpleName()).isEqualTo("HikariDataSource"); + + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + statement.execute("SELECT 1"); + } + } + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasAttribute(maybeStable(DB_STATEMENT), "SELECT ?"))); + }); + } +}