Skip to content

Commit b02219c

Browse files
authored
Wrap instrumented data source in spring starter (#14255)
1 parent e147999 commit b02219c

File tree

3 files changed

+160
-16
lines changed

3 files changed

+160
-16
lines changed

instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
library("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion")
6464
library("org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion")
6565
library("org.springframework.boot:spring-boot-starter-data-r2dbc:$springBootVersion")
66+
library("org.springframework.boot:spring-boot-starter-data-jdbc:$springBootVersion")
6667

6768
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
6869
implementation(project(":sdk-autoconfigure-support"))

instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/jdbc/DataSourcePostProcessor.java

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
import io.opentelemetry.instrumentation.jdbc.datasource.JdbcTelemetry;
1111
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil;
1212
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
13+
import java.io.PrintWriter;
14+
import java.sql.Connection;
15+
import java.sql.SQLException;
16+
import java.sql.SQLFeatureNotSupportedException;
17+
import java.util.logging.Logger;
1318
import javax.sql.DataSource;
19+
import org.springframework.aop.SpringProxy;
20+
import org.springframework.aop.framework.AdvisedSupport;
1421
import org.springframework.aop.scope.ScopedProxyUtils;
1522
import org.springframework.beans.factory.ObjectProvider;
1623
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -50,22 +57,28 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
5057
&& !isRoutingDatasource(bean)
5158
&& !ScopedProxyUtils.isScopedTarget(beanName)) {
5259
DataSource dataSource = (DataSource) bean;
53-
return JdbcTelemetry.builder(openTelemetryProvider.getObject())
54-
.setStatementSanitizationEnabled(
55-
InstrumentationConfigUtil.isStatementSanitizationEnabled(
56-
configPropertiesProvider.getObject(),
57-
"otel.instrumentation.jdbc.statement-sanitizer.enabled"))
58-
.setCaptureQueryParameters(
59-
configPropertiesProvider
60-
.getObject()
61-
.getBoolean(
62-
"otel.instrumentation.jdbc.experimental.capture-query-parameters", false))
63-
.setTransactionInstrumenterEnabled(
64-
configPropertiesProvider
65-
.getObject()
66-
.getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false))
67-
.build()
68-
.wrap(dataSource);
60+
DataSource otelDataSource =
61+
JdbcTelemetry.builder(openTelemetryProvider.getObject())
62+
.setStatementSanitizationEnabled(
63+
InstrumentationConfigUtil.isStatementSanitizationEnabled(
64+
configPropertiesProvider.getObject(),
65+
"otel.instrumentation.jdbc.statement-sanitizer.enabled"))
66+
.setCaptureQueryParameters(
67+
configPropertiesProvider
68+
.getObject()
69+
.getBoolean(
70+
"otel.instrumentation.jdbc.experimental.capture-query-parameters", false))
71+
.setTransactionInstrumenterEnabled(
72+
configPropertiesProvider
73+
.getObject()
74+
.getBoolean(
75+
"otel.instrumentation.jdbc.experimental.transaction.enabled", false))
76+
.build()
77+
.wrap(dataSource);
78+
79+
// wrap instrumented data source into a proxy that unwraps to the original data source
80+
// see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/13512
81+
return new DataSource$$Wrapper(otelDataSource, dataSource);
6982
}
7083
return bean;
7184
}
@@ -75,4 +88,65 @@ public Object postProcessAfterInitialization(Object bean, String beanName) {
7588
public int getOrder() {
7689
return Ordered.LOWEST_PRECEDENCE - 20;
7790
}
91+
92+
// Wrapper for DataSource that pretends to be a spring aop proxy. $$ in class name is commonly
93+
// used by bytecode proxies and is tested by
94+
// org.springframework.aop.support.AopUtils.isAopProxy(). This proxy can be unwrapped with
95+
// ((Advised) dataSource).getTargetSource().getTarget() and it unwraps to the original data
96+
// source.
97+
@SuppressWarnings("checkstyle:TypeName")
98+
private static class DataSource$$Wrapper extends AdvisedSupport
99+
implements SpringProxy, DataSource {
100+
private final DataSource delegate;
101+
102+
DataSource$$Wrapper(DataSource delegate, DataSource original) {
103+
this.delegate = delegate;
104+
setTarget(original);
105+
}
106+
107+
@Override
108+
public Connection getConnection() throws SQLException {
109+
return delegate.getConnection();
110+
}
111+
112+
@Override
113+
public Connection getConnection(String username, String password) throws SQLException {
114+
return delegate.getConnection(username, password);
115+
}
116+
117+
@Override
118+
public PrintWriter getLogWriter() throws SQLException {
119+
return delegate.getLogWriter();
120+
}
121+
122+
@Override
123+
public void setLogWriter(PrintWriter out) throws SQLException {
124+
delegate.setLogWriter(out);
125+
}
126+
127+
@Override
128+
public void setLoginTimeout(int seconds) throws SQLException {
129+
delegate.setLoginTimeout(seconds);
130+
}
131+
132+
@Override
133+
public int getLoginTimeout() throws SQLException {
134+
return delegate.getLoginTimeout();
135+
}
136+
137+
@Override
138+
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
139+
return delegate.getParentLogger();
140+
}
141+
142+
@Override
143+
public <T> T unwrap(Class<T> iface) throws SQLException {
144+
return delegate.unwrap(iface);
145+
}
146+
147+
@Override
148+
public boolean isWrapperFor(Class<?> iface) throws SQLException {
149+
return delegate.isWrapperFor(iface);
150+
}
151+
}
78152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc;
7+
8+
import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable;
9+
import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_STATEMENT;
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
import io.opentelemetry.api.OpenTelemetry;
13+
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
14+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
15+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
16+
import java.sql.Connection;
17+
import java.sql.Statement;
18+
import java.util.Collections;
19+
import javax.sql.DataSource;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.RegisterExtension;
22+
import org.springframework.aop.framework.Advised;
23+
import org.springframework.aop.support.AopUtils;
24+
import org.springframework.boot.autoconfigure.AutoConfigurations;
25+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
26+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
27+
28+
class JdbcInstrumentationAutoConfigurationTest {
29+
30+
@RegisterExtension
31+
static final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create();
32+
33+
private final ApplicationContextRunner runner =
34+
new ApplicationContextRunner()
35+
.withBean(
36+
ConfigProperties.class,
37+
() -> DefaultConfigProperties.createFromMap(Collections.emptyMap()))
38+
.withConfiguration(
39+
AutoConfigurations.of(
40+
JdbcInstrumentationAutoConfiguration.class, DataSourceAutoConfiguration.class))
41+
.withBean("openTelemetry", OpenTelemetry.class, testing::getOpenTelemetry);
42+
43+
@SuppressWarnings("deprecation") // using deprecated semconv
44+
@Test
45+
void statementSanitizerEnabledByDefault() {
46+
runner.run(
47+
context -> {
48+
DataSource dataSource = context.getBean(DataSource.class);
49+
50+
assertThat(AopUtils.isAopProxy(dataSource)).isTrue();
51+
assertThat(dataSource.getClass().getSimpleName()).isNotEqualTo("HikariDataSource");
52+
// unwrap the instrumented data source to get the original data source
53+
Object original = ((Advised) dataSource).getTargetSource().getTarget();
54+
assertThat(AopUtils.isAopProxy(original)).isFalse();
55+
assertThat(original.getClass().getSimpleName()).isEqualTo("HikariDataSource");
56+
57+
try (Connection connection = dataSource.getConnection()) {
58+
try (Statement statement = connection.createStatement()) {
59+
statement.execute("SELECT 1");
60+
}
61+
}
62+
63+
testing.waitAndAssertTraces(
64+
trace ->
65+
trace.hasSpansSatisfyingExactly(
66+
span -> span.hasAttribute(maybeStable(DB_STATEMENT), "SELECT ?")));
67+
});
68+
}
69+
}

0 commit comments

Comments
 (0)