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")