diff --git a/instrumentation/jdbc/build.gradle.kts b/instrumentation/jdbc/build.gradle.kts index f1416fd72..6e5860568 100644 --- a/instrumentation/jdbc/build.gradle.kts +++ b/instrumentation/jdbc/build.gradle.kts @@ -16,10 +16,16 @@ dependencies { testInstrumentation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-jdbc") - testLibrary("com.microsoft.sqlserver:mssql-jdbc:6.1.0.jre8") testCompileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") testImplementation("org.testcontainers:testcontainers") + + // SQL Server + testLibrary("com.microsoft.sqlserver:mssql-jdbc:6.1.0.jre8") testImplementation("org.testcontainers:mssqlserver") + + // Oracle + testLibrary("com.oracle.database.jdbc:ojdbc8:23.9.0.25.07") + testImplementation("org.testcontainers:oracle-free") } tasks.withType().configureEach { diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerUtil.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagator.java similarity index 73% rename from instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerUtil.java rename to instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagator.java index 5a080db84..5c08314e0 100644 --- a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerUtil.java +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagator.java @@ -14,23 +14,22 @@ * limitations under the License. */ -package com.splunk.opentelemetry.instrumentation.jdbc.sqlserver; +package com.splunk.opentelemetry.instrumentation.jdbc; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.util.VirtualField; -import java.nio.charset.StandardCharsets; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; -public final class SqlServerUtil { +public abstract class AbstractDbContextPropagator { private static final VirtualField connectionState = VirtualField.find(Connection.class, String.class); - public static void propagateContext(Connection connection) throws SQLException { + public void propagateContext(Connection connection) throws SQLException { AtomicReference state = new AtomicReference<>(); W3CTraceContextPropagator.getInstance() .inject( @@ -46,17 +45,13 @@ public static void propagateContext(Connection connection) throws SQLException { if (traceparent == null && existingTraceparent != null) { // we need to clear existing tracing state from the connection connectionState.set(connection, null); - setContextInfo(connection, new byte[0]); + setContext(connection, null); } else if (!Objects.equals(traceparent, existingTraceparent)) { connectionState.set(connection, traceparent); - setContextInfo(connection, traceparent.getBytes(StandardCharsets.UTF_8)); + setContext(connection, traceparent); } } - private static void setContextInfo(Connection connection, byte[] contextInfo) - throws SQLException { - PreparedStatement statement = connection.prepareStatement("set context_info ?"); - statement.setBytes(1, contextInfo); - statement.executeUpdate(); - } + protected abstract void setContext(Connection connection, @Nullable String contextInfo) + throws SQLException; } diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleContextPropagator.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleContextPropagator.java new file mode 100644 index 000000000..e28f7a9e2 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleContextPropagator.java @@ -0,0 +1,34 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc.oracle; + +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagator; +import java.sql.Connection; +import java.sql.SQLException; +import javax.annotation.Nullable; + +public final class OracleContextPropagator extends AbstractDbContextPropagator { + public static final OracleContextPropagator INSTANCE = new OracleContextPropagator(); + + private OracleContextPropagator() {} + + @Override + protected void setContext(Connection connection, @Nullable String contextInfo) + throws SQLException { + connection.setClientInfo("OCSID.ACTION", contextInfo); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleInstrumentationModule.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleInstrumentationModule.java new file mode 100644 index 000000000..3c4babebd --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleInstrumentationModule.java @@ -0,0 +1,54 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc.oracle; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class OracleInstrumentationModule extends InstrumentationModule { + + public OracleInstrumentationModule() { + super("splunk-jdbc", "splunk-jdbc-oracle"); + } + + @Override + public int order() { + // run after jdbc instrumentation + return 2; + } + + @Override + public boolean defaultEnabled(ConfigProperties config) { + return false; + } + + @Override + public List typeInstrumentations() { + return singletonList(new OracleStatementInstrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("com.splunk.opentelemetry.instrumentation"); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleStatementInstrumentation.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleStatementInstrumentation.java new file mode 100644 index 000000000..b2cc499c0 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleStatementInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc.oracle; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.sql.Statement; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OracleStatementInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "oracle.jdbc.driver.OraclePreparedStatementWrapper", + "oracle.jdbc.driver.OracleStatementWrapper"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic() + .and( + nameStartsWith("execute") + .and(takesNoArguments().or(takesArgument(0, String.class)))), + this.getClass().getName() + "$SetActionAdvice"); + } + + @SuppressWarnings("unused") + public static class SetActionAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Statement statement, @Advice.Local("splunkCallDepth") CallDepth callDepth) + throws Exception { + callDepth = CallDepth.forClass(OracleContextPropagator.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + OracleContextPropagator.INSTANCE.propagateContext(statement.getConnection()); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Local("splunkCallDepth") CallDepth callDepth) { + callDepth.decrementAndGet(); + } + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerContextPropagator.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerContextPropagator.java new file mode 100644 index 000000000..56dfd13e1 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerContextPropagator.java @@ -0,0 +1,40 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc.sqlserver; + +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagator; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import javax.annotation.Nullable; + +public final class SqlServerContextPropagator extends AbstractDbContextPropagator { + public static final SqlServerContextPropagator INSTANCE = new SqlServerContextPropagator(); + + private SqlServerContextPropagator() {} + + @Override + protected void setContext(Connection connection, @Nullable String contextInfo) + throws SQLException { + byte[] contextBytes = + contextInfo == null ? new byte[0] : contextInfo.getBytes(StandardCharsets.UTF_8); + PreparedStatement statement = connection.prepareStatement("set context_info ?"); + statement.setBytes(1, contextBytes); + statement.executeUpdate(); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerStatementInstrumentation.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerStatementInstrumentation.java index e4c5bd2d0..56edfce24 100644 --- a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerStatementInstrumentation.java +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerStatementInstrumentation.java @@ -55,12 +55,12 @@ public static class SetContextAdvice { public static void onEnter( @Advice.This Statement statement, @Advice.Local("splunkCallDepth") CallDepth callDepth) throws Exception { - callDepth = CallDepth.forClass(SqlServerUtil.class); + callDepth = CallDepth.forClass(SqlServerContextPropagator.class); if (callDepth.getAndIncrement() > 0) { return; } - SqlServerUtil.propagateContext(statement.getConnection()); + SqlServerContextPropagator.INSTANCE.propagateContext(statement.getConnection()); } @Advice.OnMethodExit(suppress = Throwable.class) diff --git a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagationTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagationTest.java new file mode 100644 index 000000000..3712803c1 --- /dev/null +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractDbContextPropagationTest.java @@ -0,0 +1,349 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public abstract class AbstractDbContextPropagationTest { + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + protected abstract InstrumentationExtension getTesting(); + + private static List contextPropagationArguments() { + return Arrays.asList( + // Statement tests + Arguments.of( + named( + "Statement.executeQuery", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeQuery("SELECT 1"); + } + })), + Arguments.of( + named( + "Statement.executeUpdate", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("INSERT INTO test_table VALUES(1)"); + } + })), + Arguments.of( + named( + "Statement.executeUpdate with autoGeneratedKeys", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); + } + })), + Arguments.of( + named( + "Statement.executeUpdate with columnIndexes", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate("INSERT INTO test_table VALUES(1)", new int[] {1}); + } + })), + Arguments.of( + named( + "Statement.executeUpdate with columnNames", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "INSERT INTO test_table VALUES(1)", new String[] {"value"}); + } + })), + Arguments.of( + named( + "Statement.execute", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("SELECT 1"); + } + })), + Arguments.of( + named( + "Statement.execute with autoGeneratedKeys", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); + } + })), + Arguments.of( + named( + "Statement.execute with columnIndexes", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute("INSERT INTO test_table VALUES(1)", new int[] {1}); + } + })), + Arguments.of( + named( + "Statement.execute with columnNames", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO test_table VALUES(1)", new String[] {"value"}); + } + })), + Arguments.of( + named( + "Statement.executeBatch", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.addBatch("INSERT INTO test_table VALUES(1)"); + statement.executeBatch(); + } + })), + Arguments.of( + named( + "Statement.executeLargeBatch", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.addBatch("INSERT INTO test_table VALUES(1)"); + statement.executeLargeBatch(); + } + })), + Arguments.of( + named( + "Statement.executeLargeUpdate", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeLargeUpdate("INSERT INTO test_table VALUES(1)"); + } + })), + Arguments.of( + named( + "Statement.executeLargeUpdate with autoGeneratedKeys", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeLargeUpdate( + "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); + } + })), + Arguments.of( + named( + "Statement.executeLargeUpdate with columnIndexes", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeLargeUpdate( + "INSERT INTO test_table VALUES(1)", new int[] {1}); + } + })), + Arguments.of( + named( + "Statement.executeLargeUpdate with columnNames", + (Action) + connection -> { + try (Statement statement = connection.createStatement()) { + statement.executeLargeUpdate( + "INSERT INTO test_table VALUES(1)", new String[] {"value"}); + } + })), + // PreparedStatement tests + Arguments.of( + named( + "PreparedStatement.executeQuery", + (Action) + connection -> { + try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { + statement.executeQuery(); + } + })), + Arguments.of( + named( + "PreparedStatement.executeUpdate", + (Action) + connection -> { + try (PreparedStatement statement = + connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { + statement.executeUpdate(); + } + })), + Arguments.of( + named( + "PreparedStatement.execute", + (Action) + connection -> { + try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { + statement.execute(); + } + })), + Arguments.of( + named( + "PreparedStatement.executeLargeUpdate", + (Action) + connection -> { + try (PreparedStatement statement = + connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { + statement.executeLargeUpdate(); + } + })), + Arguments.of( + named( + "PreparedStatement.executeBatch", + (Action) + connection -> { + try (PreparedStatement statement = + connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { + statement.addBatch(); + statement.executeBatch(); + } + })), + Arguments.of( + named( + "PreparedStatement.executeLargeBatch", + (Action) + connection -> { + try (PreparedStatement statement = + connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { + statement.addBatch(); + statement.executeLargeBatch(); + } + })), + // CallableStatement tests + Arguments.of( + named( + "CallableStatement.execute", + (Action) + connection -> { + try (CallableStatement statement = connection.prepareCall("SELECT 1")) { + statement.execute(); + } + }))); + } + + protected abstract Connection newConnection() throws SQLException; + + @ParameterizedTest + @MethodSource("contextPropagationArguments") + void contextPropagation(Action action) throws Exception { + Connection connection = newConnection(); + cleanup.deferCleanup(connection); + + assertNoContext(connection); + + getTesting().runWithSpan("parent", () -> action.accept(connection)); + + AtomicReference jdbcSpan = new AtomicReference<>(); + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasNoParent(), + span -> { + span.hasParent(trace.getSpan(0)); + jdbcSpan.set(span.actual().getSpanContext()); + })); + + assertSameSpan(jdbcSpan.get(), getContext(connection)); + + assertNoContext(connection); + } + + private static void assertSameSpan(SpanContext expected, Context context) { + SpanContext actual = Span.fromContext(context).getSpanContext(); + assertThat(expected.getTraceId()).isEqualTo(actual.getTraceId()); + assertThat(expected.getSpanId()).isEqualTo(actual.getSpanId()); + } + + private static Context toContext(String traceparent) { + if (traceparent == null) { + return Context.root(); + } + + return W3CTraceContextPropagator.getInstance() + .extract( + Context.root(), + traceparent, + new TextMapGetter<>() { + public String get(String carrier, String key) { + if ("traceparent".equals(key)) { + return carrier; + } + return null; + } + + @Override + public Iterable keys(String carrier) { + return Collections.singleton("traceparent"); + } + }); + } + + protected abstract String getTraceparent(Connection connection) throws SQLException; + + private Context getContext(Connection connection) throws SQLException { + return toContext(getTraceparent(connection)); + } + + private void assertNoContext(Connection connection) throws SQLException { + CallDepth callDepthJdbc = CallDepth.forClass(Statement.class); + // disable jdbc instrumentation, so it wouldn't create a span for the statement execution + callDepthJdbc.getAndIncrement(); + try (Statement statement = connection.createStatement()) { + statement.execute("SELECT 1"); + assertSameSpan(SpanContext.getInvalid(), getContext(connection)); + } finally { + callDepthJdbc.decrementAndGet(); + } + } + + @FunctionalInterface + interface Action { + void accept(Connection connection) throws Exception; + } +} diff --git a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleTest.java new file mode 100644 index 000000000..d30b57091 --- /dev/null +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/oracle/OracleTest.java @@ -0,0 +1,83 @@ +/* + * Copyright Splunk Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.splunk.opentelemetry.instrumentation.jdbc.oracle; + +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.oracle.OracleContainer; + +class OracleTest extends AbstractDbContextPropagationTest { + private static final Logger logger = LoggerFactory.getLogger(OracleTest.class); + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final OracleContainer oracle = + new OracleContainer("gvenzl/oracle-free:slim-faststart"); + + static { + oracle.withLogConsumer(new Slf4jLogConsumer(logger)); + } + + @BeforeAll + static void setup() throws Exception { + oracle.start(); + try (Connection connection = + DriverManager.getConnection( + oracle.getJdbcUrl(), oracle.getUsername(), oracle.getPassword())) { + + try (Statement createTable = connection.createStatement()) { + createTable.execute("CREATE TABLE test_table (value INT NOT NULL)"); + } + } + + testing.waitForTraces(1); + testing.clearData(); + } + + @AfterAll + static void cleanup() { + oracle.stop(); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected Connection newConnection() throws SQLException { + return DriverManager.getConnection( + oracle.getJdbcUrl(), oracle.getUsername(), oracle.getPassword()); + } + + @Override + protected String getTraceparent(Connection connection) throws SQLException { + return connection.getClientInfo("OCSID.ACTION"); + } +} diff --git a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerTest.java index c581b7c5a..2350261a4 100644 --- a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerTest.java +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/sqlserver/SqlServerTest.java @@ -16,40 +16,25 @@ package com.splunk.opentelemetry.instrumentation.jdbc.sqlserver; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Named.named; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.propagation.TextMapGetter; +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.javaagent.bootstrap.CallDepth; import java.nio.charset.StandardCharsets; -import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.MSSQLServerContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; -class SqlServerTest { +class SqlServerTest extends AbstractDbContextPropagationTest { private static final Logger logger = LoggerFactory.getLogger(SqlServerTest.class); @RegisterExtension @@ -81,293 +66,19 @@ static void cleanup() { sqlServer.stop(); } - private static List contextPropagationArguments() { - return Arrays.asList( - // Statement tests - Arguments.of( - named( - "Statement.executeQuery", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeQuery("SELECT 1"); - } - })), - Arguments.of( - named( - "Statement.executeUpdate", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate("INSERT INTO test_table VALUES(1)"); - } - })), - Arguments.of( - named( - "Statement.executeUpdate with autoGeneratedKeys", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( - "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); - } - })), - Arguments.of( - named( - "Statement.executeUpdate with columnIndexes", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate("INSERT INTO test_table VALUES(1)", new int[] {1}); - } - })), - Arguments.of( - named( - "Statement.executeUpdate with columnNames", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( - "INSERT INTO test_table VALUES(1)", new String[] {"value"}); - } - })), - Arguments.of( - named( - "Statement.execute", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.execute("SELECT 1"); - } - })), - Arguments.of( - named( - "Statement.execute with autoGeneratedKeys", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.execute( - "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); - } - })), - Arguments.of( - named( - "Statement.execute with columnIndexes", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.execute("INSERT INTO test_table VALUES(1)", new int[] {1}); - } - })), - Arguments.of( - named( - "Statement.execute with columnNames", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.execute( - "INSERT INTO test_table VALUES(1)", new String[] {"value"}); - } - })), - Arguments.of( - named( - "Statement.executeBatch", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.addBatch("INSERT INTO test_table VALUES(1)"); - statement.executeBatch(); - } - })), - Arguments.of( - named( - "Statement.executeLargeBatch", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.addBatch("INSERT INTO test_table VALUES(1)"); - statement.executeLargeBatch(); - } - })), - Arguments.of( - named( - "Statement.executeLargeUpdate", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeLargeUpdate("INSERT INTO test_table VALUES(1)"); - } - })), - Arguments.of( - named( - "Statement.executeLargeUpdate with autoGeneratedKeys", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeLargeUpdate( - "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); - } - })), - Arguments.of( - named( - "Statement.executeLargeUpdate with columnIndexes", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeLargeUpdate( - "INSERT INTO test_table VALUES(1)", new int[] {1}); - } - })), - Arguments.of( - named( - "Statement.executeLargeUpdate with columnNames", - (Action) - connection -> { - try (Statement statement = connection.createStatement()) { - statement.executeLargeUpdate( - "INSERT INTO test_table VALUES(1)", new String[] {"value"}); - } - })), - // PreparedStatement tests - Arguments.of( - named( - "PreparedStatement.executeQuery", - (Action) - connection -> { - try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { - statement.executeQuery(); - } - })), - Arguments.of( - named( - "PreparedStatement.executeUpdate", - (Action) - connection -> { - try (PreparedStatement statement = - connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { - statement.executeUpdate(); - } - })), - Arguments.of( - named( - "PreparedStatement.execute", - (Action) - connection -> { - try (PreparedStatement statement = connection.prepareStatement("SELECT 1")) { - statement.execute(); - } - })), - Arguments.of( - named( - "PreparedStatement.executeLargeUpdate", - (Action) - connection -> { - try (PreparedStatement statement = - connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { - statement.executeLargeUpdate(); - } - })), - Arguments.of( - named( - "PreparedStatement.executeBatch", - (Action) - connection -> { - try (PreparedStatement statement = - connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { - statement.addBatch(); - statement.executeBatch(); - } - })), - Arguments.of( - named( - "PreparedStatement.executeLargeBatch", - (Action) - connection -> { - try (PreparedStatement statement = - connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { - statement.addBatch(); - statement.executeLargeBatch(); - } - })), - // CallableStatement tests - Arguments.of( - named( - "CallableStatement.execute", - (Action) - connection -> { - try (CallableStatement statement = connection.prepareCall("SELECT 1")) { - statement.execute(); - } - }))); - } - - @ParameterizedTest - @MethodSource("contextPropagationArguments") - void contextPropagation(Action action) throws Exception { - Connection connection = - DriverManager.getConnection(sqlServer.getJdbcUrl(), "sa", sqlServer.getPassword()); - - assertNoContext(connection); - - testing.runWithSpan( - "parent", - () -> { - action.accept(connection); - }); - - AtomicReference jdbcSpan = new AtomicReference<>(); - testing.waitAndAssertTraces( - trace -> - trace.hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasNoParent(), - span -> { - span.hasParent(trace.getSpan(0)); - jdbcSpan.set(span.actual().getSpanContext()); - })); - - assertSameSpan(jdbcSpan.get(), getContext(connection)); - - assertNoContext(connection); - } - - private static void assertSameSpan(SpanContext expected, Context context) { - SpanContext actual = Span.fromContext(context).getSpanContext(); - assertThat(expected.getTraceId()).isEqualTo(actual.getTraceId()); - assertThat(expected.getSpanId()).isEqualTo(actual.getSpanId()); - } - - private static String toString(byte[] bytes) { - // CONTEXT_INFO() returns a 128 byte array that is padded with zeroes - for (int i = 0; i < bytes.length; i++) { - if (bytes[i] == 0) { - return new String(bytes, 0, i, StandardCharsets.UTF_8); - } - } - return new String(bytes, StandardCharsets.UTF_8); + @Override + protected InstrumentationExtension getTesting() { + return testing; } - private static Context toContext(String traceparent) { - return W3CTraceContextPropagator.getInstance() - .extract( - Context.root(), - traceparent, - new TextMapGetter<>() { - public String get(String carrier, String key) { - if ("traceparent".equals(key)) { - return carrier; - } - return null; - } - - @Override - public Iterable keys(String carrier) { - return Collections.singleton("traceparent"); - } - }); + @Override + protected Connection newConnection() throws SQLException { + return DriverManager.getConnection(sqlServer.getJdbcUrl(), "sa", sqlServer.getPassword()); } - private static Context getContext(Connection connection) throws SQLException { - CallDepth callDepthSplunk = CallDepth.forClass(SqlServerUtil.class); + @Override + protected String getTraceparent(Connection connection) throws SQLException { + CallDepth callDepthSplunk = CallDepth.forClass(SqlServerContextPropagator.class); CallDepth callDepthJdbc = CallDepth.forClass(Statement.class); // disable instrumentation, so we could read the current value callDepthSplunk.getAndIncrement(); @@ -378,7 +89,7 @@ private static Context getContext(Connection connection) throws SQLException { try (ResultSet resultSet = statement.getResultSet()) { resultSet.next(); byte[] bytes = resultSet.getBytes(1); - return bytes != null ? toContext(toString(bytes)) : Context.root(); + return bytes == null ? null : toString(bytes); } } finally { callDepthJdbc.decrementAndGet(); @@ -386,20 +97,13 @@ private static Context getContext(Connection connection) throws SQLException { } } - private static void assertNoContext(Connection connection) throws SQLException { - CallDepth callDepthJdbc = CallDepth.forClass(Statement.class); - // disable jdbc instrumentation, so it wouldn't create a span for the statement execution - callDepthJdbc.getAndIncrement(); - try (Statement statement = connection.createStatement()) { - statement.execute("SELECT 1"); - assertSameSpan(SpanContext.getInvalid(), getContext(connection)); - } finally { - callDepthJdbc.decrementAndGet(); + private static String toString(byte[] bytes) { + // CONTEXT_INFO() returns a 128 byte array that is padded with zeroes + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == 0) { + return new String(bytes, 0, i, StandardCharsets.UTF_8); + } } - } - - @FunctionalInterface - interface Action { - void accept(Connection connection) throws Exception; + return new String(bytes, StandardCharsets.UTF_8); } }