Use Instrumenter in JDBC instrumentation
* Use Instrumenter in JDBC instrumentation
Except jdbc-datasource, that one is going to be in a separate PR
* Use Instrumenter in JDBC instrumentation - code review comments
MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/InstrumenterBuilder.java
@@ -110,6 +110,15 @@ public final class InstrumenterBuilder<REQUEST, RESPONSE> {
return newInstrumenter(InstrumenterConstructor.internal(), SpanKindExtractor.alwaysInternal());
}
+ /**
+ * Returns a new {@link Instrumenter} which will create spans with kind determined by the passed
+ * {@code spanKindExtractor} and do no context propagation.
+ */
+ public Instrumenter<REQUEST, RESPONSE> newInstrumenter(
+ SpanKindExtractor<? super REQUEST> spanKindExtractor) {
+ return newInstrumenter(InstrumenterConstructor.internal(), spanKindExtractor);
+ }
+
private Instrumenter<REQUEST, RESPONSE> newInstrumenter(
InstrumenterConstructor<REQUEST, RESPONSE> constructor,
SpanKindExtractor<? super REQUEST> spanKindExtractor) {

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+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/database.md">database
+ * attributes</a>. Instrumentations of database libraries should extend this class, defining {@link
+ * REQUEST} with the actual request type of the instrumented library. If an attribute is not
+ * available in this library, it is appropriate to return {@code null} from the protected attribute
+ * methods, but implement as many as possible for best compliance with the OpenTelemetry
+ * specification.
+ */
+public abstract class DbAttributesExtractor<REQUEST> extends AttributesExtractor<REQUEST, Void> {
+ @Override
+ protected void onStart(AttributesBuilder attributes, REQUEST request) {
+ set(attributes, SemanticAttributes.DB_SYSTEM, dbSystem(request));
+ set(attributes, SemanticAttributes.DB_USER, dbUser(request));
+ set(attributes, SemanticAttributes.DB_NAME, dbName(request));
+ set(attributes, SemanticAttributes.DB_CONNECTION_STRING, dbConnectionString(request));
+ set(attributes, SemanticAttributes.DB_STATEMENT, dbStatement(request));
+ set(attributes, SemanticAttributes.DB_OPERATION, dbOperation(request));
+ }
+
+ @Override
+ protected final void onEnd(AttributesBuilder attributes, REQUEST request, Void unused) {}
+
+ @Nullable
+ protected abstract String dbSystem(REQUEST request);
+
+ @Nullable
+ protected abstract String dbUser(REQUEST request);
+
+ @Nullable
+ protected abstract String dbName(REQUEST request);
+
+ @Nullable
+ protected abstract String dbConnectionString(REQUEST request);
+
+ @Nullable
+ protected abstract String dbStatement(REQUEST request);
+
+ @Nullable
+ protected abstract String dbOperation(REQUEST request);
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public final class DbSpanNameExtractor<REQUEST> implements SpanNameExtractor<REQUEST> {
+ /**
+ * Returns a {@link SpanNameExtractor} that constructs the span name according to DB semantic
+ * conventions: {@code <db.operation> <db.name><table>}.
+ *
+ * @see DbAttributesExtractor#dbOperation(Object) used to extract {@code <db.operation>}.
+ * @see DbAttributesExtractor#dbName(Object) used to extract {@code <db.name>}.
+ * @see SqlAttributesExtractor#dbTable(Object) used to extract {@code <db.table>}.
+ */
+ public static <REQUEST> SpanNameExtractor<REQUEST> create(
+ DbAttributesExtractor<REQUEST> attributesExtractor) {
+ return new DbSpanNameExtractor<>(attributesExtractor);
+ }
+
+ private static final String DEFAULT_SPAN_NAME = "DB Query";
+
+ private final DbAttributesExtractor<REQUEST> attributesExtractor;
+
+ private DbSpanNameExtractor(DbAttributesExtractor<REQUEST> attributesExtractor) {
+ this.attributesExtractor = attributesExtractor;
+ }
+
+ @Override
+ public String extract(REQUEST request) {
+ String operation = attributesExtractor.dbOperation(request);
+ String dbName = attributesExtractor.dbName(request);
+ if (operation == null) {
+ return dbName == null ? DEFAULT_SPAN_NAME : dbName;
+ }
+
+ String table = getTableName(request);
+ StringBuilder name = new StringBuilder(operation);
+ if (dbName != null || table != null) {
+ name.append(' ');
+ }
+ if (dbName != null) {
+ name.append(dbName);
+ if (table != null) {
+ name.append('.');
+ }
+ }
+ if (table != null) {
+ name.append(table);
+ }
+ return name.toString();
+ }
+
+ @Nullable
+ private String getTableName(REQUEST request) {
+ if (attributesExtractor instanceof SqlAttributesExtractor) {
+ return ((SqlAttributesExtractor<REQUEST>) attributesExtractor).dbTable(request);
+ }
+ return null;
+ }
+}

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.instrumentation.api.db.SqlStatementInfo;
+import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Extractor of <a
+ * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md">database
+ * attributes</a>. This class is designed with SQL (or SQL-like) database clients in mind. Aside
+ * from adding the same attributes as {@link DbAttributesExtractor}, it has two more features:
+ *
+ * <ul>
+ * <li>It sanitizes the raw SQL query and removes all parameters;
+ * <li>It enables adding the table name extracted by the sanitizer as a parameter.
+ * </ul>
+ */
+public abstract class SqlAttributesExtractor<REQUEST> extends DbAttributesExtractor<REQUEST> {
+
+ @Override
+ protected final void onStart(AttributesBuilder attributes, REQUEST request) {
+ super.onStart(attributes, request);
+ AttributeKey<String> dbTable = dbTableAttribute();
+ if (dbTable != null) {
+ set(attributes, dbTable, dbTable(request));
+ }
+ }
+
+ @Nullable
+ @Override
+ protected final String dbStatement(REQUEST request) {
+ return sanitize(request).getFullStatement();
+ }
+
+ @Nullable
+ @Override
+ protected final String dbOperation(REQUEST request) {
+ return sanitize(request).getOperation();
+ }
+
+ @Nullable
+ protected final String dbTable(REQUEST request) {
+ return sanitize(request).getTable();
+ }
+
+ private SqlStatementInfo sanitize(REQUEST request) {
+ // sanitized statement is cached
+ return SqlStatementSanitizer.sanitize(rawDbStatement(request));
+ }
+
+ @Nullable
+ protected abstract AttributeKey<String> dbTableAttribute();
+
+ @Nullable
+ protected abstract String rawDbStatement(REQUEST request);
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbAttributesExtractorTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+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 DbAttributesExtractorTest {
+ DbAttributesExtractor<Map<String, String>> underTest =
+ new DbAttributesExtractor<Map<String, String>>() {
+ @Override
+ protected String dbSystem(Map<String, String> map) {
+ return map.get("db.system");
+ }
+
+ @Override
+ protected String dbUser(Map<String, String> map) {
+ return map.get("db.user");
+ }
+
+ @Override
+ protected String dbName(Map<String, String> map) {
+ return map.get("db.name");
+ }
+
+ @Override
+ protected String dbConnectionString(Map<String, String> map) {
+ return map.get("db.connection_string");
+ }
+
+ @Override
+ protected String dbStatement(Map<String, String> map) {
+ return map.get("db.statement");
+ }
+
+ @Override
+ protected String dbOperation(Map<String, String> map) {
+ return map.get("db.operation");
+ }
+ };
+
+ @Test
+ void shouldExtractAllAvailableAttributes() {
+ // given
+ Map<String, String> request = new HashMap<>();
+ request.put("db.system", "myDb");
+ request.put("db.user", "username");
+ request.put("db.name", "potatoes");
+ request.put("db.connection_string", "mydb:///potatoes");
+ request.put("db.statement", "SELECT * FROM potato");
+ request.put("db.operation", "SELECT");
+
+ // 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.DB_SYSTEM, "myDb"),
+ entry(SemanticAttributes.DB_USER, "username"),
+ entry(SemanticAttributes.DB_NAME, "potatoes"),
+ entry(SemanticAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"),
+ entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato"),
+ entry(SemanticAttributes.DB_OPERATION, "SELECT"));
+
+ assertThat(endAttributes.build().isEmpty()).isTrue();
+ }
+
+ @Test
+ void shouldExtractNoAttributesIfNoneAreAvailable() {
+ // when
+ AttributesBuilder attributes = Attributes.builder();
+ underTest.onStart(attributes, Collections.emptyMap());
+
+ // then
+ assertThat(attributes.build().isEmpty()).isTrue();
+ }
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/DbSpanNameExtractorTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.BDDMockito.given;
+
+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 DbSpanNameExtractorTest {
+ @Mock DbAttributesExtractor<DbRequest> dbAttributesExtractor;
+ @Mock SqlAttributesExtractor<DbRequest> sqlAttributesExtractor;
+
+ @Test
+ void shouldExtractFullSpanName() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ // cannot stub dbOperation() and dbTable() because they're final
+ given(sqlAttributesExtractor.rawDbStatement(dbRequest)).willReturn("SELECT * FROM table");
+ given(sqlAttributesExtractor.dbName(dbRequest)).willReturn("database");
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(sqlAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("SELECT database.table", spanName);
+ }
+
+ @Test
+ void shouldExtractOperationAndTable() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ // cannot stub dbOperation() and dbTable() because they're final
+ given(sqlAttributesExtractor.rawDbStatement(dbRequest)).willReturn("SELECT * FROM table");
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(sqlAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("SELECT table", spanName);
+ }
+
+ @Test
+ void shouldExtractOperationAndName() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ given(dbAttributesExtractor.dbOperation(dbRequest)).willReturn("SELECT");
+ given(dbAttributesExtractor.dbName(dbRequest)).willReturn("database");
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(dbAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("SELECT database", spanName);
+ }
+
+ @Test
+ void shouldExtractOperation() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ given(dbAttributesExtractor.dbOperation(dbRequest)).willReturn("SELECT");
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(dbAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("SELECT", spanName);
+ }
+
+ @Test
+ void shouldExtractDbName() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ given(dbAttributesExtractor.dbName(dbRequest)).willReturn("database");
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(dbAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("database", spanName);
+ }
+
+ @Test
+ void shouldFallBackToDefaultSpanName() {
+ // given
+ DbRequest dbRequest = new DbRequest();
+
+ SpanNameExtractor<DbRequest> underTest = DbSpanNameExtractor.create(dbAttributesExtractor);
+
+ // when
+ String spanName = underTest.extract(dbRequest);
+
+ // then
+ assertEquals("DB Query", spanName);
+ }
+
+ static class DbRequest {}
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/db/SqlAttributesExtractorTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter.db;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+import io.opentelemetry.api.common.AttributeKey;
+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 SqlAttributesExtractorTest {
+ AttributeKey<String> dbTableAttribute;
+ SqlAttributesExtractor<Map<String, String>> underTest =
+ new SqlAttributesExtractor<Map<String, String>>() {
+
+ @Override
+ protected AttributeKey<String> dbTableAttribute() {
+ return dbTableAttribute;
+ }
+
+ @Override
+ protected String rawDbStatement(Map<String, String> map) {
+ return map.get("db.statement");
+ }
+
+ @Override
+ protected String dbSystem(Map<String, String> map) {
+ return map.get("db.system");
+ }
+
+ @Override
+ protected String dbUser(Map<String, String> map) {
+ return map.get("db.user");
+ }
+
+ @Override
+ protected String dbName(Map<String, String> map) {
+ return map.get("db.name");
+ }
+
+ @Override
+ protected String dbConnectionString(Map<String, String> map) {
+ return map.get("db.connection_string");
+ }
+ };
+
+ @Test
+ void shouldExtractAllAttributes() {
+ // given
+ Map<String, String> request = new HashMap<>();
+ request.put("db.system", "myDb");
+ request.put("db.user", "username");
+ request.put("db.name", "potatoes");
+ request.put("db.connection_string", "mydb:///potatoes");
+ request.put("db.statement", "SELECT * FROM potato WHERE id=12345");
+
+ dbTableAttribute = SemanticAttributes.DB_SQL_TABLE;
+
+ // 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.DB_SYSTEM, "myDb"),
+ entry(SemanticAttributes.DB_USER, "username"),
+ entry(SemanticAttributes.DB_NAME, "potatoes"),
+ entry(SemanticAttributes.DB_CONNECTION_STRING, "mydb:///potatoes"),
+ entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato WHERE id=?"),
+ entry(SemanticAttributes.DB_OPERATION, "SELECT"),
+ entry(SemanticAttributes.DB_SQL_TABLE, "potato"));
+
+ assertThat(endAttributes.build().isEmpty()).isTrue();
+ }
+
+ @Test
+ void shouldNotExtractTableIfAttributeIsNotSet() {
+ // given
+ Map<String, String> request = new HashMap<>();
+ request.put("db.statement", "SELECT * FROM potato WHERE id=12345");
+
+ dbTableAttribute = null;
+
+ // when
+ AttributesBuilder attributes = Attributes.builder();
+ underTest.onStart(attributes, request);
+
+ // then
+ assertThat(attributes.build())
+ .containsOnly(
+ entry(SemanticAttributes.DB_STATEMENT, "SELECT * FROM potato WHERE id=?"),
+ entry(SemanticAttributes.DB_OPERATION, "SELECT"));
+ }
+
+ @Test
+ void shouldExtractNoAttributesIfNoneAreAvailable() {
+ // when
+ AttributesBuilder attributes = Attributes.builder();
+ underTest.onStart(attributes, Collections.emptyMap());
+
+ // then
+ assertThat(attributes.build().isEmpty()).isTrue();
+ }
+}

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/DbRequest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc;
+
+import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcUtils.connectionFromStatement;
+import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcUtils.extractDbInfo;
+
+import com.google.auto.value.AutoValue;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.Statement;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+@AutoValue
+public abstract class DbRequest {
+ public abstract DbInfo getDbInfo();
+
+ @Nullable
+ public abstract String getStatement();
+
+ @Nullable
+ public static DbRequest create(PreparedStatement statement) {
+ return create(statement, JdbcMaps.preparedStatements.get(statement));
+ }
+
+ @Nullable
+ public static DbRequest create(Statement statement, String dbStatementString) {
+ Connection connection = connectionFromStatement(statement);
+ if (connection == null) {
+ return null;
+ }
+
+ return create(extractDbInfo(connection), dbStatementString);
+ }
+
+ public static DbRequest create(DbInfo dbInfo, String statement) {
+ return new AutoValue_DbRequest(dbInfo, statement);
+ }
+}

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.instrumentation.api.instrumenter.db.SqlAttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class JdbcAttributesExtractor extends SqlAttributesExtractor<DbRequest> {
+ @Nullable
+ @Override
+ protected String dbSystem(DbRequest dbRequest) {
+ return dbRequest.getDbInfo().getSystem();
+ }
+
+ @Nullable
+ @Override
+ protected String dbUser(DbRequest dbRequest) {
+ return dbRequest.getDbInfo().getUser();
+ }
+
+ @Nullable
+ @Override
+ protected String dbName(DbRequest dbRequest) {
+ DbInfo dbInfo = dbRequest.getDbInfo();
+ return dbInfo.getName() == null ? dbInfo.getDb() : dbInfo.getName();
+ }
+
+ @Nullable
+ @Override
+ protected String dbConnectionString(DbRequest dbRequest) {
+ return dbRequest.getDbInfo().getShortUrl();
+ }
+
+ @Override
+ protected AttributeKey<String> dbTableAttribute() {
+ return SemanticAttributes.DB_SQL_TABLE;
+ }
+
+ @Nullable
+ @Override
+ protected String rawDbStatement(DbRequest dbRequest) {
+ return dbRequest.getStatement();
+ }
+}

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcInstrumenters.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.db.DbSpanNameExtractor;
+
+public final class JdbcInstrumenters {
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.javaagent.jdbc";
+
+ private static final Instrumenter<DbRequest, Void> INSTRUMENTER;
+
+ static {
+ DbAttributesExtractor<DbRequest> attributesExtractor = new JdbcAttributesExtractor();
+ SpanNameExtractor<DbRequest> spanName = DbSpanNameExtractor.create(attributesExtractor);
+
+ INSTRUMENTER =
+ Instrumenter.<DbRequest, Void>newBuilder(
+ GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanName)
+ .addAttributesExtractor(attributesExtractor)
+ .addAttributesExtractor(new JdbcNetAttributesExtractor())
+ .newInstrumenter(SpanKindExtractor.alwaysClient());
+ }
+
+ public static Instrumenter<DbRequest, Void> instrumenter() {
+ return INSTRUMENTER;
+ }
+
+ private JdbcInstrumenters() {}
+}

ADD
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcNetAttributesExtractor.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jdbc;
+
+import io.opentelemetry.instrumentation.api.instrumenter.NetAttributesExtractor;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class JdbcNetAttributesExtractor extends NetAttributesExtractor<DbRequest, Void> {
+
+ @Nullable
+ @Override
+ protected String transport(DbRequest dbRequest) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ protected String peerName(DbRequest request, Void unused) {
+ return request.getDbInfo().getHost();
+ }
+
+ @Nullable
+ @Override
+ protected Long peerPort(DbRequest request, Void unused) {
+ Integer port = request.getDbInfo().getPort();
+ return port == null ? null : port.longValue();
+ }
+
+ @Nullable
+ @Override
+ protected String peerIp(DbRequest request, Void unused) {
+ return null;
+ }
+}

DELETE
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcTracer.java
-/*
- * Copyright The OpenTelemetry Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.opentelemetry.javaagent.instrumentation.jdbc;
-
-import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcUtils.connectionFromStatement;
-
-import io.opentelemetry.api.trace.SpanBuilder;
-import io.opentelemetry.context.Context;
-import io.opentelemetry.instrumentation.api.db.SqlStatementInfo;
-import io.opentelemetry.instrumentation.api.db.SqlStatementSanitizer;
-import io.opentelemetry.instrumentation.api.tracer.DatabaseClientTracer;
-import io.opentelemetry.instrumentation.api.tracer.net.NetPeerAttributes;
-import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
-import java.net.InetSocketAddress;
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.PreparedStatement;
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class JdbcTracer extends DatabaseClientTracer<DbInfo, String, SqlStatementInfo> {
- private static final JdbcTracer TRACER = new JdbcTracer();
-
- private JdbcTracer() {
- super(NetPeerAttributes.INSTANCE);
- }
-
- public static JdbcTracer tracer() {
- return TRACER;
- }
-
- @Override
- protected String getInstrumentationName() {
- return "io.opentelemetry.javaagent.jdbc";
- }
-
- public Context startSpan(Context parentContext, PreparedStatement statement) {
- return startSpan(parentContext, statement, JdbcMaps.preparedStatements.get(statement));
- }
-
- public Context startSpan(Context parentContext, Statement statement, String query) {
- Connection connection = connectionFromStatement(statement);
- if (connection == null) {
- return null;
- }
-
- DbInfo dbInfo = extractDbInfo(connection);
-
- return startSpan(parentContext, dbInfo, query);
- }
-
- @Override
- protected SqlStatementInfo sanitizeStatement(String statement) {
- return SqlStatementSanitizer.sanitize(statement);
- }
-
- @Override
- protected String spanName(
- DbInfo connection, String statement, SqlStatementInfo sanitizedStatement) {
- return conventionSpanName(
- dbName(connection), sanitizedStatement.getOperation(), sanitizedStatement.getTable());
- }
-
- @Override
- protected String dbSystem(DbInfo info) {
- return info.getSystem();
- }
-
- @Override
- protected String dbUser(DbInfo info) {
- return info.getUser();
- }
-
- @Override
- protected String dbName(DbInfo info) {
- if (info.getName() != null) {
- return info.getName();
- } else {
- return info.getDb();
- }
- }
-
- @Override
- protected String dbConnectionString(DbInfo info) {
- return info.getShortUrl();
- }
-
- // TODO find a way to implement
- @Override
- protected InetSocketAddress peerAddress(DbInfo dbInfo) {
- return null;
- }
-
- @Override
- protected void onStatement(
- SpanBuilder span, DbInfo connection, String statement, SqlStatementInfo sanitizedStatement) {
- super.onStatement(span, connection, statement, sanitizedStatement);
- String table = sanitizedStatement.getTable();
- if (table != null) {
- span.setAttribute(SemanticAttributes.DB_SQL_TABLE, table);
- }
- }
-
- @Override
- protected String dbStatement(
- DbInfo connection, String statement, SqlStatementInfo sanitizedStatement) {
- return sanitizedStatement.getFullStatement();
- }
-
- @Override
- protected String dbOperation(
- DbInfo connection, String statement, SqlStatementInfo sanitizedStatement) {
- return sanitizedStatement.getOperation();
- }
-
- private DbInfo extractDbInfo(Connection connection) {
- DbInfo dbInfo = JdbcMaps.connectionInfo.get(connection);
- /*
- * Logic to get the DBInfo from a JDBC Connection, if the connection was not created via
- * Driver.connect, or it has never seen before, the connectionInfo map will return null and will
- * attempt to extract DBInfo from the connection. If the DBInfo can't be extracted, then the
- * connection will be stored with the DEFAULT DBInfo as the value in the connectionInfo map to
- * avoid retry overhead.
- */
- {
- if (dbInfo == null) {
- try {
- DatabaseMetaData metaData = connection.getMetaData();
- String url = metaData.getURL();
- if (url != null) {
- try {
- dbInfo = JdbcConnectionUrlParser.parse(url, connection.getClientInfo());
- } catch (Throwable ex) {
- // getClientInfo is likely not allowed.
- dbInfo = JdbcConnectionUrlParser.parse(url, null);
- }
- } else {
- dbInfo = DbInfo.DEFAULT;
- }
- } catch (SQLException se) {
- dbInfo = DbInfo.DEFAULT;
- }
- JdbcMaps.connectionInfo.put(connection, dbInfo);
- }
- }
- return dbInfo;
- }
-}

MODIFY
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcUtils.java
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/JdbcUtils.java
@@ -7,11 +7,13 @@ package io.opentelemetry.javaagent.instrumentation.jdbc;
import java.lang.reflect.Field;
import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
import java.sql.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public abstract class JdbcUtils {
+public final class JdbcUtils {
private static final Logger log = LoggerFactory.getLogger(JdbcUtils.class);
@@ -60,4 +62,36 @@ public abstract class JdbcUtils {
}
return connection;
}
+
+ public static DbInfo extractDbInfo(Connection connection) {
+ return JdbcMaps.connectionInfo.computeIfAbsent(connection, JdbcUtils::computeDbInfo);
+ }
+
+ private static DbInfo computeDbInfo(Connection connection) {
+ /*
+ * Logic to get the DBInfo from a JDBC Connection, if the connection was not created via
+ * Driver.connect, or it has never seen before, the connectionInfo map will return null and will
+ * attempt to extract DBInfo from the connection. If the DBInfo can't be extracted, then the
+ * connection will be stored with the DEFAULT DBInfo as the value in the connectionInfo map to
+ * avoid retry overhead.
+ */
+ try {
+ DatabaseMetaData metaData = connection.getMetaData();
+ String url = metaData.getURL();
+ if (url != null) {
+ try {
+ return JdbcConnectionUrlParser.parse(url, connection.getClientInfo());
+ } catch (Throwable ex) {
+ // getClientInfo is likely not allowed.
+ return JdbcConnectionUrlParser.parse(url, null);
+ }
+ } else {
+ return DbInfo.DEFAULT;
+ }
+ } catch (SQLException se) {
+ return DbInfo.DEFAULT;
+ }
+ }
+
+ private JdbcUtils() {}
}

MODIFY
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/PreparedStatementInstrumentation.java
package io.opentelemetry.javaagent.instrumentation.jdbc;
import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext;
-import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcTracer.tracer;
+import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcInstrumenters.instrumenter;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import static java.util.Collections.singletonMap;
@@ -51,6 +51,7 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This PreparedStatement statement,
+ @Advice.Local("otelRequest") DbRequest request,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
// Connection#getMetaData() may execute a Statement or PreparedStatement to retrieve DB info
@@ -65,17 +66,20 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
}
Context parentContext = currentContext();
- if (!tracer().shouldStartSpan(parentContext)) {
+ request = DbRequest.create(statement);
+
+ if (request == null || !instrumenter().shouldStart(parentContext, request)) {
return;
}
- context = tracer().startSpan(parentContext, statement);
+ context = instrumenter().start(parentContext, request);
scope = context.makeCurrent();
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Thrown Throwable throwable,
+ @Advice.Local("otelRequest") DbRequest request,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
if (scope == null) {
@@ -84,11 +88,7 @@ public class PreparedStatementInstrumentation implements TypeInstrumentation {
CallDepthThreadLocalMap.reset(Statement.class);
scope.close();
- if (throwable == null) {
- tracer().end(context);
- } else {
- tracer().endExceptionally(context, throwable);
- }
+ instrumenter().end(context, request, null, throwable);
}
}
}

MODIFY
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java
instrumentation/jdbc/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jdbc/StatementInstrumentation.java
package io.opentelemetry.javaagent.instrumentation.jdbc;
import static io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge.currentContext;
-import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcTracer.tracer;
+import static io.opentelemetry.javaagent.instrumentation.jdbc.JdbcInstrumenters.instrumenter;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import static java.util.Collections.singletonMap;
@@ -51,6 +51,7 @@ public class StatementInstrumentation implements TypeInstrumentation {
public static void onEnter(
@Advice.Argument(0) String sql,
@Advice.This Statement statement,
+ @Advice.Local("otelRequest") DbRequest request,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
// Connection#getMetaData() may execute a Statement or PreparedStatement to retrieve DB info
@@ -65,17 +66,20 @@ public class StatementInstrumentation implements TypeInstrumentation {
}
Context parentContext = currentContext();
- if (!tracer().shouldStartSpan(parentContext)) {
+ request = DbRequest.create(statement, sql);
+
+ if (request == null || !instrumenter().shouldStart(parentContext, request)) {
return;
}
- context = tracer().startSpan(parentContext, statement, sql);
+ context = instrumenter().start(parentContext, request);
scope = context.makeCurrent();
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Thrown Throwable throwable,
+ @Advice.Local("otelRequest") DbRequest request,
@Advice.Local("otelContext") Context context,
@Advice.Local("otelScope") Scope scope) {
if (scope == null) {
@@ -84,11 +88,7 @@ public class StatementInstrumentation implements TypeInstrumentation {
CallDepthThreadLocalMap.reset(Statement.class);
scope.close();
- if (throwable == null) {
- tracer().end(context);
- } else {
- tracer().endExceptionally(context, throwable);
- }
+ instrumenter().end(context, request, null, throwable);
}
}
}

MODIFY
instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy
instrumentation/jdbc/javaagent/src/test/groovy/JdbcInstrumentationTest.groovy
@@ -609,6 +609,7 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"$SemanticAttributes.DB_SYSTEM.key" "testdb"
"$SemanticAttributes.DB_STATEMENT.key" "testing ?"
"$SemanticAttributes.DB_CONNECTION_STRING.key" "testdb://localhost"
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
}
}
}
@@ -650,6 +651,7 @@ class JdbcInstrumentationTest extends AgentInstrumentationSpecification {
"$SemanticAttributes.DB_STATEMENT.key" sanitizedQuery
"$SemanticAttributes.DB_OPERATION.key" operation
"$SemanticAttributes.DB_SQL_TABLE.key" table
+ "$SemanticAttributes.NET_PEER_NAME.key" "localhost"
}
}
}

Use Instrumenter in JDBC instrumentation (#2739)

* Use Instrumenter in JDBC instrumentation

Except jdbc-datasource, that one is going to be in a separate PR

* Use Instrumenter in JDBC instrumentation - code review comments