Skip to content

Commit ddf7d67

Browse files
Copilottrask
andauthored
Preserve JDBC instrumentation when unwrapping standard interfaces (#20947)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: trask <218610+trask@users.noreply.github.com>
1 parent 1073839 commit ddf7d67

4 files changed

Lines changed: 120 additions & 3 deletions

File tree

‎instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryConnection.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,15 @@ public int getNetworkTimeout() throws SQLException {
454454

455455
@Override
456456
public <T> T unwrap(Class<T> iface) throws SQLException {
457+
if (iface.isInstance(this)) {
458+
return iface.cast(this);
459+
}
457460
return delegate.unwrap(iface);
458461
}
459462

460463
@Override
461464
public boolean isWrapperFor(Class<?> iface) throws SQLException {
462-
return delegate.isWrapperFor(iface);
465+
return iface.isInstance(this) || delegate.isWrapperFor(iface);
463466
}
464467

465468
// visible for testing

‎instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryResultSet.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,11 +1030,14 @@ public void updateObject(String columnLabel, Object x, SQLType targetSqlType)
10301030

10311031
@Override
10321032
public <T> T unwrap(Class<T> iface) throws SQLException {
1033+
if (iface.isInstance(this)) {
1034+
return iface.cast(this);
1035+
}
10331036
return delegate.unwrap(iface);
10341037
}
10351038

10361039
@Override
10371040
public boolean isWrapperFor(Class<?> iface) throws SQLException {
1038-
return delegate.isWrapperFor(iface);
1041+
return iface.isInstance(this) || delegate.isWrapperFor(iface);
10391042
}
10401043
}

‎instrumentation/jdbc/library/src/main/java/io/opentelemetry/instrumentation/jdbc/internal/OpenTelemetryStatement.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,15 @@ public boolean isCloseOnCompletion() throws SQLException {
307307

308308
@Override
309309
public <T> T unwrap(Class<T> iface) throws SQLException {
310+
if (iface.isInstance(this)) {
311+
return iface.cast(this);
312+
}
310313
return delegate.unwrap(iface);
311314
}
312315

313316
@Override
314317
public boolean isWrapperFor(Class<?> iface) throws SQLException {
315-
return delegate.isWrapperFor(iface);
318+
return iface.isInstance(this) || delegate.isWrapperFor(iface);
316319
}
317320

318321
// JDBC 4.2

‎instrumentation/jdbc/library/src/test/java/io/opentelemetry/instrumentation/jdbc/internal/WrapperTest.java‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
77

88
import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createStatementInstrumenter;
99
import static io.opentelemetry.instrumentation.jdbc.internal.JdbcInstrumenterFactory.createTransactionInstrumenter;
10+
import static java.util.stream.Collectors.toList;
1011
import static org.assertj.core.api.Assertions.assertThat;
1112
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1213
import static org.assertj.core.api.Assertions.fail;
1314
import static org.junit.jupiter.api.Assumptions.assumeTrue;
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.verify;
17+
import static org.mockito.Mockito.verifyNoInteractions;
18+
import static org.mockito.Mockito.when;
1419

1520
import io.opentelemetry.api.OpenTelemetry;
1621
import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenter;
@@ -26,9 +31,14 @@
2631
import java.sql.ResultSet;
2732
import java.sql.SQLException;
2833
import java.sql.Statement;
34+
import java.sql.Wrapper;
2935
import java.util.ArrayList;
3036
import java.util.List;
37+
import java.util.stream.Stream;
3138
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.params.ParameterizedTest;
40+
import org.junit.jupiter.params.provider.Arguments;
41+
import org.junit.jupiter.params.provider.MethodSource;
3242

3343
class WrapperTest {
3444

@@ -112,6 +122,102 @@ void jdbc45MethodsPropagateSqlException() throws Exception {
112122
.isSameAs(expected);
113123
}
114124

125+
@ParameterizedTest
126+
@MethodSource("wrappers")
127+
void supportedInterfacesReturnWrapperWithoutConsultingDelegate(Wrapper wrapper, Wrapper delegate)
128+
throws SQLException {
129+
for (Class<?> iface : supportedInterfaces(wrapper)) {
130+
assertThat(wrapper.unwrap(iface)).isSameAs(wrapper);
131+
assertThat(wrapper.isWrapperFor(iface)).isTrue();
132+
}
133+
verifyNoInteractions(delegate);
134+
}
135+
136+
@ParameterizedTest
137+
@MethodSource("wrappers")
138+
void delegatesVendorInterfaces(Wrapper wrapper, Wrapper delegate) throws SQLException {
139+
VendorJdbcObject vendorObject = mock(VendorJdbcObject.class);
140+
when(delegate.unwrap(VendorJdbcObject.class)).thenReturn(vendorObject);
141+
when(delegate.isWrapperFor(VendorJdbcObject.class)).thenReturn(true);
142+
143+
assertThat(wrapper.unwrap(VendorJdbcObject.class)).isSameAs(vendorObject);
144+
assertThat(wrapper.isWrapperFor(VendorJdbcObject.class)).isTrue();
145+
verify(delegate).unwrap(VendorJdbcObject.class);
146+
verify(delegate).isWrapperFor(VendorJdbcObject.class);
147+
}
148+
149+
@ParameterizedTest
150+
@MethodSource("wrappers")
151+
void preservesUnsupportedInterfaceBehavior(Wrapper wrapper, Wrapper delegate)
152+
throws SQLException {
153+
SQLException exception = new SQLException("Unsupported interface");
154+
when(delegate.unwrap(Runnable.class)).thenThrow(exception);
155+
when(delegate.isWrapperFor(Runnable.class)).thenReturn(false);
156+
157+
assertThatThrownBy(() -> wrapper.unwrap(Runnable.class)).isSameAs(exception);
158+
assertThat(wrapper.isWrapperFor(Runnable.class)).isFalse();
159+
verify(delegate).unwrap(Runnable.class);
160+
verify(delegate).isWrapperFor(Runnable.class);
161+
162+
when(delegate.isWrapperFor(Runnable.class)).thenThrow(exception);
163+
assertThatThrownBy(() -> wrapper.isWrapperFor(Runnable.class)).isSameAs(exception);
164+
}
165+
166+
private static List<Class<?>> supportedInterfaces(Wrapper wrapper) {
167+
return Stream.of(
168+
Connection.class,
169+
Statement.class,
170+
PreparedStatement.class,
171+
CallableStatement.class,
172+
ResultSet.class,
173+
Wrapper.class,
174+
AutoCloseable.class)
175+
.filter(iface -> iface.isInstance(wrapper))
176+
.collect(toList());
177+
}
178+
179+
private static Stream<Arguments> wrappers() {
180+
Connection connectionDelegate = mock(Connection.class);
181+
OpenTelemetryConnection connection = (OpenTelemetryConnection) wrap(connectionDelegate);
182+
Statement statementDelegate = mock(Statement.class);
183+
PreparedStatement preparedDelegate = mock(PreparedStatement.class);
184+
CallableStatement callableDelegate = mock(CallableStatement.class);
185+
ResultSet resultSetDelegate = mock(ResultSet.class);
186+
DbInfo dbInfo = DbInfo.builder().build();
187+
Statement statement =
188+
new OpenTelemetryStatement<>(
189+
statementDelegate,
190+
connection,
191+
dbInfo,
192+
connection.statementInstrumenter,
193+
SqlCommenter.noop());
194+
195+
return Stream.of(
196+
Arguments.of(connection, connectionDelegate),
197+
Arguments.of(statement, statementDelegate),
198+
Arguments.of(
199+
new OpenTelemetryPreparedStatement<>(
200+
preparedDelegate,
201+
connection,
202+
dbInfo,
203+
"SELECT 1",
204+
connection.statementInstrumenter,
205+
false,
206+
SqlCommenter.noop()),
207+
preparedDelegate),
208+
Arguments.of(
209+
new OpenTelemetryCallableStatement<>(
210+
callableDelegate,
211+
connection,
212+
dbInfo,
213+
"CALL test()",
214+
connection.statementInstrumenter,
215+
false,
216+
SqlCommenter.noop()),
217+
callableDelegate),
218+
Arguments.of(OpenTelemetryResultSet.wrap(resultSetDelegate, statement), resultSetDelegate));
219+
}
220+
115221
private static Connection wrap(Connection delegate) {
116222
OpenTelemetry noop = OpenTelemetry.noop();
117223
return OpenTelemetryConnection.create(
@@ -132,6 +238,8 @@ private static Object invokeConnectionMethod(Connection wrapper, String name, St
132238
return Connection.class.getMethod(name, String.class).invoke(wrapper, arg);
133239
}
134240

241+
private interface VendorJdbcObject {}
242+
135243
void validate(Class<?> jdbcClass, Class<?> wrapperClass) throws NoSuchMethodException {
136244
for (Method method : jdbcClass.getMethods()) {
137245
Method result = wrapperClass.getMethod(method.getName(), method.getParameterTypes());

0 commit comments

Comments
 (0)