diff --git a/instrumentation/jdbc/build.gradle.kts b/instrumentation/jdbc/build.gradle.kts index a64e3af77..93286f8d7 100644 --- a/instrumentation/jdbc/build.gradle.kts +++ b/instrumentation/jdbc/build.gradle.kts @@ -13,6 +13,8 @@ muzzle { dependencies { compileOnly("io.opentelemetry:opentelemetry-api") + compileOnly("io.opentelemetry.semconv:opentelemetry-semconv-incubating") + compileOnly(project(":custom")) testInstrumentation("io.opentelemetry.javaagent.instrumentation:opentelemetry-javaagent-jdbc") @@ -26,8 +28,14 @@ dependencies { // Oracle testLibrary("com.oracle.database.jdbc:ojdbc8:23.9.0.25.07") testImplementation("org.testcontainers:testcontainers-oracle-free") + + // PostgreSQL + testLibrary("org.postgresql:postgresql:42.1.1") + testImplementation("org.testcontainers:testcontainers-postgresql") } tasks.withType().configureEach { + systemProperty("testLatestDeps", findProperty("testLatestDeps") as Boolean) jvmArgs("-Dotel.instrumentation.splunk-jdbc.enabled=true") + jvmArgs("-Dotel.service.name=test-service") } diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/PropagatorInitializer.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/PropagatorInitializer.java new file mode 100644 index 000000000..d0ca82d5b --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/PropagatorInitializer.java @@ -0,0 +1,42 @@ +/* + * 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 io.opentelemetry.sdk.autoconfigure.AutoConfigureUtil.getResource; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.incubating.DeploymentIncubatingAttributes.DEPLOYMENT_ENVIRONMENT_NAME; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_NAMESPACE; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; + +@AutoService(AgentListener.class) +public class PropagatorInitializer implements AgentListener { + + @Override + public void afterAgent(AutoConfiguredOpenTelemetrySdk sdk) { + Resource resource = getResource(sdk); + String serviceName = resource.getAttribute(SERVICE_NAME); + String serviceNamespace = resource.getAttribute(SERVICE_NAMESPACE); + String deploymentEnvironment = resource.getAttribute(DEPLOYMENT_ENVIRONMENT_NAME); + + SqlCommenterInitializer.propagator = + new ServiceAttributePropagator(serviceName, serviceNamespace, deploymentEnvironment); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagator.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagator.java new file mode 100644 index 000000000..e9e15e5f0 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagator.java @@ -0,0 +1,65 @@ +/* + * 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 io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.incubating.DeploymentIncubatingAttributes.DEPLOYMENT_ENVIRONMENT_NAME; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_NAMESPACE; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Collection; +import java.util.Collections; + +class ServiceAttributePropagator implements TextMapPropagator { + private final String serviceName; + private final String serviceNamespace; + private final String deploymentEnvironment; + + ServiceAttributePropagator( + String serviceName, String serviceNamespace, String deploymentEnvironment) { + this.serviceName = serviceName; + this.serviceNamespace = serviceNamespace; + this.deploymentEnvironment = deploymentEnvironment; + } + + @Override + public void inject(Context context, C carrier, TextMapSetter setter) { + setIfNotNull(setter, carrier, SERVICE_NAME.getKey(), serviceName); + setIfNotNull(setter, carrier, SERVICE_NAMESPACE.getKey(), serviceNamespace); + setIfNotNull(setter, carrier, DEPLOYMENT_ENVIRONMENT_NAME.getKey(), deploymentEnvironment); + } + + private static void setIfNotNull( + TextMapSetter setter, C carrier, String key, String value) { + if (value != null) { + setter.set(carrier, key, value); + } + } + + @Override + public Context extract(Context context, C carrier, TextMapGetter getter) { + return context; + } + + @Override + public Collection fields() { + return Collections.emptyList(); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/SqlCommenterInitializer.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/SqlCommenterInitializer.java new file mode 100644 index 000000000..89595c2dd --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/SqlCommenterInitializer.java @@ -0,0 +1,42 @@ +/* + * 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 com.google.auto.service.AutoService; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.internal.SqlCommenterBuilder; +import io.opentelemetry.javaagent.bootstrap.internal.AgentInstrumentationConfig; +import io.opentelemetry.javaagent.bootstrap.internal.sqlcommenter.SqlCommenterCustomizer; + +@AutoService(SqlCommenterCustomizer.class) +public class SqlCommenterInitializer implements SqlCommenterCustomizer { + // propagates service.name and other static attributes + static TextMapPropagator propagator = TextMapPropagator.noop(); + + @Override + public void customize(SqlCommenterBuilder sqlCommenterBuilder) { + sqlCommenterBuilder.setEnabled( + AgentInstrumentationConfig.get() + .getBoolean("otel.instrumentation.splunk-jdbc.enabled", false)); + sqlCommenterBuilder.setPropagator( + (connection, executed) -> { + // note that besides jdbc this applies to r2dbc and other data access apis that upstream + // has sqlcommenter support for + return propagator; + }); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlContextPropagator.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlContextPropagator.java new file mode 100644 index 000000000..5c4646fab --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlContextPropagator.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.postgresql; + +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagator; +import java.sql.Connection; +import java.sql.SQLException; +import javax.annotation.Nullable; + +public final class PostgreSqlContextPropagator extends AbstractDbContextPropagator { + public static final PostgreSqlContextPropagator INSTANCE = new PostgreSqlContextPropagator(); + + private PostgreSqlContextPropagator() {} + + @Override + protected void setContext(Connection connection, @Nullable String contextInfo) + throws SQLException { + connection.setClientInfo("ApplicationName", contextInfo); + } +} diff --git a/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlInstrumentationModule.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlInstrumentationModule.java new file mode 100644 index 000000000..81be12711 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlInstrumentationModule.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.postgresql; + +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 PostgreSqlInstrumentationModule extends InstrumentationModule { + + public PostgreSqlInstrumentationModule() { + super("splunk-jdbc", "splunk-jdbc-postgresql"); + } + + @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 PostgreSqlStatementInstrumentation()); + } + + @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/postgresql/PostgreSqlStatementInstrumentation.java b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlStatementInstrumentation.java new file mode 100644 index 000000000..c0c6b8e31 --- /dev/null +++ b/instrumentation/jdbc/src/main/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlStatementInstrumentation.java @@ -0,0 +1,69 @@ +/* + * 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.postgresql; + +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 PostgreSqlStatementInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf("org.postgresql.jdbc.PgPreparedStatement", "org.postgresql.jdbc.PgStatement"); + } + + @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(PostgreSqlContextPropagator.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + PostgreSqlContextPropagator.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/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractConnectionUsingDbContextPropagationTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractConnectionUsingDbContextPropagationTest.java new file mode 100644 index 000000000..c5e984ff9 --- /dev/null +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/AbstractConnectionUsingDbContextPropagationTest.java @@ -0,0 +1,94 @@ +/* + * 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 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.javaagent.bootstrap.CallDepth; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; + +public abstract class AbstractConnectionUsingDbContextPropagationTest + extends AbstractDbContextPropagationTest { + + @Override + protected void assertBeforeQuery(Connection connection) throws Exception { + assertNoContext(connection); + } + + @Override + protected void assertAfterQuery(Connection connection, SpanContext parent, SpanContext jdbc) + throws Exception { + assertSameSpan(jdbc, 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(); + } + } +} 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 index 3712803c1..9ab43459c 100644 --- 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 @@ -16,24 +16,20 @@ package com.splunk.opentelemetry.instrumentation.jdbc; -import static org.assertj.core.api.Assertions.assertThat; +import static com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest.Capability.COLUMN_INDEXES; +import static com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest.Capability.LARGE_OPERATIONS; +import static org.junit.jupiter.api.Assumptions.assumeTrue; 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; @@ -80,12 +76,13 @@ private static List contextPropagationArguments() { Arguments.of( named( "Statement.executeUpdate with columnIndexes", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.executeUpdate("INSERT INTO test_table VALUES(1)", new int[] {1}); } - })), + }, + COLUMN_INDEXES))), Arguments.of( named( "Statement.executeUpdate with columnNames", @@ -118,12 +115,13 @@ private static List contextPropagationArguments() { Arguments.of( named( "Statement.execute with columnIndexes", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.execute("INSERT INTO test_table VALUES(1)", new int[] {1}); } - })), + }, + COLUMN_INDEXES))), Arguments.of( named( "Statement.execute with columnNames", @@ -147,52 +145,58 @@ private static List contextPropagationArguments() { Arguments.of( named( "Statement.executeLargeBatch", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.addBatch("INSERT INTO test_table VALUES(1)"); statement.executeLargeBatch(); } - })), + }, + LARGE_OPERATIONS))), Arguments.of( named( "Statement.executeLargeUpdate", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.executeLargeUpdate("INSERT INTO test_table VALUES(1)"); } - })), + }, + LARGE_OPERATIONS))), Arguments.of( named( "Statement.executeLargeUpdate with autoGeneratedKeys", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.executeLargeUpdate( "INSERT INTO test_table VALUES(1)", Statement.NO_GENERATED_KEYS); } - })), + }, + LARGE_OPERATIONS))), Arguments.of( named( "Statement.executeLargeUpdate with columnIndexes", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.executeLargeUpdate( "INSERT INTO test_table VALUES(1)", new int[] {1}); } - })), + }, + COLUMN_INDEXES, + LARGE_OPERATIONS))), Arguments.of( named( "Statement.executeLargeUpdate with columnNames", - (Action) + Action.of( connection -> { try (Statement statement = connection.createStatement()) { statement.executeLargeUpdate( "INSERT INTO test_table VALUES(1)", new String[] {"value"}); } - })), + }, + LARGE_OPERATIONS))), // PreparedStatement tests Arguments.of( named( @@ -225,13 +229,14 @@ private static List contextPropagationArguments() { Arguments.of( named( "PreparedStatement.executeLargeUpdate", - (Action) + Action.of( connection -> { try (PreparedStatement statement = connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { statement.executeLargeUpdate(); } - })), + }, + LARGE_OPERATIONS))), Arguments.of( named( "PreparedStatement.executeBatch", @@ -246,14 +251,15 @@ private static List contextPropagationArguments() { Arguments.of( named( "PreparedStatement.executeLargeBatch", - (Action) + Action.of( connection -> { try (PreparedStatement statement = connection.prepareStatement("INSERT INTO test_table VALUES(1)")) { statement.addBatch(); statement.executeLargeBatch(); } - })), + }, + LARGE_OPERATIONS))), // CallableStatement tests Arguments.of( named( @@ -271,79 +277,93 @@ private static List contextPropagationArguments() { @ParameterizedTest @MethodSource("contextPropagationArguments") void contextPropagation(Action action) throws Exception { + action.checkCapabilities(this); + Connection connection = newConnection(); cleanup.deferCleanup(connection); - assertNoContext(connection); + assertBeforeQuery(connection); getTesting().runWithSpan("parent", () -> action.accept(connection)); AtomicReference jdbcSpan = new AtomicReference<>(); + AtomicReference parentSpan = new AtomicReference<>(); getTesting() .waitAndAssertTraces( trace -> trace.hasSpansSatisfyingExactly( - span -> span.hasName("parent").hasNoParent(), + span -> { + span.hasName("parent").hasNoParent(); + parentSpan.set(span.actual().getSpanContext()); + }, span -> { span.hasParent(trace.getSpan(0)); jdbcSpan.set(span.actual().getSpanContext()); })); - assertSameSpan(jdbcSpan.get(), getContext(connection)); + assertAfterQuery(connection, parentSpan.get(), jdbcSpan.get()); + } + + protected abstract void assertBeforeQuery(Connection connection) throws Exception; + + protected abstract void assertAfterQuery( + Connection connection, SpanContext parent, SpanContext jdbc) throws Exception; - assertNoContext(connection); + protected boolean supportsColumnIndexes() { + return true; } - 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()); + protected boolean supportsLarge() { + return true; } - private static Context toContext(String traceparent) { - if (traceparent == null) { - return Context.root(); - } + @FunctionalInterface + interface Action { + void accept(Connection connection) throws Exception; - return W3CTraceContextPropagator.getInstance() - .extract( - Context.root(), - traceparent, - new TextMapGetter<>() { - public String get(String carrier, String key) { - if ("traceparent".equals(key)) { - return carrier; - } - return null; - } + default void checkCapabilities(AbstractDbContextPropagationTest test) {} - @Override - public Iterable keys(String carrier) { - return Collections.singleton("traceparent"); - } - }); + static Action of(Action action, Capability... capabilities) { + return new DefaultAction(action, capabilities) { + @Override + public void accept(Connection connection) throws Exception { + action.accept(connection); + } + }; + } } - protected abstract String getTraceparent(Connection connection) throws SQLException; + static class DefaultAction implements Action { + private final Action delegate; + private final Capability[] capabilities; - private Context getContext(Connection connection) throws SQLException { - return toContext(getTraceparent(connection)); - } + DefaultAction(Action delegate, Capability... capabilities) { + this.delegate = delegate; + this.capabilities = capabilities; + } + + @Override + public void accept(Connection connection) throws Exception { + delegate.accept(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(); + @Override + public void checkCapabilities(AbstractDbContextPropagationTest test) { + for (Capability capability : capabilities) { + switch (capability) { + case COLUMN_INDEXES: + assumeTrue(test.supportsColumnIndexes()); + break; + case LARGE_OPERATIONS: + assumeTrue(test.supportsLarge()); + break; + } + } } } - @FunctionalInterface - interface Action { - void accept(Connection connection) throws Exception; + enum Capability { + COLUMN_INDEXES, + LARGE_OPERATIONS } } diff --git a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagatorTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagatorTest.java new file mode 100644 index 000000000..b659d5548 --- /dev/null +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/ServiceAttributePropagatorTest.java @@ -0,0 +1,47 @@ +/* + * 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 io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.incubating.DeploymentIncubatingAttributes.DEPLOYMENT_ENVIRONMENT_NAME; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_NAMESPACE; +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ServiceAttributePropagatorTest { + + @Test + void testPropagation() { + TextMapPropagator propagator = + new ServiceAttributePropagator( + "service-name", "service-namespace", "deployment-environment"); + Map carrier = new LinkedHashMap<>(); + propagator.inject(Context.root(), carrier, (map, key, value) -> map.put(key, value)); + + assertThat(carrier) + .containsExactly( + entry(SERVICE_NAME.getKey(), "service-name"), + entry(SERVICE_NAMESPACE.getKey(), "service-namespace"), + entry(DEPLOYMENT_ENVIRONMENT_NAME.getKey(), "deployment-environment")); + } +} 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 index d30b57091..0735125f6 100644 --- 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 @@ -16,7 +16,7 @@ package com.splunk.opentelemetry.instrumentation.jdbc.oracle; -import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest; +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractConnectionUsingDbContextPropagationTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import java.sql.Connection; @@ -31,18 +31,15 @@ import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.oracle.OracleContainer; -class OracleTest extends AbstractDbContextPropagationTest { +class OracleTest extends AbstractConnectionUsingDbContextPropagationTest { 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)); - } + new OracleContainer("gvenzl/oracle-free:slim-faststart") + .withLogConsumer(new Slf4jLogConsumer(logger)); @BeforeAll static void setup() throws Exception { diff --git a/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlTest.java b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlTest.java new file mode 100644 index 000000000..3a40e70ee --- /dev/null +++ b/instrumentation/jdbc/src/test/java/com/splunk/opentelemetry/instrumentation/jdbc/postgresql/PostgreSqlTest.java @@ -0,0 +1,141 @@ +/* + * 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.postgresql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractConnectionUsingDbContextPropagationTest; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.postgresql.PostgreSQLContainer; + +class PostgreSqlTest extends AbstractConnectionUsingDbContextPropagationTest { + private static final Logger logger = LoggerFactory.getLogger(PostgreSqlTest.class); + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static final PostgreSQLContainer postgres = + new PostgreSQLContainer("postgres:9.6.8") + .withCommand("postgres -c log_statement=all") + .withLogConsumer(outputFrame -> captureLog(outputFrame.getUtf8String())) + .withLogConsumer(new Slf4jLogConsumer(logger)); + private static final List executedSql = new ArrayList<>(); + + @BeforeAll + static void setup() throws Exception { + postgres.start(); + try (Connection connection = + DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.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() { + postgres.stop(); + } + + @BeforeEach + void cleanupTest() { + executedSql.clear(); + } + + private static void captureLog(String log) { + String prefix = "LOG: execute : "; + if (!log.startsWith(prefix)) { + return; + } + String sql = log.substring(prefix.length()).trim(); + executedSql.add(sql); + } + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected boolean supportsColumnIndexes() { + return false; + } + + @Override + protected boolean supportsLarge() { + return Boolean.getBoolean("testLatestDeps"); + } + + @Override + protected Connection newConnection() throws SQLException { + return DriverManager.getConnection( + postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword()); + } + + @Override + protected String getTraceparent(Connection connection) throws SQLException { + CallDepth callDepthSplunk = CallDepth.forClass(PostgreSqlContextPropagator.class); + CallDepth callDepthJdbc = CallDepth.forClass(Statement.class); + // disable instrumentation, so we could read the current value + callDepthSplunk.getAndIncrement(); + // disable jdbc instrumentation, so it wouldn't create a span for the statement execution + callDepthJdbc.getAndIncrement(); + try (Statement statement = connection.createStatement()) { + statement.execute("SELECT CURRENT_SETTING('application_name')"); + try (ResultSet resultSet = statement.getResultSet()) { + resultSet.next(); + return resultSet.getString(1); + } + } finally { + callDepthJdbc.decrementAndGet(); + callDepthSplunk.decrementAndGet(); + } + } + + @Override + protected void assertAfterQuery(Connection connection, SpanContext parent, SpanContext jdbc) + throws Exception { + super.assertAfterQuery(connection, parent, jdbc); + + String expectedComment = "/*service.name='test-service'"; + await() + .untilAsserted( + () -> assertThat(executedSql).anyMatch(sql -> sql.contains(expectedComment))); + } +} 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 a95c661f8..549be4cdd 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,7 +16,7 @@ package com.splunk.opentelemetry.instrumentation.jdbc.sqlserver; -import com.splunk.opentelemetry.instrumentation.jdbc.AbstractDbContextPropagationTest; +import com.splunk.opentelemetry.instrumentation.jdbc.AbstractConnectionUsingDbContextPropagationTest; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.javaagent.bootstrap.CallDepth; @@ -34,24 +34,23 @@ import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.mssqlserver.MSSQLServerContainer; -class SqlServerTest extends AbstractDbContextPropagationTest { +class SqlServerTest extends AbstractConnectionUsingDbContextPropagationTest { private static final Logger logger = LoggerFactory.getLogger(SqlServerTest.class); @RegisterExtension static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); private static final MSSQLServerContainer sqlServer = - new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); - - static { - sqlServer.withLogConsumer(new Slf4jLogConsumer(logger)); - } + new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-latest") + .acceptLicense() + .withLogConsumer(new Slf4jLogConsumer(logger)); @BeforeAll static void setup() throws Exception { sqlServer.start(); try (Connection connection = - DriverManager.getConnection(sqlServer.getJdbcUrl(), "sa", sqlServer.getPassword())) { + DriverManager.getConnection( + sqlServer.getJdbcUrl(), sqlServer.getUsername(), sqlServer.getPassword())) { try (Statement createTable = connection.createStatement()) { createTable.execute("CREATE TABLE test_table (value INT NOT NULL)"); } @@ -73,7 +72,8 @@ protected InstrumentationExtension getTesting() { @Override protected Connection newConnection() throws SQLException { - return DriverManager.getConnection(sqlServer.getJdbcUrl(), "sa", sqlServer.getPassword()); + return DriverManager.getConnection( + sqlServer.getJdbcUrl(), sqlServer.getUsername(), sqlServer.getPassword()); } @Override diff --git a/instrumentation/jdbc/src/test/resources/logback-test.xml b/instrumentation/jdbc/src/test/resources/logback-test.xml new file mode 100644 index 000000000..262757484 --- /dev/null +++ b/instrumentation/jdbc/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + +