Skip to content

Commit 54241b8

Browse files
author
Rajeswari
committed
Fix Nacos/Spring Boot config refresh error by proxying instrumented DataSource
1 parent a24bd0a commit 54241b8

File tree

2 files changed

+122
-37
lines changed

2 files changed

+122
-37
lines changed

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

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,46 +46,65 @@ private static boolean isRoutingDatasource(Object bean) {
4646
}
4747

4848
@CanIgnoreReturnValue
49-
@Override
50-
public Object postProcessAfterInitialization(Object bean, String beanName) {
51-
// Exclude scoped proxy beans to avoid double wrapping
52-
if (bean instanceof DataSource
53-
&& !isRoutingDatasource(bean)
54-
&& !ScopedProxyUtils.isScopedTarget(beanName)) {
55-
DataSource dataSource = (DataSource) bean;
56-
DataSource wrapped = JdbcTelemetry.builder(openTelemetryProvider.getObject())
57-
.setStatementSanitizationEnabled(
58-
InstrumentationConfigUtil.isStatementSanitizationEnabled(
59-
configPropertiesProvider.getObject(),
60-
"otel.instrumentation.jdbc.statement-sanitizer.enabled"))
61-
.setCaptureQueryParameters(
62-
configPropertiesProvider
63-
.getObject()
64-
.getBoolean(
65-
"otel.instrumentation.jdbc.experimental.capture-query-parameters", false))
66-
.setTransactionInstrumenterEnabled(
67-
configPropertiesProvider
68-
.getObject()
69-
.getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false))
70-
.build()
71-
.wrap(dataSource);
49+
@Override
50+
public Object postProcessAfterInitialization(Object bean, String beanName) {
51+
// Exclude scoped proxy beans to avoid double wrapping
52+
if (bean instanceof DataSource
53+
&& !isRoutingDatasource(bean)
54+
&& !ScopedProxyUtils.isScopedTarget(beanName)) {
55+
DataSource dataSource = (DataSource) bean;
7256

73-
ProxyFactory proxyFactory = new ProxyFactory(DataSource.class);
74-
proxyFactory.setTarget(bean);
75-
proxyFactory.addAdvice(
76-
new MethodInterceptor() {
77-
@Nullable
78-
@Override
79-
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
80-
return AopUtils.invokeJoinpointUsingReflection(
81-
wrapped, invocation.getMethod(), invocation.getArguments());
82-
}
83-
});
57+
// Wrap the original DataSource with OpenTelemetry instrumentation
58+
DataSource wrapped = JdbcTelemetry.builder(openTelemetryProvider.getObject())
59+
.setStatementSanitizationEnabled(
60+
InstrumentationConfigUtil.isStatementSanitizationEnabled(
61+
configPropertiesProvider.getObject(),
62+
"otel.instrumentation.jdbc.statement-sanitizer.enabled"))
63+
.setCaptureQueryParameters(
64+
configPropertiesProvider
65+
.getObject()
66+
.getBoolean(
67+
"otel.instrumentation.jdbc.experimental.capture-query-parameters", false))
68+
.setTransactionInstrumenterEnabled(
69+
configPropertiesProvider
70+
.getObject()
71+
.getBoolean("otel.instrumentation.jdbc.experimental.transaction.enabled", false))
72+
.build()
73+
.wrap(dataSource);
8474

85-
return proxyFactory.getProxy();
86-
}
87-
return bean;
75+
/**
76+
* Spring Boot's configuration binding and rebinding mechanisms (such as those triggered by
77+
* Nacos configuration refresh) may attempt to reconstruct beans using their concrete class
78+
* constructors. If a custom DataSource implementation (such as OpenTelemetryDataSource)
79+
* is returned directly, Spring may not find a suitable constructor during rebinding, resulting
80+
* in errors like "ExistingValue must be an instance of com.zaxxer.hikari.HikariDataSource".
81+
*
82+
* To prevent this, we create a JDK dynamic proxy implementing only the DataSource interface.
83+
* The proxy delegates all method calls to the wrapped (instrumented) DataSource.
84+
* This approach "hides" the actual implementation class and ensures that Spring interacts only
85+
* with the DataSource interface, avoiding issues related to constructor resolution or type casting
86+
* during bean rebinding.
87+
*/
88+
ProxyFactory proxyFactory = new ProxyFactory(DataSource.class);
89+
// Set the original bean as the target (important for AOP and bean lifecycle)
90+
proxyFactory.setTarget(bean);
91+
// Delegate all method calls to the wrapped, instrumented DataSource
92+
proxyFactory.addAdvice(
93+
new MethodInterceptor() {
94+
@Nullable
95+
@Override
96+
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
97+
return AopUtils.invokeJoinpointUsingReflection(
98+
wrapped, invocation.getMethod(), invocation.getArguments());
99+
}
100+
});
101+
102+
// Return the proxy instead of the instrumented DataSource instance
103+
// This ensures proper interaction with Spring's bean lifecycle and rebinding
104+
return proxyFactory.getProxy();
88105
}
106+
return bean;
107+
}
89108

