diff --git a/.fossa.yml b/.fossa.yml index 58789f03e9ee..c6eb503a117c 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -139,6 +139,9 @@ targets: - type: gradle path: ./ target: ':instrumentation:gwt-2.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:hbase-client-2.0:javaagent' - type: gradle path: ./ target: ':instrumentation:helidon-4.3:javaagent' diff --git a/.github/config/latest-dep-versions.json b/.github/config/latest-dep-versions.json index dcb827af5f4f..356049056300 100644 --- a/.github/config/latest-dep-versions.json +++ b/.github/config/latest-dep-versions.json @@ -296,6 +296,10 @@ "org.apache.dubbo:dubbo#+": "3.3.6", "org.apache.dubbo:dubbo-config-api#+": "3.3.6", "org.apache.geode:geode-core#+": "2.0.2", + "org.apache.hbase:hbase-client#+": "2.6.5", + "org.apache.hbase:hbase-client#2.4.+": "2.4.18", + "org.apache.hbase:hbase-shaded-client#+": "2.4.18", + "org.apache.hbase:hbase-shaded-client#2.4.+": "2.4.18", "org.apache.httpcomponents.client5:httpclient5#+": "5.6.1", "org.apache.httpcomponents:httpasyncclient#+": "4.1.5", "org.apache.httpcomponents:httpclient#+": "4.5.14", diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index f5c36c84ed21..e7525ffb0f1e 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -30,6 +30,7 @@ These are the supported libraries and frameworks: | [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/) | 2.0+ | [opentelemetry-apache-dbcp-2.0](../instrumentation/apache-dbcp-2.0/library) | [Database Pool Metrics] | | [Apache Dubbo](https://github.com/apache/dubbo/) | 2.7+ | [opentelemetry-apache-dubbo-2.7](../instrumentation/apache-dubbo-2.7/library-autoconfigure) | [RPC Client Spans], [RPC Server Spans] | | [Apache ElasticJob](https://shardingsphere.apache.org/elasticjob/) | 3.0+ | N/A | none | +| [Apache HBase](https://hbase.apache.org/) | 2.0 - 2.4.x | N/A | [Database Client Spans], [Database Client Metrics] [6] | | [Apache HttpAsyncClient](https://hc.apache.org/index.html) | 4.1+ | N/A | [HTTP Client Spans], [HTTP Client Metrics] | | [Apache HttpClient](https://hc.apache.org/index.html) | 2.0+ | [opentelemetry-apache-httpclient-4.3](../instrumentation/apache-httpclient/apache-httpclient-4.3/library),
[opentelemetry-apache-httpclient-5.2](../instrumentation/apache-httpclient/apache-httpclient-5.2/library) | [HTTP Client Spans], [HTTP Client Metrics] | | [Apache Iceberg](https://iceberg.apache.org/) | N/A | [opentelemetry-iceberg-1.8](../instrumentation/iceberg-1.8/library/) | none | @@ -44,7 +45,7 @@ These are the supported libraries and frameworks: | [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0+ | N/A | [Messaging Spans] | | [Apache RocketMQ Remoting-based Client](https://rocketmq.apache.org/) | 4.8+ | [opentelemetry-rocketmq-client-4.8](../instrumentation/rocketmq/rocketmq-client-4.8/library) | [Messaging Spans] | | [Apache Struts](https://github.com/apache/struts) | 2.3+ | N/A | Provides `http.route` [2], Controller Spans [3] | -| [Apache Thrift](https://thrift.apache.org/) | 0.13+ | [opentelemetry-thrift-0.13](../instrumentation/thrift-0.13/library) | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | +| [Apache Thrift](https://thrift.apache.org/) | 0.13+ | [opentelemetry-thrift-0.13](../instrumentation/thrift-0.13/library) | [RPC Client Spans], [RPC Client Metrics], [RPC Server Spans], [RPC Server Metrics] | | [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | N/A | Provides `http.route` [2], Controller Spans [3] | | [Apache Wicket](https://wicket.apache.org/) | 8.0+ | N/A | Provides `http.route` [2] | | [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | diff --git a/instrumentation/hbase-client-2.0/javaagent/build.gradle.kts b/instrumentation/hbase-client-2.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..ee6192e500c4 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/build.gradle.kts @@ -0,0 +1,75 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +otelJava { + // HBase 2.0.x test stack is not reliable on JDK 25+. + maxJavaVersionForTests.set(JavaVersion.VERSION_24) +} + +muzzle { + pass { + group.set("org.apache.hbase") + module.set("hbase-client") + versions.set("[2.0.0, 2.5.0)") + assertInverse.set(true) + } +} + +dependencies { + library("org.apache.hbase:hbase-client:2.0.0") + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":instrumentation:hbase-client-2.0:testing")) + + latestDepTestLibrary("org.apache.hbase:hbase-client:2.4.+") // native on-by-default instrumentation after this version +} + +testing { + suites { + val shadedClientTest by registering(JvmTestSuite::class) { + dependencies { + implementation("org.apache.hbase:hbase-shaded-client:${baseVersion("2.0.0").orLatest("2.4.+")}") + implementation(project(":instrumentation:hbase-client-2.0:testing")) + } + } + } +} + +abstract class HbaseBuildService : BuildService + +// HBase test container binds fixed host ports, disallow running tests in parallel. +gradle.sharedServices.registerIfAbsent("hbaseBuildService", HbaseBuildService::class.java) { + maxParallelUsages.convention(1) +} + +tasks { + withType().configureEach { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + usesService(gradle.sharedServices.registrations["hbaseBuildService"].service) + systemProperty("collectMetadata", otelProps.collectMetadata) + } + + val stableSemconvSuites = testing.suites.withType(JvmTestSuite::class) + .map { suite -> + register("${suite.name}StableSemconv") { + testClassesDirs = suite.sources.output.classesDirs + classpath = suite.sources.runtimeClasspath + + jvmArgs("-Dotel.semconv-stability.opt-in=database") + systemProperty("metadataConfig", "otel.semconv-stability.opt-in=database") + } + } + + check { + dependsOn(testing.suites, stableSemconvSuites) + } + + if (otelProps.denyUnsafe) { + withType().configureEach { + enabled = false + } + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/AbstractRpcClientInstrumentation.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/AbstractRpcClientInstrumentation.java new file mode 100644 index 000000000000..768731c63fe8 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/AbstractRpcClientInstrumentation.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.getTableName; +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.instrumenter; +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.resetRequestAndContext; +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.setRequestAndContext; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hadoop.hbase.net.Address; +import org.apache.hadoop.hbase.security.User; +import org.apache.hbase.thirdparty.com.google.protobuf.Descriptors; + +class AbstractRpcClientInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.hadoop.hbase.ipc.AbstractRpcClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("callMethod") + .and( + takesArgument( + 0, + named( + "org.apache.hbase.thirdparty.com.google.protobuf.Descriptors$MethodDescriptor"))) + .and(takesArgument(4, named("org.apache.hadoop.hbase.security.User"))), + getClass().getName() + "$CallMethodAdvice"); + } + + @SuppressWarnings("unused") + public static class CallMethodAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static RequestAndContext onEnter( + @Advice.Argument(0) Descriptors.MethodDescriptor md, + @Advice.Argument(4) User ticket, + @Advice.Argument(5) Object addr) { + String hostname = null; + Integer port = null; + if (addr instanceof Address) { + Address address = (Address) addr; + port = address.getPort(); + hostname = address.getHostname(); + } else if (addr instanceof InetSocketAddress) { + InetSocketAddress address = (InetSocketAddress) addr; + port = address.getPort(); + hostname = address.getHostString(); + } + HbaseRequest request = + HbaseRequest.create(md.getName(), getTableName(), ticket.getName(), hostname, port); + Context parentContext = Java8BytecodeBridge.currentContext(); + if (!instrumenter().shouldStart(parentContext, request)) { + return null; + } + Context context = instrumenter().start(parentContext, request); + Scope scope = context.makeCurrent(); + RequestAndContext requestAndContext = RequestAndContext.create(request, scope, context); + setRequestAndContext(requestAndContext); + return requestAndContext; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.Enter @Nullable RequestAndContext requestAndContext) { + resetRequestAndContext(); + if (requestAndContext == null) { + return; + } + + Scope scope = requestAndContext.getScope(); + scope.close(); + + if (throwable != null) { + instrumenter() + .end(requestAndContext.getContext(), requestAndContext.getRequest(), null, throwable); + } + } + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseAttributesGetter.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseAttributesGetter.java new file mode 100644 index 000000000000..fcbdfd707323 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseAttributesGetter.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesGetter; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemNameIncubatingValues; +import java.net.InetSocketAddress; +import javax.annotation.Nullable; + +final class HbaseAttributesGetter implements DbClientAttributesGetter { + + @Nullable + @Override + public String getDbSystemName(HbaseRequest hbaseRequest) { + return DbSystemNameIncubatingValues.HBASE; + } + + @Nullable + @Override + public String getDbNamespace(HbaseRequest hbaseRequest) { + return hbaseRequest.getTable(); + } + + @Nullable + @Override + // Old database semconv still use db.user, so we must implement the deprecated hook. + @SuppressWarnings("deprecation") + public String getUser(HbaseRequest hbaseRequest) { + return hbaseRequest.getUser(); + } + + @Nullable + @Override + public String getDbQueryText(HbaseRequest hbaseRequest) { + return null; + } + + @Nullable + @Override + public String getDbOperationName(HbaseRequest hbaseRequest) { + return hbaseRequest.getOperation(); + } + + @Nullable + @Override + public InetSocketAddress getNetworkPeerInetSocketAddress( + HbaseRequest request, @Nullable Void unused) { + if (request.getHost() == null || request.getPort() == null) { + return null; + } + return InetSocketAddress.createUnresolved(request.getHost(), request.getPort()); + } + + @Nullable + @Override + public String getServerAddress(HbaseRequest request) { + return request.getHost(); + } + + @Nullable + @Override + public Integer getServerPort(HbaseRequest request) { + return request.getPort(); + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseInstrumentationModule.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseInstrumentationModule.java new file mode 100644 index 000000000000..a0ae60325eae --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseInstrumentationModule.java @@ -0,0 +1,58 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public final class HbaseInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + private static final String CALL_UTIL = "org.apache.hadoop.hbase.ipc.OpenTelemetryCallUtil"; + + public HbaseInstrumentationModule() { + super("hbase-client", "hbase-client-2.0"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed( + // added in 2.0.0 + "org.apache.hadoop.hbase.ipc.RpcConnection", + "org.apache.hadoop.hbase.client.AsyncAdmin") + // added in 2.5.0 (native OTel support) + .and(not(hasClassesNamed("org.apache.hadoop.hbase.client.trace.IpcClientSpanBuilder"))); + } + + @Override + public boolean isHelperClass(String className) { + return CALL_UTIL.equals(className); + } + + @Override + public List injectedClassNames() { + return singletonList(CALL_UTIL); + } + + @Override + public List typeInstrumentations() { + return asList( + new RegionServerCallableInstrumentation(), + new AbstractRpcClientInstrumentation(), + new RpcConnectionInstrumentation(), + new IpcCallInstrumentation()); + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseRequest.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseRequest.java new file mode 100644 index 000000000000..40c146b19740 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseRequest.java @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +@AutoValue +public abstract class HbaseRequest { + + public static HbaseRequest create( + @Nullable String operation, + @Nullable String table, + @Nullable String user, + @Nullable String host, + @Nullable Integer port) { + return new AutoValue_HbaseRequest(operation, table, user, host, port); + } + + @Nullable + public abstract String getOperation(); + + @Nullable + public abstract String getTable(); + + @Nullable + public abstract String getUser(); + + @Nullable + public abstract String getHost(); + + @Nullable + public abstract Integer getPort(); +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseSingletons.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseSingletons.java new file mode 100644 index 000000000000..32b760485598 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseSingletons.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics; +import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor; +import javax.annotation.Nullable; + +public class HbaseSingletons { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.hbase-client-2.0"; + private static final ThreadLocal tableNameThreadLocal = new ThreadLocal<>(); + private static final ThreadLocal requestAndContextThreadLocal = + new ThreadLocal<>(); + private static final Instrumenter instrumenter = createInstrumenter(); + + public static void setTableName(String tableName) { + tableNameThreadLocal.set(tableName); + } + + @Nullable + public static String getTableName() { + return tableNameThreadLocal.get(); + } + + public static void resetTableName() { + tableNameThreadLocal.remove(); + } + + public static void setRequestAndContext(RequestAndContext requestAndContext) { + requestAndContextThreadLocal.set(requestAndContext); + } + + @Nullable + public static RequestAndContext getRequestAndContext() { + return requestAndContextThreadLocal.get(); + } + + public static void resetRequestAndContext() { + requestAndContextThreadLocal.remove(); + } + + public static Instrumenter instrumenter() { + return instrumenter; + } + + private static Instrumenter createInstrumenter() { + HbaseAttributesGetter hbaseAttributesGetter = new HbaseAttributesGetter(); + return Instrumenter.builder( + GlobalOpenTelemetry.get(), + INSTRUMENTATION_NAME, + DbClientSpanNameExtractor.create(hbaseAttributesGetter)) + .addAttributesExtractor(DbClientAttributesExtractor.create(hbaseAttributesGetter)) + .addOperationMetrics(DbClientMetrics.get()) + .buildInstrumenter(SpanKindExtractor.alwaysClient()); + } + + private HbaseSingletons() {} +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/IpcCallInstrumentation.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/IpcCallInstrumentation.java new file mode 100644 index 000000000000..5a60b2a04433 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/IpcCallInstrumentation.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.instrumenter; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.io.IOException; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hadoop.hbase.ipc.OpenTelemetryCallUtil; + +class IpcCallInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.hadoop.hbase.ipc.Call"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + namedOneOf("callComplete", "setTimeout"), getClass().getName() + "$CallAdvice"); + } + + @SuppressWarnings("unused") + public static class CallAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Object call, + @Advice.Origin("#m") String methodName, + @Advice.Argument(value = 0, optional = true) @Nullable IOException timeoutError, + @Advice.FieldValue(value = "error") @Nullable IOException callError) { + IOException error = "setTimeout".equals(methodName) ? timeoutError : callError; + RequestAndContext requestAndContext = + OpenTelemetryCallUtil.getAndClearRequestAndContext(call); + if (requestAndContext == null) { + return; + } + + instrumenter() + .end(requestAndContext.getContext(), requestAndContext.getRequest(), null, error); + } + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RegionServerCallableInstrumentation.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RegionServerCallableInstrumentation.java new file mode 100644 index 000000000000..9dba424f0e40 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RegionServerCallableInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.resetTableName; +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.setTableName; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hadoop.hbase.TableName; + +class RegionServerCallableInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return namedOneOf( + "org.apache.hadoop.hbase.client.RegionServerCallable", + "org.apache.hadoop.hbase.client.RegionAdminServiceCallable"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod(named("call"), getClass().getName() + "$RpcCallAdvice"); + } + + @SuppressWarnings("unused") + public static class RpcCallAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.FieldValue(value = "tableName") TableName table) { + if (table != null) { + setTableName(table.getNameAsString()); + } + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit() { + resetTableName(); + } + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RequestAndContext.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RequestAndContext.java new file mode 100644 index 000000000000..80c1df737c6b --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RequestAndContext.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; + +@AutoValue +public abstract class RequestAndContext { + + public static RequestAndContext create(HbaseRequest request, Scope scope, Context context) { + return new AutoValue_RequestAndContext(request, scope, context); + } + + public abstract HbaseRequest getRequest(); + + public abstract Scope getScope(); + + public abstract Context getContext(); +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RpcConnectionInstrumentation.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RpcConnectionInstrumentation.java new file mode 100644 index 000000000000..fe457e167511 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/RpcConnectionInstrumentation.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.HbaseSingletons.getRequestAndContext; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.hadoop.hbase.ipc.OpenTelemetryCallUtil; + +class RpcConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return extendsClass(named("org.apache.hadoop.hbase.ipc.RpcConnection")); + } + + @Override + public ElementMatcher classLoaderOptimization() { + return hasClassesNamed("org.apache.hadoop.hbase.ipc.RpcConnection"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("sendRequest").and(takesArgument(0, named("org.apache.hadoop.hbase.ipc.Call"))), + getClass().getName() + "$SendRequestAdvice"); + } + + @SuppressWarnings("unused") + public static class SendRequestAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) Object call) { + RequestAndContext requestAndContext = getRequestAndContext(); + OpenTelemetryCallUtil.setRequestAndContext(call, requestAndContext); + } + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/main/java/org/apache/hadoop/hbase/ipc/OpenTelemetryCallUtil.java b/instrumentation/hbase-client-2.0/javaagent/src/main/java/org/apache/hadoop/hbase/ipc/OpenTelemetryCallUtil.java new file mode 100644 index 000000000000..746755967344 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/main/java/org/apache/hadoop/hbase/ipc/OpenTelemetryCallUtil.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.hadoop.hbase.ipc; + +import io.opentelemetry.instrumentation.api.util.VirtualField; +import io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0.RequestAndContext; +import javax.annotation.Nullable; + +// Helper for accessing the virtual field on package-private Call. +public final class OpenTelemetryCallUtil { + private static final VirtualField requestAndContextField = + VirtualField.find(Call.class, RequestAndContext.class); + + public static void setRequestAndContext( + Object call, @Nullable RequestAndContext requestAndContext) { + requestAndContextField.set((Call) call, requestAndContext); + } + + @Nullable + public static RequestAndContext getAndClearRequestAndContext(Object call) { + RequestAndContext requestAndContext = requestAndContextField.get((Call) call); + requestAndContextField.set((Call) call, null); + return requestAndContext; + } + + private OpenTelemetryCallUtil() {} +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/shadedClientTest/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseShadedClient20Test.java b/instrumentation/hbase-client-2.0/javaagent/src/shadedClientTest/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseShadedClient20Test.java new file mode 100644 index 000000000000..b02aa9288d50 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/shadedClientTest/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseShadedClient20Test.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.hbase.testing.AbstractHbaseTest; +import org.junit.jupiter.api.extension.RegisterExtension; + +class HbaseShadedClient20Test extends AbstractHbaseTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/hbase-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseClient20Test.java b/instrumentation/hbase-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseClient20Test.java new file mode 100644 index 000000000000..944ddf2c91c9 --- /dev/null +++ b/instrumentation/hbase-client-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hbase/client/v2_0/HbaseClient20Test.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.client.v2_0; + +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.javaagent.instrumentation.hbase.testing.AbstractHbaseTest; +import org.junit.jupiter.api.extension.RegisterExtension; + +class HbaseClient20Test extends AbstractHbaseTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } +} diff --git a/instrumentation/hbase-client-2.0/metadata.yaml b/instrumentation/hbase-client-2.0/metadata.yaml new file mode 100644 index 000000000000..ea5fbbd9cab7 --- /dev/null +++ b/instrumentation/hbase-client-2.0/metadata.yaml @@ -0,0 +1,8 @@ +display_name: HBase Client +description: > + This instrumentation enables database client spans and database client metrics for the Apache + HBase client. +semantic_conventions: + - DATABASE_CLIENT_SPANS + - DATABASE_CLIENT_METRICS +library_link: https://hbase.apache.org/ diff --git a/instrumentation/hbase-client-2.0/testing/build.gradle.kts b/instrumentation/hbase-client-2.0/testing/build.gradle.kts new file mode 100644 index 000000000000..74ab8c8d4285 --- /dev/null +++ b/instrumentation/hbase-client-2.0/testing/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api("io.opentelemetry.javaagent:opentelemetry-testing-common") + + compileOnly("org.apache.hbase:hbase-client:2.0.0") + implementation("org.testcontainers:testcontainers") +} diff --git a/instrumentation/hbase-client-2.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/testing/AbstractHbaseTest.java b/instrumentation/hbase-client-2.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/testing/AbstractHbaseTest.java new file mode 100644 index 000000000000..ee9dc6d202c0 --- /dev/null +++ b/instrumentation/hbase-client-2.0/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/hbase/testing/AbstractHbaseTest.java @@ -0,0 +1,543 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hbase.testing; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableDatabaseSemconv; +import static io.opentelemetry.instrumentation.testing.junit.db.DbClientMetricsTestUtil.assertDurationMetric; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStableDbSystemName; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.DbAttributes.DB_SYSTEM_NAME; +import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_MESSAGE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_STACKTRACE; +import static io.opentelemetry.semconv.ExceptionAttributes.EXCEPTION_TYPE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_NAME; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_USER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.TraceAssert; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.CompareOperator; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Admin; +import org.apache.hadoop.hbase.client.Append; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; +import org.apache.hadoop.hbase.client.Connection; +import org.apache.hadoop.hbase.client.ConnectionFactory; +import org.apache.hadoop.hbase.client.Delete; +import org.apache.hadoop.hbase.client.Get; +import org.apache.hadoop.hbase.client.Increment; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.client.Result; +import org.apache.hadoop.hbase.client.ResultScanner; +import org.apache.hadoop.hbase.client.RowMutations; +import org.apache.hadoop.hbase.client.Scan; +import org.apache.hadoop.hbase.client.Table; +import org.apache.hadoop.hbase.client.TableDescriptor; +import org.apache.hadoop.hbase.client.TableDescriptorBuilder; +import org.apache.hadoop.hbase.ipc.CallTimeoutException; +import org.apache.hadoop.hbase.util.Bytes; +import org.assertj.core.api.AbstractAssert; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.utility.DockerImageName; + +@SuppressWarnings("deprecation") // using deprecated semconv +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractHbaseTest { + + protected static final int MASTER_PORT = 16000; + protected static final int REGION_SERVER_PORT = 16020; + protected static final String INSTRUMENTATION_NAME = "io.opentelemetry.hbase-client-2.0"; + + private static final String NAMESPACE = "ot_test"; + protected static final byte[] COLUMN_FAMILY = Bytes.toBytes("cf"); + protected static final TableName TABLE_NAME = TableName.valueOf("ot_test:eleven_test_table"); + protected static final TableName META = TableName.valueOf("hbase:meta"); + + private static final String DB_SYSTEM_VALUE = "hbase"; + protected static final String SCAN = "Scan"; + protected static final String MUTATE = "Mutate"; + protected static final String GET = "Get"; + protected static final String MULTI = "Multi"; + + private static final int GET_TIMEOUT_OPERATION_TIMEOUT_MILLIS = 1000; + private static final int GET_TIMEOUT_RPC_TIMEOUT_MILLIS = 200; + private static final String ROW_1 = "row1"; + private static final String ROW_2 = "row2"; + private static final String ROW_3 = "row3"; + private static final String ROW_4 = "row4"; + private static final String ROW_5 = "row5"; + private static final String SCAN_ROW = "scan-row"; + + protected abstract InstrumentationExtension testing(); + + @RegisterExtension final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + private final String hostname = getHostName(); + protected final GenericContainer hbaseContainer = createHbaseContainer(hostname); + + protected Connection connection; + + private static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + return "localhost"; + } + } + + private static GenericContainer createHbaseContainer(String hostname) { + return new GenericContainer<>(DockerImageName.parse("harisekhon/hbase:2.0")) + .withCreateContainerCmdModifier( + cmd -> + cmd.getHostConfig() + .withPortBindings( + new PortBinding(Ports.Binding.bindPort(2181), new ExposedPort(2181)), + new PortBinding(Ports.Binding.bindPort(16000), new ExposedPort(16000)), + new PortBinding(Ports.Binding.bindPort(16010), new ExposedPort(16010)), + new PortBinding(Ports.Binding.bindPort(16020), new ExposedPort(16020)), + new PortBinding(Ports.Binding.bindPort(16030), new ExposedPort(16030)), + new PortBinding(Ports.Binding.bindPort(16201), new ExposedPort(16201)), + new PortBinding(Ports.Binding.bindPort(16301), new ExposedPort(16301)))) + .withExposedPorts(2181, 16000, 16010, 16020, 16030, 16201, 16301) + .withStartupTimeout(Duration.ofMinutes(2)) + .withCreateContainerCmdModifier(cmd -> cmd.withHostName(hostname)) + .waitingFor( + new WaitAllStrategy() + .withStrategy(Wait.forLogMessage(".*Master has completed initialization.*\\n", 1)) + .withStrategy(Wait.forListeningPorts(2181, MASTER_PORT, REGION_SERVER_PORT)) + .withStartupTimeout(Duration.ofMinutes(2))); + } + + @BeforeAll + void setUp() throws IOException { + hbaseContainer.start(); + cleanup.deferAfterAll(hbaseContainer::stop); + String host = hbaseContainer.getHost(); + Configuration config = HBaseConfiguration.create(); + config.set("hbase.zookeeper.quorum", host); + config.set("hbase.zookeeper.property.clientPort", "2181"); + connection = ConnectionFactory.createConnection(config); + cleanup.deferAfterAll(connection); + testing() + .runWithSpan( + "setup", + () -> { + createNamespaceAndTable(); + seedRows(); + }); + testing().waitForTraces(1); + testing().clearData(); + } + + private void createNamespaceAndTable() throws IOException { + try (Admin admin = connection.getAdmin()) { + NamespaceDescriptor namespaceDescriptor = NamespaceDescriptor.create(NAMESPACE).build(); + admin.createNamespace(namespaceDescriptor); + ColumnFamilyDescriptor columnFamilyDescriptor = + ColumnFamilyDescriptorBuilder.newBuilder(COLUMN_FAMILY).build(); + TableDescriptor tableDescriptor = + TableDescriptorBuilder.newBuilder(TABLE_NAME) + .setColumnFamily(columnFamilyDescriptor) + .build(); + admin.createTable(tableDescriptor); + } + } + + private void seedRows() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + table.put(row(ROW_1, "col1_val_1", "col2_val_1")); + table.put(row(SCAN_ROW, "scan_col1_val", "scan_col2_val")); + } + } + + private static Put row(String rowKey, String col1Value, String col2Value) { + Put put = new Put(Bytes.toBytes(rowKey)); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col1"), Bytes.toBytes(col1Value)); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col2"), Bytes.toBytes(col2Value)); + return put; + } + + @Test + void testListNamespace() throws IOException { + Admin admin = connection.getAdmin(); + cleanup.deferCleanup(admin); + + List namespaces = new ArrayList<>(); + for (NamespaceDescriptor ns : admin.listNamespaceDescriptors()) { + namespaces.add(ns.getName()); + } + assertThat(namespaces).contains(NAMESPACE); + + testing() + .waitAndAssertTraces( + traceAssertConsumer(null, "IsMasterRunning", MASTER_PORT, false), + traceAssertConsumer(null, "ListNamespaceDescriptors", MASTER_PORT, false)); + } + + @Test + void testListTable() throws IOException { + Admin admin = connection.getAdmin(); + cleanup.deferCleanup(admin); + + assertThat(admin.listTableNames()).contains(TABLE_NAME); + + testing() + .waitAndAssertTraces( + traceAssertConsumer(null, "IsMasterRunning", MASTER_PORT, false), + traceAssertConsumer(null, "GetTableNames", MASTER_PORT, false)); + } + + @Test + void testPut() throws IOException { + try (Connection putConnection = + ConnectionFactory.createConnection(connection.getConfiguration()); + Table table = putConnection.getTable(TABLE_NAME)) { + Put put = row("put-row", "put_col1_val", "put_col2_val"); + table.put(put); + } + testing() + .waitAndAssertTraces( + traceAssertConsumer(META, SCAN, REGION_SERVER_PORT, true), + traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testGet() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + Get get = new Get(Bytes.toBytes(ROW_1)); + Result result = table.get(get); + assertThat(value(result, "col1")).isEqualTo("col1_val_1"); + assertThat(value(result, "col2")).isEqualTo("col2_val_1"); + } + testing().waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, GET, REGION_SERVER_PORT, true)); + } + + private static String value(Result result, String column) { + return Bytes.toString(result.getValue(COLUMN_FAMILY, Bytes.toBytes(column))); + } + + @Test + void testGetTimeout() throws IOException { + try (Connection timeoutConnection = ConnectionFactory.createConnection(getTimeoutConfig()); + Table table = timeoutConnection.getTable(TABLE_NAME)) { + warmUpTimeoutConnection(table); + + assertThatExceptionOfType(IOException.class).isThrownBy(() -> getWithPausedContainer(table)); + } + + testing() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(GET + " " + TABLE_NAME.getNameAsString()) + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo( + maybeStable(DB_SYSTEM), + maybeStableDbSystemName(DB_SYSTEM_VALUE)), + equalTo(maybeStable(DB_OPERATION), GET), + equalTo(maybeStable(DB_NAME), TABLE_NAME.getNameAsString()), + equalTo(SERVER_ADDRESS, hostname), + equalTo(SERVER_PORT, REGION_SERVER_PORT), + equalTo( + ERROR_TYPE, + emitStableDatabaseSemconv() + ? CallTimeoutException.class.getName() + : null), + satisfies( + DB_USER, + emitStableDatabaseSemconv() + ? AbstractAssert::isNull + : AbstractAssert::isNotNull)) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("exception") + .hasAttributesSatisfyingExactly( + equalTo( + EXCEPTION_TYPE, + CallTimeoutException.class.getName()), + satisfies(EXCEPTION_MESSAGE, AbstractAssert::isNotNull), + satisfies( + EXCEPTION_STACKTRACE, + AbstractAssert::isNotNull))))); + } + + private Configuration getTimeoutConfig() { + Configuration timeoutConfig = HBaseConfiguration.create(connection.getConfiguration()); + timeoutConfig.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 0); + timeoutConfig.setLong(HConstants.HBASE_CLIENT_PAUSE, 1); + timeoutConfig.setInt( + HConstants.HBASE_CLIENT_OPERATION_TIMEOUT, GET_TIMEOUT_OPERATION_TIMEOUT_MILLIS); + timeoutConfig.setInt(HConstants.HBASE_RPC_TIMEOUT_KEY, GET_TIMEOUT_RPC_TIMEOUT_MILLIS); + timeoutConfig.setInt(HConstants.HBASE_RPC_READ_TIMEOUT_KEY, GET_TIMEOUT_RPC_TIMEOUT_MILLIS); + timeoutConfig.setInt(HConstants.HBASE_RPC_WRITE_TIMEOUT_KEY, GET_TIMEOUT_RPC_TIMEOUT_MILLIS); + return timeoutConfig; + } + + private void warmUpTimeoutConnection(Table table) throws IOException { + table.exists(new Get(Bytes.toBytes(ROW_1))); + testing().waitForTraces(2); + testing().clearData(); + } + + private void getWithPausedContainer(Table table) throws IOException { + hbaseContainer.getDockerClient().pauseContainerCmd(hbaseContainer.getContainerId()).exec(); + try { + table.get(new Get(Bytes.toBytes(ROW_1))); + } finally { + hbaseContainer.getDockerClient().unpauseContainerCmd(hbaseContainer.getContainerId()).exec(); + } + } + + @Test + void testScan() throws IOException { + List rowIdList = new ArrayList<>(); + try (Table table = connection.getTable(TABLE_NAME)) { + Scan scan = new Scan(); + scan.setCaching(5); + scan.setRowPrefixFilter(Bytes.toBytes(SCAN_ROW)); + try (ResultScanner scanner = table.getScanner(scan)) { + for (Result result : scanner) { + rowIdList.add(Bytes.toString(result.getRow())); + } + } + } + assertThat(rowIdList).containsExactly(SCAN_ROW); + testing().waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, SCAN, REGION_SERVER_PORT, true)); + } + + @Test + void testBatchGet() throws IOException { + Result[] results; + try (Table table = connection.getTable(TABLE_NAME)) { + List getList = new ArrayList<>(); + getList.add(new Get(Bytes.toBytes(ROW_1))); + getList.add(new Get(Bytes.toBytes(ROW_2))); + getList.add(new Get(Bytes.toBytes(ROW_5))); + results = table.get(getList); + } + assertThat(results).hasSize(3); + { + assertThat(Bytes.toString(results[0].getRow())).isEqualTo(ROW_1); + assertThat(value(results[0], "col1")).isEqualTo("col1_val_1"); + assertThat(value(results[0], "col2")).isEqualTo("col2_val_1"); + } + { + assertThat(Bytes.toString(results[1].getRow())).isNull(); + assertThat(value(results[1], "col1")).isNull(); + assertThat(value(results[1], "col2")).isNull(); + } + { + assertThat(Bytes.toString(results[2].getRow())).isNull(); + assertThat(value(results[2], "col1")).isNull(); + assertThat(value(results[2], "col2")).isNull(); + } + testing().waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MULTI, REGION_SERVER_PORT, true)); + } + + @Test + void testBatchPut() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + List putList = new ArrayList<>(); + for (int i = 2; i < 5; i++) { + Put put = new Put(Bytes.toBytes("batch-put-row" + i)); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col1"), Bytes.toBytes("col1_val_" + i)); + putList.add(put); + } + table.put(putList); + } + testing().waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MULTI, REGION_SERVER_PORT, true)); + } + + @Test + void testDelete() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + table.delete(new Delete(Bytes.toBytes(ROW_4))); + } + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testAppend() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + Append append = new Append(Bytes.toBytes("append-row")); + append.add(COLUMN_FAMILY, Bytes.toBytes("col3"), Bytes.toBytes(1L)); + table.append(append); + } + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testIncrement() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + Increment increment = new Increment(Bytes.toBytes("increment-row")); + increment.addColumn(COLUMN_FAMILY, Bytes.toBytes("col3"), 1L); + table.increment(increment); + } + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testCheckAndPutSuccess() throws IOException { + boolean success; + try (Table table = connection.getTable(TABLE_NAME)) { + byte[] rowKey = Bytes.toBytes(ROW_1); + Put put = new Put(rowKey); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col4"), Bytes.toBytes("new_value")); + success = + table.checkAndPut( + rowKey, COLUMN_FAMILY, Bytes.toBytes("col1"), Bytes.toBytes("col1_val_1"), put); + } + assertThat(success).isTrue(); + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testCheckAndPutFail() throws IOException { + boolean success; + try (Table table = connection.getTable(TABLE_NAME)) { + byte[] rowKey = Bytes.toBytes(ROW_1); + Put put = new Put(rowKey); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col5"), Bytes.toBytes("new_value")); + success = + table.checkAndPut( + rowKey, COLUMN_FAMILY, Bytes.toBytes("col1"), Bytes.toBytes("expected_value"), put); + } + assertThat(success).isFalse(); + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void testCheckAndMutateSuccess() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + byte[] rowKey = Bytes.toBytes(ROW_1); + Put put = new Put(Bytes.toBytes(ROW_3)); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col4"), Bytes.toBytes("new_value1")); + put.addColumn(COLUMN_FAMILY, Bytes.toBytes("col5"), Bytes.toBytes("new_value2")); + + RowMutations rowMutations = new RowMutations(Bytes.toBytes(ROW_3)); + rowMutations.add(put); + Delete delete = new Delete(Bytes.toBytes(ROW_3)); + delete.addColumns(COLUMN_FAMILY, Bytes.toBytes("col1")); + rowMutations.add(delete); + + table + .checkAndMutate(rowKey, COLUMN_FAMILY) + .qualifier(Bytes.toBytes("col1")) + .ifMatches(CompareOperator.EQUAL, Bytes.toBytes("col1_val_1")) + .thenMutate(rowMutations); + + Result result = table.get(new Get(Bytes.toBytes(ROW_3))); + assertThat(value(result, "col4")).isEqualTo("new_value1"); + assertThat(value(result, "col5")).isEqualTo("new_value2"); + assertThat(result.getValue(COLUMN_FAMILY, Bytes.toBytes("col1"))).isNull(); + } + testing() + .waitAndAssertTraces( + traceAssertConsumer(TABLE_NAME, MULTI, REGION_SERVER_PORT, true), + traceAssertConsumer(TABLE_NAME, GET, REGION_SERVER_PORT, true)); + } + + @Test + void testCheckAndDeleteSuccess() throws IOException { + boolean success; + try (Table table = connection.getTable(TABLE_NAME)) { + byte[] rowKey = Bytes.toBytes(ROW_1); + Delete delete = new Delete(rowKey); + delete.addColumn(COLUMN_FAMILY, Bytes.toBytes("col4")); + success = + table.checkAndDelete( + rowKey, COLUMN_FAMILY, Bytes.toBytes("col1"), Bytes.toBytes("col1_val_1"), delete); + } + assertThat(success).isTrue(); + testing() + .waitAndAssertTraces(traceAssertConsumer(TABLE_NAME, MUTATE, REGION_SERVER_PORT, true)); + } + + @Test + void hasDurationMetric() throws IOException { + try (Table table = connection.getTable(TABLE_NAME)) { + table.get(new Get(Bytes.toBytes(ROW_1))); + } + testing().waitForTraces(1); + assertDurationMetric( + testing(), + INSTRUMENTATION_NAME, + DB_SYSTEM_NAME, + maybeStable(DB_OPERATION), + maybeStable(DB_NAME), + SERVER_ADDRESS, + SERVER_PORT); + } + + protected Consumer traceAssertConsumer( + TableName table, String operation, int port, boolean hasTable) { + String spanName; + if (hasTable) { + spanName = operation + " " + table.getNameAsString(); + } else if (emitStableDatabaseSemconv()) { + spanName = operation + " " + hostname + ":" + port; + } else { + spanName = operation; + } + return trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(spanName) + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(DB_SYSTEM), maybeStableDbSystemName(DB_SYSTEM_VALUE)), + equalTo(maybeStable(DB_OPERATION), operation), + equalTo(maybeStable(DB_NAME), hasTable ? table.getNameAsString() : null), + equalTo(SERVER_ADDRESS, hostname), + equalTo(SERVER_PORT, port), + satisfies( + DB_USER, + emitStableDatabaseSemconv() + ? AbstractAssert::isNull + : AbstractAssert::isNotNull))); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d438a90e0bb6..f5494e111835 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -309,6 +309,8 @@ include(":instrumentation:gwt-2.0:javaagent") include(":instrumentation:helidon-4.3:javaagent") include(":instrumentation:helidon-4.3:library") include(":instrumentation:helidon-4.3:testing") +include(":instrumentation:hbase-client-2.0:javaagent") +include(":instrumentation:hbase-client-2.0:testing") include(":instrumentation:hibernate:hibernate-3.3:javaagent") include(":instrumentation:hibernate:hibernate-4.0:javaagent") include(":instrumentation:hibernate:hibernate-6.0:javaagent")