Wrap instrumented data source in spring starter (#14255)

This commit is contained in:
Lauri Tulmin 2025-08-12 02:00:11 +03:00 committed by GitHub
parent e147999616
commit b02219c206
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 160 additions and 16 deletions

View File

@ -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"))

View File

@ -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,7 +57,8 @@ final class DataSourcePostProcessor implements BeanPostProcessor, Ordered {
&& !isRoutingDatasource(bean)
&& !ScopedProxyUtils.isScopedTarget(beanName)) {
DataSource dataSource = (DataSource) bean;
return JdbcTelemetry.builder(openTelemetryProvider.getObject())
DataSource otelDataSource =
JdbcTelemetry.builder(openTelemetryProvider.getObject())
.setStatementSanitizationEnabled(
InstrumentationConfigUtil.isStatementSanitizationEnabled(
configPropertiesProvider.getObject(),
@ -63,9 +71,14 @@ final class DataSourcePostProcessor implements BeanPostProcessor, Ordered {
.setTransactionInstrumenterEnabled(
configPropertiesProvider
.getObject()
.getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false))
.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> T unwrap(Class<T> iface) throws SQLException {
return delegate.unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return delegate.isWrapperFor(iface);
}
}
}

View File

@ -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 ?")));
});
}
}