90109
// To be one of the first bean post-processors to be executed
91110
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.jdbc;
2+
3+
import io.opentelemetry.api.OpenTelemetry;
4+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
5+
import org.junit.jupiter.api.Test;
6+
import org.springframework.beans.factory.ObjectProvider;
7+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
8+
9+
import javax.sql.DataSource;
10+
import java.sql.Connection;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.mockito.Mockito.*;
14+
15+
class DataSourcePostProcessorTest {
16+
17+
@Test
18+
void shouldWrapDataSourceWithProxy() throws Exception {
19+
// Set up mocks
20+
DataSource original = mock(DataSource.class);
21+
Connection mockConn = mock(Connection.class);
22+
when(original.getConnection()).thenReturn(mockConn);
23+
24+
// Create mocks for OpenTelemetry and ConfigProperties
25+
OpenTelemetry openTelemetry = mock(OpenTelemetry.class);
26+
ConfigProperties configProperties = mock(ConfigProperties.class);
27+
when(configProperties.getBoolean(anyString(), anyBoolean())).thenReturn(false);
28+
29+
// Mock JdbcTelemetry builder chain to return a mock DataSource as 'wrapped'
30+
JdbcTelemetry.Builder builder = mock(JdbcTelemetry.Builder.class, RETURNS_SELF);
31+
JdbcTelemetry telemetry = mock(JdbcTelemetry.class);
32+
DataSource wrapped = mock(DataSource.class);
33+
when(wrapped.getConnection()).thenReturn(mockConn);
34+
when(telemetry.wrap(any(DataSource.class))).thenReturn(wrapped);
35+
when(builder.build()).thenReturn(telemetry);
36+
37+
// Mock the static builder method (requires mockito-inline or PowerMockito)
38+
try (var mocked = Mockito.mockStatic(JdbcTelemetry.class)) {
39+
mocked.when(() -> JdbcTelemetry.builder(any())).thenReturn(builder);
40+
41+
ObjectProvider<OpenTelemetry> openTelemetryProvider = () -> openTelemetry;
42+
ObjectProvider<ConfigProperties> configPropertiesProvider = () -> configProperties;
43+
44+
DataSourcePostProcessor postProcessor = new DataSourcePostProcessor(
45+
openTelemetryProvider, configPropertiesProvider
46+
);
47+
48+
// Act
49+
Object processed = postProcessor.postProcessAfterInitialization(original, "myDataSource");
50+
51+
// Assert
52+
assertThat(processed).isInstanceOf(DataSource.class);
53+
assertThat(processed).isNotSameAs(original);
54+
55+
// The proxy should delegate calls to the wrapped DataSource
56+
DataSource proxied = (DataSource) processed;
57+
Connection proxiedConnection = proxied.getConnection();
58+
assertThat(proxiedConnection).isEqualTo(mockConn);
59+
60+
// The original DataSource should NOT be called directly by the proxy
61+
verify(original, never()).getConnection();
62+
// The wrapped DataSource should be called
63+
verify(wrapped, times(1)).getConnection();
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)