Moved jdbc-datasource instrumentation to Instrumenter API
ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.code;
+
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Extractor of <a
+ * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/span-general.md#source-code-attributes">source
+ * code attributes</a>.
+ */
+public abstract class CodeAttributesExtractor<REQUEST, RESPONSE>
+ extends AttributesExtractor<REQUEST, RESPONSE> {
+
+ @Override
+ protected final void onStart(AttributesBuilder attributes, REQUEST request) {
+ Class<?> cls = codeClass(request);
+ if (cls != null) {
+ set(attributes, SemanticAttributes.CODE_NAMESPACE, cls.getName());
+ }
+ set(attributes, SemanticAttributes.CODE_FUNCTION, methodName(request));
+ set(attributes, SemanticAttributes.CODE_FILEPATH, filePath(request));
+ set(attributes, SemanticAttributes.CODE_LINENO, lineNumber(request));
+ }
+
+ @Override
+ protected final void onEnd(
+ AttributesBuilder attributes, REQUEST request, @Nullable RESPONSE response) {}
+
+ @Nullable
+ protected abstract Class<?> codeClass(REQUEST request);
+
+ @Nullable
+ protected abstract String methodName(REQUEST request);
+
+ @Nullable
+ protected abstract String filePath(REQUEST request);
+
+ @Nullable
+ protected abstract Long lineNumber(REQUEST request);
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.code;
+
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.tracer.SpanNames;
+
+/**
+ * A helper {@link SpanNameExtractor} implementation for instrumentations that target specific Java
+ * classes/methods.
+ */
+public final class CodeSpanNameExtractor<REQUEST> implements SpanNameExtractor<REQUEST> {
+
+ /**
+ * Returns a {@link SpanNameExtractor} that constructs the span name according to the following
+ * pattern: {@code <class.simpleName>.<methodName>}.
+ */
+ public static <REQUEST> SpanNameExtractor<REQUEST> create(
+ CodeAttributesExtractor<REQUEST, ?> attributesExtractor) {
+ return new CodeSpanNameExtractor<>(attributesExtractor);
+ }
+
+ private final CodeAttributesExtractor<REQUEST, ?> attributesExtractor;
+
+ private CodeSpanNameExtractor(CodeAttributesExtractor<REQUEST, ?> attributesExtractor) {
+ this.attributesExtractor = attributesExtractor;
+ }
+
+ @Override
+ public String extract(REQUEST request) {
+ Class<?> cls = attributesExtractor.codeClass(request);
+ // TODO: avoid using SpanNames, encapsulate the logic here
+ String className = cls != null ? SpanNames.spanNameForClass(cls) : "<unknown>";
+ String methodName = defaultString(attributesExtractor.methodName(request));
+ return className + "." + methodName;
+ }
+
+ private static String defaultString(String s) {
+ return s == null ? "<unknown>" : s;
+ }
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeAttributesExtractorTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.code;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+class CodeAttributesExtractorTest {
+
+ CodeAttributesExtractor<Map<String, String>, Void> underTest =
+ new CodeAttributesExtractor<Map<String, String>, Void>() {
+ @Override
+ protected Class<?> codeClass(Map<String, String> request) {
+ try {
+ String className = request.get("class");
+ return className == null ? null : Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override
+ protected String methodName(Map<String, String> request) {
+ return request.get("methodName");
+ }
+
+ @Override
+ protected String filePath(Map<String, String> request) {
+ return request.get("filePath");
+ }
+
+ @Override
+ protected Long lineNumber(Map<String, String> request) {
+ String lineNo = request.get("lineNo");
+ return lineNo == null ? null : Long.parseLong(lineNo);
+ }
+ };
+
+ @Test
+ void shouldExtractAllAttributes() {
+ // given
+ Map<String, String> request = new HashMap<>();
+ request.put("class", TestClass.class.getName());
+ request.put("methodName", "doSomething");
+ request.put("filePath", "/tmp/TestClass.java");
+ request.put("lineNo", "42");
+
+ // when
+ AttributesBuilder startAttributes = Attributes.builder();
+ underTest.onStart(startAttributes, request);
+
+ AttributesBuilder endAttributes = Attributes.builder();
+ underTest.onEnd(endAttributes, request, null);
+
+ // then
+ assertThat(startAttributes.build())
+ .containsOnly(
+ entry(SemanticAttributes.CODE_NAMESPACE, TestClass.class.getName()),
+ entry(SemanticAttributes.CODE_FUNCTION, "doSomething"),
+ entry(SemanticAttributes.CODE_FILEPATH, "/tmp/TestClass.java"),
+ entry(SemanticAttributes.CODE_LINENO, 42L));
+
+ assertThat(endAttributes.build().isEmpty()).isTrue();
+ }
+
+ @Test
+ void shouldExtractNoAttributesIfNoneAreAvailable() {
+ // when
+ AttributesBuilder attributes = Attributes.builder();
+ underTest.onStart(attributes, Collections.emptyMap());
+
+ // then
+ assertThat(attributes.build().isEmpty()).isTrue();
+ }
+
+ static class TestClass {}
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/code/CodeSpanNameExtractorTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.code;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.BDDMockito.willReturn;
+
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class CodeSpanNameExtractorTest {
+ @Mock CodeAttributesExtractor<Object, Void> attributesExtractor;
+
+ @Test
+ void shouldExtractFullSpanName() {
+ // given
+ Object request = new Object();
+
+ willReturn(TestClass.class).given(attributesExtractor).codeClass(request);
+ willReturn("doSomething").given(attributesExtractor).methodName(request);
+
+ SpanNameExtractor<Object> underTest = CodeSpanNameExtractor.create(attributesExtractor);
+
+ // when
+ String spanName = underTest.extract(request);
+
+ // then
+ assertEquals("TestClass.doSomething", spanName);
+ }
+
+ @Test
+ void shouldExtractFullSpanNameForAnonymousClass() {
+ // given
+ AnonymousBaseClass anon = new AnonymousBaseClass() {};
+ Object request = new Object();
+
+ willReturn(anon.getClass()).given(attributesExtractor).codeClass(request);
+ willReturn("doSomething").given(attributesExtractor).methodName(request);
+
+ SpanNameExtractor<Object> underTest = CodeSpanNameExtractor.create(attributesExtractor);
+
+ // when
+ String spanName = underTest.extract(request);
+
+ // then
+ assertEquals(getClass().getSimpleName() + "$1.doSomething", spanName);
+ }
+
+ static class TestClass {}
+
+ static class AnonymousBaseClass {}
+}

DELETE
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DataSourceTracer.java
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.jdbc;
-
-import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
-
-public class DataSourceTracer extends BaseTracer {
- private static final DataSourceTracer TRACER = new DataSourceTracer();
-
- public static DataSourceTracer tracer() {
- return TRACER;
- }
-
- @Override
- protected String getInstrumentationName() {
- return "io.opentelemetry.javaagent.jdbc";
- }
-}

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceCodeAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc.datasource;
+
+import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor;
+import javax.sql.DataSource;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class DataSourceCodeAttributesExtractor extends CodeAttributesExtractor<DataSource, Void> {
+
+ @Override
+ protected Class<?> codeClass(DataSource dataSource) {
+ return dataSource.getClass();
+ }
+
+ @Override
+ protected String methodName(DataSource dataSource) {
+ return "getConnection";
+ }
+
+ @Override
+ protected @Nullable String filePath(DataSource dataSource) {
+ return null;
+ }
+
+ @Override
+ protected @Nullable Long lineNumber(DataSource dataSource) {
+ return null;
+ }
+}

RENAME
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DataSourceInstrumentation.java
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentation.java
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.jdbc;
+package io.opentelemetry.javaagent.instrumentation.jdbc.datasource;
-import static io.opentelemetry.api.trace.SpanKind.CLIENT;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
-import static io.opentelemetry.javaagent.instrumentation.jdbc.DataSourceTracer.tracer;
+import static io.opentelemetry.javaagent.instrumentation.jdbc.datasource.DataSourceInstrumenters.instrumenter;
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.opentelemetry.context.Context;
@@ -39,18 +38,20 @@ public class DataSourceInstrumentation implements TypeInstrumentation {
@Advice.This DataSource ds,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
- if (!Java8BytecodeBridge.currentSpan().getSpanContext().isValid()) {
+ Context parentContext = Java8BytecodeBridge.currentContext();
+ if (!Java8BytecodeBridge.spanFromContext(parentContext).getSpanContext().isValid()) {
// this instrumentation is already very noisy, and calls to getConnection outside of an
// existing trace do not tend to be very interesting
return;
}
- context = tracer().startSpan(ds.getClass().getSimpleName() + ".getConnection", CLIENT);
+ context = instrumenter().start(parentContext, ds);
scope = context.makeCurrent();
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
+ @Advice.This DataSource ds,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope,
@Advice.Thrown Throwable throwable) {
@@ -58,12 +59,7 @@ public class DataSourceInstrumentation implements TypeInstrumentation {
return;
}
scope.close();
-
- if (throwable != null) {
- tracer().endExceptionally(context, throwable);
- } else {
- tracer().end(context);
- }
+ instrumenter().end(context, ds, null, throwable);
}
}
}

RENAME
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DataSourceInstrumentationModule.java
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumentationModule.java
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.jdbc;
+package io.opentelemetry.javaagent.instrumentation.jdbc.datasource;
import static java.util.Collections.singletonList;

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/datasource/DataSourceInstrumenters.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc.datasource;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.code.CodeAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.code.CodeSpanNameExtractor;
+import javax.sql.DataSource;
+
+public final class DataSourceInstrumenters {
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jdbc";
+
+ private static final Instrumenter<DataSource, Void> INSTRUMENTER;
+
+ static {
+ CodeAttributesExtractor<DataSource, Void> attributesExtractor =
+ new DataSourceCodeAttributesExtractor();
+ SpanNameExtractor<DataSource> spanNameExtractor =
+ CodeSpanNameExtractor.create(attributesExtractor);
+
+ INSTRUMENTER =
+ Instrumenter.<DataSource, Void>newBuilder(
+ GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor)
+ .addAttributesExtractor(attributesExtractor)
+ .newInstrumenter();
+ }
+
+ public static Instrumenter<DataSource, Void> instrumenter() {
+ return INSTRUMENTER;
+ }
+}

MODIFY
instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy
instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy
*/
import static io.opentelemetry.api.trace.SpanKind.CLIENT
+import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
@@ -30,6 +31,7 @@ import spock.lang.Unroll
import test.TestConnection
import test.TestDriver
+@Unroll
class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
@Shared
@@ -162,7 +164,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
}
}
- @Unroll
def "basic statement with #connection.getClass().getCanonicalName() on #system generates spans"() {
setup:
Statement statement = connection.createStatement()
@@ -218,7 +219,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "SELECT 3 FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT ? FROM INFORMATION_SCHEMA.SYSTEM_USERS" | "SELECT INFORMATION_SCHEMA.SYSTEM_USERS" | "hsqldb:mem:" | "INFORMATION_SCHEMA.SYSTEM_USERS"
}
- @Unroll
def "prepared statement execute on #system with #connection.getClass().getCanonicalName() generates a span"() {
setup:
PreparedStatement statement = connection.prepareStatement(query)
@@ -268,7 +268,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1"
}
- @Unroll
def "prepared statement query on #system with #connection.getClass().getCanonicalName() generates a span"() {
setup:
PreparedStatement statement = connection.prepareStatement(query)
@@ -317,7 +316,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1"
}
- @Unroll
def "prepared call on #system with #connection.getClass().getCanonicalName() generates a span"() {
setup:
CallableStatement statement = connection.prepareCall(query)
@@ -366,7 +364,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" | "SELECT ? FROM SYSIBM.SYSDUMMY1" | "SELECT SYSIBM.SYSDUMMY1" | "derby:memory:" | "SYSIBM.SYSDUMMY1"
}
- @Unroll
def "statement update on #system with #connection.getClass().getCanonicalName() generates a span"() {
setup:
Statement statement = connection.createStatement()
@@ -417,7 +414,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"hsqldb" | cpDatasources.get("c3p0").get("hsqldb").getConnection() | "SA" | "CREATE TABLE PUBLIC.S_HSQLDB_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "hsqldb:mem:"
}
- @Unroll
def "prepared statement update on #system with #connection.getClass().getCanonicalName() generates a span"() {
setup:
def sql = connection.nativeSQL(query)
@@ -463,7 +459,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" | "derby:memory:"
}
- @Unroll
def "connection constructor throwing then generating correct spans after recovery using #driver connection (prepare statement = #prepareStatement)"() {
setup:
Connection connection = null
@@ -546,17 +541,21 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
span(1) {
name "${datasource.class.simpleName}.getConnection"
- kind CLIENT
+ kind INTERNAL
childOf span(0)
attributes {
+ "$SemanticAttributes.CODE_NAMESPACE.key" datasource.class.name
+ "$SemanticAttributes.CODE_FUNCTION.key" "getConnection"
}
}
if (recursive) {
span(2) {
name "${datasource.class.simpleName}.getConnection"
- kind CLIENT
+ kind INTERNAL
childOf span(1)
attributes {
+ "$SemanticAttributes.CODE_NAMESPACE.key" datasource.class.name
+ "$SemanticAttributes.CODE_FUNCTION.key" "getConnection"
}
}
}
@@ -615,7 +614,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
query = "testing 123"
}
- @Unroll
def "should produce proper span name #spanName"() {
setup:
def driver = new TestDriver()
@@ -657,7 +655,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"jdbc:testdb://localhost" | "CREATE TABLE table" | "CREATE TABLE table" | "DB Query" | null | null | null
}
- @Unroll
def "#connectionPoolName connections should be cached in case of wrapped connections"() {
setup:
String dbType = "hsqldb"
@@ -720,7 +717,6 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
}
// regression test for https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/2644
- @Unroll
def "should handle recursive Statements inside Connection.getMetaData(): #desc"() {
given:
def connection = new DbCallingConnection(usePreparedStatementInConnection)

Moved jdbc-datasource instrumentation to Instrumenter API (#3160)