diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 3812321cf5ff..e7cd6b91eac1 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -131,6 +131,7 @@ These are the supported libraries and frameworks: | [Vert.x HttpClient](https://vertx.io/docs/apidocs/io/vertx/core/http/HttpClient.html) | 3.0+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | | [Vert.x Kafka Client](https://vertx.io/docs/vertx-kafka-client/java/) | 3.6+ | N/A | [Messaging Spans] | | [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ | N/A | context propagation only | +| [Vert.x SQL Client](https://github.com/eclipse-vertx/vertx-sql-client/) | 4.0+ | N/A | [Database Client Spans] | | [Vibur DBCP](https://www.vibur.org/) | 11.0+ | [opentelemetry-vibur-dbcp-11.0](../instrumentation/vibur-dbcp-11.0/library) | [Database Pool Metrics] | | [ZIO](https://zio.dev/) | 2.0.0+ | N/A | Context propagation | diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..e83288d1d379 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.vertx") + module.set("vertx-sql-client") + versions.set("[4.0.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("io.vertx:vertx-sql-client:4.1.0") + compileOnly("io.vertx:vertx-codegen:4.1.0") + + testLibrary("io.vertx:vertx-pg-client:4.1.0") + testLibrary("io.vertx:vertx-codegen:4.1.0") + testLibrary("io.vertx:vertx-opentelemetry:4.1.0") +} + +tasks.withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java new file mode 100644 index 000000000000..bccb0d94fd8a --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/PoolInstrumentation.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.SqlConnectOptions; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class PoolInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.sqlclient.Pool"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("pool") + .and(takesArguments(3)) + .and(takesArgument(1, named("io.vertx.sqlclient.SqlConnectOptions"))) + .and(returns(named("io.vertx.sqlclient.Pool"))), + PoolInstrumentation.class.getName() + "$PoolAdvice"); + } + + @SuppressWarnings("unused") + public static class PoolAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(1) SqlConnectOptions sqlConnectOptions, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepth.forClass(Pool.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + // set connection options to ThreadLocal, they will be read in SqlClientBase constructor + setSqlConnectOptions(sqlConnectOptions); + } + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + setSqlConnectOptions(null); + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java new file mode 100644 index 000000000000..34394e6b66b7 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryExecutorInstrumentation.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_CONTEXT_KEY; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_PARENT_CONTEXT_KEY; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.OTEL_REQUEST_KEY; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.getSqlConnectOptions; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.impl.future.PromiseInternal; +import io.vertx.sqlclient.impl.PreparedStatement; +import io.vertx.sqlclient.impl.QueryExecutorUtil; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class QueryExecutorInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.sqlclient.impl.QueryExecutor"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), QueryExecutorInstrumentation.class.getName() + "$ConstructorAdvice"); + transformer.applyAdviceToMethod( + namedOneOf("executeSimpleQuery", "executeExtendedQuery", "executeBatchQuery"), + QueryExecutorInstrumentation.class.getName() + "$QueryAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This Object queryExecutor) { + // copy connection options from ThreadLocal to VirtualField + QueryExecutorUtil.setConnectOptions(queryExecutor, getSqlConnectOptions()); + } + } + + @SuppressWarnings("unused") + public static class QueryAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object queryExecutor, + @Advice.AllArguments Object[] arguments, + @Advice.Local("otelCallDepth") CallDepth callDepth, + @Advice.Local("otelRequest") VertxSqlClientRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + callDepth = CallDepth.forClass(queryExecutor.getClass()); + if (callDepth.getAndIncrement() > 0) { + return; + } + + // The parameter we need are in different positions, we are not going to have separate + // advices for all of them. The method gets the statement either as String or + // PreparedStatement, use the first argument that is either of these. PromiseInternal is + // always at the end of the argument list. + String sql = null; + PromiseInternal promiseInternal = null; + for (Object argument : arguments) { + if (sql == null) { + if (argument instanceof String) { + sql = (String) argument; + } else if (argument instanceof PreparedStatement) { + sql = ((PreparedStatement) argument).sql(); + } + } else if (argument instanceof PromiseInternal) { + promiseInternal = (PromiseInternal) argument; + } + } + if (sql == null || promiseInternal == null) { + return; + } + + otelRequest = + new VertxSqlClientRequest(sql, QueryExecutorUtil.getConnectOptions(queryExecutor)); + Context parentContext = currentContext(); + if (!instrumenter().shouldStart(parentContext, otelRequest)) { + return; + } + + context = instrumenter().start(parentContext, otelRequest); + scope = context.makeCurrent(); + promiseInternal.context().localContextData().put(OTEL_REQUEST_KEY, otelRequest); + promiseInternal.context().localContextData().put(OTEL_CONTEXT_KEY, context); + promiseInternal.context().localContextData().put(OTEL_PARENT_CONTEXT_KEY, parentContext); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Local("otelCallDepth") CallDepth callDepth, + @Advice.Local("otelRequest") VertxSqlClientRequest otelRequest, + @Advice.Local("otelContext") Context context, + @Advice.Local("otelScope") Scope scope) { + if (callDepth.decrementAndGet() > 0) { + return; + } + if (scope == null) { + return; + } + + scope.close(); + if (throwable != null) { + instrumenter().end(context, otelRequest, null, throwable); + } + // span will be ended in QueryResultBuilderInstrumentation + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java new file mode 100644 index 000000000000..e1b3aabb86d4 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/QueryResultBuilderInstrumentation.java @@ -0,0 +1,71 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.endQuerySpan; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.core.Context; +import io.vertx.core.impl.ContextInternal; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class QueryResultBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.sqlclient.impl.QueryResultBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("tryComplete"), + QueryResultBuilderInstrumentation.class.getName() + "$CompleteAdvice"); + transformer.applyAdviceToMethod( + named("tryFail").and(takesArguments(Throwable.class)), + QueryResultBuilderInstrumentation.class.getName() + "$FailAdvice"); + } + + @SuppressWarnings("unused") + public static class CompleteAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter(@Advice.FieldValue("context") Context vertxContext) { + ContextInternal contextInternal = (ContextInternal) vertxContext; + return endQuerySpan(contextInternal.localContextData(), null); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } + + @SuppressWarnings("unused") + public static class FailAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static Scope onEnter( + @Advice.Argument(0) Throwable throwable, + @Advice.FieldValue("context") Context vertxContext) { + ContextInternal contextInternal = (ContextInternal) vertxContext; + return endQuerySpan(contextInternal.localContextData(), throwable); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Enter Scope scope) { + if (scope != null) { + scope.close(); + } + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java new file mode 100644 index 000000000000..8c8b18ad03e8 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/SqlClientBaseInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.getSqlConnectOptions; +import static io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql.VertxSqlClientSingletons.setSqlConnectOptions; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.bootstrap.CallDepth; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import io.vertx.sqlclient.SqlConnectOptions; +import io.vertx.sqlclient.impl.SqlClientBase; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class SqlClientBaseInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("io.vertx.sqlclient.impl.SqlClientBase"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), SqlClientBaseInstrumentation.class.getName() + "$ConstructorAdvice"); + transformer.applyAdviceToMethod( + namedOneOf("query", "preparedQuery"), + SqlClientBaseInstrumentation.class.getName() + "$QueryAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit(@Advice.This SqlClientBase sqlClientBase) { + // copy connection options from ThreadLocal to VirtualField + VirtualField, SqlConnectOptions> virtualField = + VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); + virtualField.set(sqlClientBase, getSqlConnectOptions()); + } + } + + @SuppressWarnings("unused") + public static class QueryAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This SqlClientBase sqlClientBase, + @Advice.Local("otelCallDepth") CallDepth callDepth) { + callDepth = CallDepth.forClass(SqlClientBase.class); + if (callDepth.getAndIncrement() > 0) { + return; + } + + // set connection options to ThreadLocal, they will be read in QueryExecutor constructor + VirtualField, SqlConnectOptions> virtualField = + VirtualField.find(SqlClientBase.class, SqlConnectOptions.class); + SqlConnectOptions sqlConnectOptions = virtualField.get(sqlClientBase); + setSqlConnectOptions(sqlConnectOptions); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, @Advice.Local("otelCallDepth") CallDepth callDepth) { + if (callDepth.decrementAndGet() > 0) { + return; + } + + setSqlConnectOptions(null); + } + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java new file mode 100644 index 000000000000..121087fe185e --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientAttributesGetter.java @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesGetter; +import javax.annotation.Nullable; + +public enum VertxSqlClientAttributesGetter + implements SqlClientAttributesGetter { + INSTANCE; + + @Override + public String getSystem(VertxSqlClientRequest request) { + return null; + } + + @Override + @Nullable + public String getUser(VertxSqlClientRequest request) { + return request.getUser(); + } + + @Override + @Nullable + public String getName(VertxSqlClientRequest request) { + return request.getDatabase(); + } + + @Override + @Nullable + public String getConnectionString(VertxSqlClientRequest request) { + return null; + } + + @Override + @Nullable + public String getRawStatement(VertxSqlClientRequest request) { + return request.getStatement(); + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java new file mode 100644 index 000000000000..7f71cf586bfb --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientInstrumentationModule.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static java.util.Arrays.asList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class VertxSqlClientInstrumentationModule extends InstrumentationModule { + + public VertxSqlClientInstrumentationModule() { + super("vertx-sql-client", "vertx-sql-client-4.0", "vertx"); + } + + @Override + public boolean isHelperClass(String className) { + return "io.vertx.sqlclient.impl.QueryExecutorUtil".equals(className); + } + + @Override + public List typeInstrumentations() { + return asList( + new PoolInstrumentation(), + new SqlClientBaseInstrumentation(), + new QueryExecutorInstrumentation(), + new QueryResultBuilderInstrumentation()); + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java new file mode 100644 index 000000000000..203856f2a5cd --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientNetAttributesGetter.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesGetter; +import javax.annotation.Nullable; + +public enum VertxSqlClientNetAttributesGetter + implements NetClientAttributesGetter { + INSTANCE; + + @Nullable + @Override + public String getTransport(VertxSqlClientRequest request, @Nullable Void unused) { + return null; + } + + @Nullable + @Override + public String getPeerName(VertxSqlClientRequest request) { + return request.getHost(); + } + + @Nullable + @Override + public Integer getPeerPort(VertxSqlClientRequest request) { + return request.getPort(); + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientRequest.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientRequest.java new file mode 100644 index 000000000000..d644d8b9a742 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import io.vertx.sqlclient.SqlConnectOptions; + +public final class VertxSqlClientRequest { + private final String statement; + private final SqlConnectOptions sqlConnectOptions; + + public VertxSqlClientRequest(String statement, SqlConnectOptions sqlConnectOptions) { + this.statement = statement; + this.sqlConnectOptions = sqlConnectOptions; + } + + public String getStatement() { + return statement; + } + + public String getUser() { + return sqlConnectOptions != null ? sqlConnectOptions.getUser() : null; + } + + public String getDatabase() { + return sqlConnectOptions != null ? sqlConnectOptions.getDatabase() : null; + } + + public String getHost() { + return sqlConnectOptions != null ? sqlConnectOptions.getHost() : null; + } + + public Integer getPort() { + return sqlConnectOptions != null ? sqlConnectOptions.getPort() : null; + } +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java new file mode 100644 index 000000000000..bfbc953fbdb2 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientSingletons.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.db.SqlClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.NetClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.net.PeerServiceAttributesExtractor; +import io.opentelemetry.javaagent.bootstrap.internal.CommonConfig; +import io.vertx.sqlclient.SqlConnectOptions; +import java.util.Map; + +public final class VertxSqlClientSingletons { + public static final String OTEL_REQUEST_KEY = "otel.request"; + public static final String OTEL_CONTEXT_KEY = "otel.context"; + public static final String OTEL_PARENT_CONTEXT_KEY = "otel.parent-context"; + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.vertx-sql-client-4.0"; + private static final Instrumenter INSTRUMENTER; + private static final ThreadLocal connectOptions = new ThreadLocal<>(); + + static { + SpanNameExtractor spanNameExtractor = + DbClientSpanNameExtractor.create(VertxSqlClientAttributesGetter.INSTANCE); + + InstrumenterBuilder builder = + Instrumenter.builder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, spanNameExtractor) + .addAttributesExtractor( + SqlClientAttributesExtractor.builder(VertxSqlClientAttributesGetter.INSTANCE) + .setStatementSanitizationEnabled( + CommonConfig.get().isStatementSanitizationEnabled()) + .build()) + .addAttributesExtractor( + NetClientAttributesExtractor.create(VertxSqlClientNetAttributesGetter.INSTANCE)) + .addAttributesExtractor( + PeerServiceAttributesExtractor.create( + VertxSqlClientNetAttributesGetter.INSTANCE, + CommonConfig.get().getPeerServiceMapping())); + + INSTRUMENTER = builder.buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + public static void setSqlConnectOptions(SqlConnectOptions sqlConnectOptions) { + connectOptions.set(sqlConnectOptions); + } + + public static SqlConnectOptions getSqlConnectOptions() { + return connectOptions.get(); + } + + public static Scope endQuerySpan(Map contextData, Throwable throwable) { + VertxSqlClientRequest otelRequest = + (VertxSqlClientRequest) contextData.remove(OTEL_REQUEST_KEY); + Context otelContext = (Context) contextData.remove(OTEL_CONTEXT_KEY); + Context otelParentContext = (Context) contextData.remove(OTEL_PARENT_CONTEXT_KEY); + if (otelRequest == null || otelContext == null || otelParentContext == null) { + return null; + } + instrumenter().end(otelContext, otelRequest, null, throwable); + return otelParentContext.makeCurrent(); + } + + private VertxSqlClientSingletons() {} +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/vertx/sqlclient/impl/QueryExecutorUtil.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/vertx/sqlclient/impl/QueryExecutorUtil.java new file mode 100644 index 000000000000..c0eb0d1478b6 --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/main/java/io/vertx/sqlclient/impl/QueryExecutorUtil.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.vertx.sqlclient.impl; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.vertx.sqlclient.SqlConnectOptions; + +// Helper class for accessing virtual field on package private QueryExecutor class. +public final class QueryExecutorUtil { + private static final VirtualField, SqlConnectOptions> connectOptionsFiled = + VirtualField.find(QueryExecutor.class, SqlConnectOptions.class); + + public static void setConnectOptions(Object queryExecutor, SqlConnectOptions connectOptions) { + connectOptionsFiled.set((QueryExecutor) queryExecutor, connectOptions); + } + + public static SqlConnectOptions getConnectOptions(Object queryExecutor) { + return connectOptionsFiled.get((QueryExecutor) queryExecutor); + } + + private QueryExecutorUtil() {} +} diff --git a/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java new file mode 100644 index 000000000000..7adf2624770e --- /dev/null +++ b/instrumentation/vertx/vertx-sql-client-4.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/vertx/v4_0/sql/VertxSqlClientTest.java @@ -0,0 +1,251 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.vertx.v4_0.sql; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_NAME; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_SQL_TABLE; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_STATEMENT; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.DB_USER; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_EVENT_NAME; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_NAME; +import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.NET_PEER_PORT; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgException; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +import io.vertx.sqlclient.Tuple; +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +class VertxSqlClientTest { + private static final Logger logger = LoggerFactory.getLogger(VertxSqlClientTest.class); + + private static final String USER_DB = "SA"; + private static final String PW_DB = "password123"; + private static final String DB = "tempdb"; + + @RegisterExtension + private static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + private static GenericContainer container; + private static Vertx vertx; + private static Pool pool; + private static int port; + + @BeforeAll + static void setUp() throws Exception { + container = + new GenericContainer<>("postgres:9.6.8") + .withEnv("POSTGRES_USER", USER_DB) + .withEnv("POSTGRES_PASSWORD", PW_DB) + .withEnv("POSTGRES_DB", DB) + .withExposedPorts(5432) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupTimeout(Duration.ofMinutes(2)); + container.start(); + vertx = Vertx.vertx(); + port = container.getMappedPort(5432); + PgConnectOptions options = + new PgConnectOptions() + .setPort(port) + .setHost(container.getHost()) + .setDatabase(DB) + .setUser(USER_DB) + .setPassword(PW_DB); + pool = Pool.pool(vertx, options, new PoolOptions().setMaxSize(4)); + pool.query("create table test(id int primary key, name varchar(255))") + .execute() + .compose( + r -> + // insert some test data + pool.query("insert into test values (1, 'Hello'), (2, 'World')").execute()) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + } + + @AfterAll + static void cleanUp() { + pool.close(); + vertx.close(); + container.stop(); + } + + @Test + void testSimpleSelect() throws Exception { + CompletableFuture result = new CompletableFuture<>(); + result.whenComplete((rows, throwable) -> testing.runWithSpan("callback", () -> {})); + testing.runWithSpan( + "parent", + () -> + pool.query("select * from test") + .execute( + rowSetAsyncResult -> { + if (rowSetAsyncResult.succeeded()) { + result.complete(rowSetAsyncResult.result()); + } else { + result.completeExceptionally(rowSetAsyncResult.cause()); + } + })); + result.get(30, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "select * from test"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "test"), + equalTo(NET_PEER_NAME, "localhost"), + equalTo(NET_PEER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testInvalidQuery() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + CompletableFuture result = new CompletableFuture<>(); + result.whenComplete( + (rows, throwable) -> testing.runWithSpan("callback", () -> latch.countDown())); + testing.runWithSpan( + "parent", + () -> + pool.query("invalid") + .execute( + rowSetAsyncResult -> { + if (rowSetAsyncResult.succeeded()) { + result.complete(rowSetAsyncResult.result()); + } else { + result.completeExceptionally(rowSetAsyncResult.cause()); + } + })); + + latch.await(30, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("tempdb") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasEventsSatisfyingExactly( + event -> + event + .hasName(EXCEPTION_EVENT_NAME) + .hasAttributesSatisfyingExactly( + equalTo(EXCEPTION_TYPE, PgException.class.getName()), + satisfies( + EXCEPTION_MESSAGE, + val -> val.contains("syntax error at or near")), + satisfies( + EXCEPTION_STACKTRACE, + val -> val.isInstanceOf(String.class)))) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "invalid"), + equalTo(NET_PEER_NAME, "localhost"), + equalTo(NET_PEER_PORT, port)), + span -> + span.hasName("callback") + .hasKind(SpanKind.INTERNAL) + .hasParent(trace.getSpan(0)))); + } + + @Test + void testPreparedSelect() throws Exception { + testing + .runWithSpan( + "parent", + () -> pool.preparedQuery("select * from test where id = $1").execute(Tuple.of(1))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("SELECT tempdb.test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "select * from test where id = $?"), + equalTo(DB_OPERATION, "SELECT"), + equalTo(DB_SQL_TABLE, "test"), + equalTo(NET_PEER_NAME, "localhost"), + equalTo(NET_PEER_PORT, port)))); + } + + @Test + void testBatch() throws Exception { + testing + .runWithSpan( + "parent", + () -> + pool.preparedQuery("insert into test values ($1, $2) returning *") + .executeBatch(Arrays.asList(Tuple.of(3, "Three"), Tuple.of(4, "Four")))) + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("parent").hasKind(SpanKind.INTERNAL), + span -> + span.hasName("INSERT tempdb.test") + .hasKind(SpanKind.CLIENT) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(DB_NAME, DB), + equalTo(DB_USER, USER_DB), + equalTo(DB_STATEMENT, "insert into test values ($?, $?) returning *"), + equalTo(DB_OPERATION, "INSERT"), + equalTo(DB_SQL_TABLE, "test"), + equalTo(NET_PEER_NAME, "localhost"), + equalTo(NET_PEER_PORT, port)))); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ee126c69c843..624afed5475b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -518,6 +518,7 @@ hideFromDependabot(":instrumentation:vertx:vertx-http-client:vertx-http-client-c hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:javaagent") hideFromDependabot(":instrumentation:vertx:vertx-kafka-client-3.6:testing") hideFromDependabot(":instrumentation:vertx:vertx-rx-java-3.5:javaagent") +hideFromDependabot(":instrumentation:vertx:vertx-sql-client-4.0:javaagent") hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:javaagent") hideFromDependabot(":instrumentation:vertx:vertx-web-3.0:testing") hideFromDependabot(":instrumentation:vibur-dbcp-11.0:javaagent")