This method blocks until the restore operation is complete, extracts the optimization token,
+ * and returns an ApiFuture for the optimization phase.
+ *
+ * @param restoreFuture The future returned by restoreTableAsync().
+ * @return An ApiFuture that tracks the optimization progress.
+ */
+ public ApiFuture awaitOptimizeRestoredTable(ApiFuture restoreFuture) {
+ // 1. Block and wait for the restore operation to complete
+ RestoredTableResult result;
+ try {
+ result = restoreFuture.get();
+ } catch (Exception e) {
+ throw new RuntimeException("Restore operation failed", e);
+ }
+
+ // 2. Extract the operation token from the result
+ // (RestoredTableResult already wraps the OptimizeRestoredTableOperationToken)
+ OptimizeRestoredTableOperationToken token = result.getOptimizeRestoredTableOperationToken();
+
+ if (token == null || Strings.isNullOrEmpty(token.getOperationName())) {
+ // If there is no optimization operation, return immediate success.
+ return ApiFutures.immediateFuture(Empty.getDefaultInstance());
+ }
+
+ // 3. Return the future for the optimization operation
+ return stub.awaitOptimizeRestoredTableCallable().resumeFutureCall(token.getOperationName());
+ }
+
/**
* Awaits a restored table is fully optimized.
*
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
index e89bd8fbb5..c1d5da6592 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java
@@ -45,6 +45,7 @@
import com.google.bigtable.admin.v2.ListBackupsRequest;
import com.google.bigtable.admin.v2.ListTablesRequest;
import com.google.bigtable.admin.v2.ModifyColumnFamiliesRequest.Modification;
+import com.google.bigtable.admin.v2.OptimizeRestoredTableMetadata;
import com.google.bigtable.admin.v2.RestoreSourceType;
import com.google.bigtable.admin.v2.RestoreTableMetadata;
import com.google.bigtable.admin.v2.SchemaBundleName;
@@ -76,6 +77,7 @@
import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest;
import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo;
import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest;
+import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken;
import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest;
import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult;
import com.google.cloud.bigtable.admin.v2.models.SchemaBundle;
@@ -285,6 +287,10 @@ public class BigtableTableAdminClientTests {
com.google.iam.v1.TestIamPermissionsRequest, com.google.iam.v1.TestIamPermissionsResponse>
mockTestIamPermissionsCallable;
+ @Mock
+ private OperationCallable
+ mockOptimizeRestoredTableCallable;
+
@Before
public void setUp() {
adminClient = BigtableTableAdminClient.create(PROJECT_ID, INSTANCE_ID, mockStub);
@@ -1682,6 +1688,59 @@ public void testWaitForConsistencyWithToken() {
assertThat(wasCalled.get()).isTrue();
}
+ @Test
+ public void testAwaitOptimizeRestoredTable() throws Exception {
+ // Setup
+ Mockito.when(mockStub.awaitOptimizeRestoredTableCallable())
+ .thenReturn(mockOptimizeRestoredTableCallable);
+
+ String optimizeToken = "my-optimization-token";
+
+ // 1. Mock the Token
+ OptimizeRestoredTableOperationToken mockToken =
+ Mockito.mock(OptimizeRestoredTableOperationToken.class);
+ Mockito.when(mockToken.getOperationName()).thenReturn(optimizeToken);
+
+ // 2. Mock the Result (wrapping the token)
+ RestoredTableResult mockResult = Mockito.mock(RestoredTableResult.class);
+ Mockito.when(mockResult.getOptimizeRestoredTableOperationToken()).thenReturn(mockToken);
+
+ // 3. Mock the Input Future (returning the result)
+ ApiFuture mockRestoreFuture = Mockito.mock(ApiFuture.class);
+ Mockito.when(mockRestoreFuture.get()).thenReturn(mockResult);
+
+ // 4. Mock the Stub's behavior (resuming the Optimize Op)
+ OperationFuture mockOptimizeOp =
+ Mockito.mock(OperationFuture.class);
+ Mockito.when(mockOptimizeRestoredTableCallable.resumeFutureCall(optimizeToken))
+ .thenReturn(mockOptimizeOp);
+
+ // Execute
+ ApiFuture result = adminClient.awaitOptimizeRestoredTable(mockRestoreFuture);
+
+ // Verify
+ assertThat(result).isEqualTo(mockOptimizeOp);
+ Mockito.verify(mockOptimizeRestoredTableCallable).resumeFutureCall(optimizeToken);
+ }
+
+ @Test
+ public void testAwaitOptimizeRestoredTable_NoOp() throws Exception {
+ // Setup: Result with NO optimization token (null or empty)
+ RestoredTableResult mockResult = Mockito.mock(RestoredTableResult.class);
+ Mockito.when(mockResult.getOptimizeRestoredTableOperationToken()).thenReturn(null);
+
+ // Mock the Input Future
+ ApiFuture mockRestoreFuture = Mockito.mock(ApiFuture.class);
+ Mockito.when(mockRestoreFuture.get()).thenReturn(mockResult);
+
+ // Execute
+ ApiFuture result = adminClient.awaitOptimizeRestoredTable(mockRestoreFuture);
+
+ // Verify: Returns immediate success (Empty) without calling the stub
+ assertThat(result.get()).isEqualTo(Empty.getDefaultInstance());
+ Mockito.verifyNoInteractions(mockStub);
+ }
+
private void mockOperationResult(
OperationCallable callable,
ReqT request,
From 054279404da3754a695bf2dcf0775b904cc2eaeb Mon Sep 17 00:00:00 2001
From: Tomo Suzuki
Date: Wed, 18 Feb 2026 16:25:19 -0500
Subject: [PATCH 03/33] chore: replace old Bigtable and Java teams with updated
names (#2790)
b/478003109
---
.github/CODEOWNERS | 10 +++++-----
.github/sync-repo-settings.yaml | 2 +-
.repo-metadata.json | 2 +-
generation_config.yaml | 2 +-
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 627419201d..e1f4bd0e65 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -4,11 +4,11 @@
# For syntax help see:
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
-# The @googleapis/api-bigtable is the default owner for changes in this repo
-* @googleapis/cloud-sdk-java-team @googleapis/api-bigtable
+# The @googleapis/bigtable-team is the default owner for changes in this repo
+* @googleapis/cloud-sdk-java-team @googleapis/bigtable-team
# for handwritten libraries, keep codeowner_team in .repo-metadata.json as owner
-**/*.java @googleapis/api-bigtable @googleapis/cloud-sdk-java-team
+**/*.java @googleapis/bigtable-team @googleapis/cloud-sdk-java-team
# The java-samples-reviewers team is the default owner for samples changes
@@ -18,5 +18,5 @@ samples/**/*.java @googleapis/java-samples-reviewers
samples/snippets/generated/ @googleapis/cloud-sdk-java-team
# Admin Module (Cloud Java Team ownership)
-**/com/google/cloud/bigtable/admin/** @googleapis/api-bigtable @googleapis/cloud-sdk-java-team
-**/com/google/bigtable/admin/** @googleapis/api-bigtable @googleapis/cloud-sdk-java-team
+**/com/google/cloud/bigtable/admin/** @googleapis/bigtable-team @googleapis/cloud-sdk-java-team
+**/com/google/bigtable/admin/** @googleapis/bigtable-team @googleapis/cloud-sdk-java-team
diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml
index 7be7e5e5f1..1005971cae 100644
--- a/.github/sync-repo-settings.yaml
+++ b/.github/sync-repo-settings.yaml
@@ -224,5 +224,5 @@ permissionRules:
permission: admin
- team: yoshi-java-admins
permission: admin
- - team: yoshi-java
+ - team: cloud-sdk-java-team
permission: push
diff --git a/.repo-metadata.json b/.repo-metadata.json
index d40cb5f9c0..8ac2726bf0 100644
--- a/.repo-metadata.json
+++ b/.repo-metadata.json
@@ -13,7 +13,7 @@
"api_id": "bigtable.googleapis.com",
"library_type": "GAPIC_COMBO",
"requires_billing": true,
- "codeowner_team": "@googleapis/api-bigtable",
+ "codeowner_team": "@googleapis/bigtable-team",
"excluded_poms": "google-cloud-bigtable-bom",
"issue_tracker": "https://issuetracker.google.com/savedsearches/559777",
"extra_versioned_modules": "google-cloud-bigtable-emulator,google-cloud-bigtable-emulator-core",
diff --git a/generation_config.yaml b/generation_config.yaml
index 206787fb31..0f2a822e5c 100644
--- a/generation_config.yaml
+++ b/generation_config.yaml
@@ -27,7 +27,7 @@ libraries:
issue_tracker: https://issuetracker.google.com/savedsearches/559777
release_level: stable
distribution_name: com.google.cloud:google-cloud-bigtable
- codeowner_team: '@googleapis/api-bigtable'
+ codeowner_team: '@googleapis/bigtable-team'
api_id: bigtable.googleapis.com
library_type: GAPIC_COMBO
extra_versioned_modules: google-cloud-bigtable-emulator,google-cloud-bigtable-emulator-core
From 54fee08e1e13e123ab5e6ac746ef620251755778 Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Thu, 19 Feb 2026 13:14:19 -0500
Subject: [PATCH 04/33] chore: introduce PeerInfo & MetadataExtractor (#2788)
* chore: introduce PeerInfo & MetadataExtractor
Centralize sideband metadata collection using a new interceptor. Which gets injected into the GrpcCallContext channel.
This provides the following benefits:
- it works even if the end user sets their own channel provider
- centralizes fetching of sideband metadata
- removes the need for fetching directpath signals from grpc internals
Change-Id: I42917074d65ccd7b8680f4a2a10c904b7646e4b6
* format
Change-Id: Id9dfd28ca4a3f9e98474b33c98895e05b830b410
* oops
Change-Id: I87f110743cd6261e07b59bdcf2bc005af0916d35
* format
Change-Id: Ide93ae0406012e8e5779d65cb9770dbda5d0562e
* remove replaced location & gfe methods
Change-Id: I2aff3f13f2f07b400d6f2099b75c9f1462077e44
* add todo
Change-Id: If07939e62e04e9c7a27862bf87ca5b8731711b75
* fix null handling of sideband data formating and remove stale code
Change-Id: Icf1b8ed5d020c9bf5386173b817a89f1679369b4
* todo
Change-Id: I99e5dd3b4b2397d32fbd41ac7c4e697f6788cd4f
* remove stale dep
Change-Id: I3c98eef573aea3364f2788135407acbba991d7c7
* Eagerly set sideband data instead of deferring until onClose
Also defensively add null checks for it and a todo to remove them
Change-Id: Ie8237bfcc8c5a0886735ca5b93c0f03f5373e24b
---
google-cloud-bigtable/pom.xml | 5 -
.../v2/stub/EnhancedBigtableStubSettings.java | 3 +-
.../v2/stub/MetadataExtractorInterceptor.java | 198 ++++++++++++++++++
.../metrics/BigtableGrpcStreamTracer.java | 33 +--
.../data/v2/stub/metrics/BigtableTracer.java | 27 +--
.../BigtableTracerStreamingCallable.java | 51 ++---
.../metrics/BigtableTracerUnaryCallable.java | 66 ++----
.../v2/stub/metrics/BuiltinMetricsTracer.java | 94 +++------
.../data/v2/stub/metrics/CompositeTracer.java | 28 +--
.../data/v2/stub/metrics/MetricsTracer.java | 26 ++-
.../bigtable/data/v2/stub/metrics/Util.java | 146 +++----------
.../metrics/BuiltinMetricsTracerTest.java | 5 +-
.../v2/stub/metrics/CompositeTracerTest.java | 21 +-
.../data/v2/stub/metrics/UtilTest.java | 22 --
14 files changed, 328 insertions(+), 397 deletions(-)
create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml
index 30d61cdc6a..bd4c6f0b63 100644
--- a/google-cloud-bigtable/pom.xml
+++ b/google-cloud-bigtable/pom.xml
@@ -136,10 +136,6 @@
com.google.protobufprotobuf-java-util
-
- com.google.code.gson
- gson
- io.opencensusopencensus-api
@@ -147,7 +143,6 @@
io.grpcgrpc-alts
- runtimeorg.checkerframework
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
index d1fe259ea1..6a9dcdfbec 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
@@ -922,7 +922,8 @@ private Builder() {
.setReverseScans(true)
.setLastScannedRowResponses(true)
.setDirectAccessRequested(DIRECT_PATH_ENABLED)
- .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED);
+ .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED)
+ .setPeerInfo(true);
}
private Builder(EnhancedBigtableStubSettings settings) {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
new file mode 100644
index 0000000000..5b43f57527
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.stub;
+
+import com.google.api.core.InternalApi;
+import com.google.api.gax.grpc.GrpcCallContext;
+import com.google.bigtable.v2.PeerInfo;
+import com.google.bigtable.v2.ResponseParams;
+import com.google.common.base.Strings;
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.grpc.Attributes;
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.ClientInterceptors;
+import io.grpc.ForwardingClientCall;
+import io.grpc.ForwardingClientCallListener;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import io.grpc.alts.AltsContextUtil;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+@InternalApi
+public class MetadataExtractorInterceptor implements ClientInterceptor {
+ private final SidebandData sidebandData = new SidebandData();
+
+ public GrpcCallContext injectInto(GrpcCallContext ctx) {
+ // TODO: migrate to using .withTransportChannel
+ // This will require a change on gax's side to expose the underlying ManagedChannel in
+ // GrpcTransportChannel (its currently package private).
+ return ctx.withChannel(ClientInterceptors.intercept(ctx.getChannel(), this))
+ .withCallOptions(ctx.getCallOptions().withOption(SidebandData.KEY, sidebandData));
+ }
+
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) {
+ return new ForwardingClientCall.SimpleForwardingClientCall(
+ channel.newCall(methodDescriptor, callOptions)) {
+ @Override
+ public void start(Listener responseListener, Metadata headers) {
+ sidebandData.reset();
+
+ super.start(
+ new ForwardingClientCallListener.SimpleForwardingClientCallListener(
+ responseListener) {
+ @Override
+ public void onHeaders(Metadata headers) {
+ sidebandData.onResponseHeaders(headers, getAttributes());
+ super.onHeaders(headers);
+ }
+
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ sidebandData.onClose(status, trailers);
+ super.onClose(status, trailers);
+ }
+ },
+ headers);
+ }
+ };
+ }
+
+ public SidebandData getSidebandData() {
+ return sidebandData;
+ }
+
+ public static class SidebandData {
+ private static final CallOptions.Key KEY =
+ CallOptions.Key.create("bigtable-sideband");
+
+ private static final Metadata.Key SERVER_TIMING_HEADER_KEY =
+ Metadata.Key.of("server-timing", Metadata.ASCII_STRING_MARSHALLER);
+ private static final Pattern SERVER_TIMING_HEADER_PATTERN =
+ Pattern.compile(".*dur=(?\\d+)");
+ private static final Metadata.Key LOCATION_METADATA_KEY =
+ Metadata.Key.of("x-goog-ext-425905942-bin", Metadata.BINARY_BYTE_MARSHALLER);
+ private static final Metadata.Key PEER_INFO_KEY =
+ Metadata.Key.of("bigtable-peer-info", Metadata.ASCII_STRING_MARSHALLER);
+
+ @Nullable private volatile ResponseParams responseParams;
+ @Nullable private volatile PeerInfo peerInfo;
+ @Nullable private volatile Long gfeTiming;
+
+ @Nullable
+ public ResponseParams getResponseParams() {
+ return responseParams;
+ }
+
+ @Nullable
+ public PeerInfo getPeerInfo() {
+ return peerInfo;
+ }
+
+ @Nullable
+ public Long getGfeTiming() {
+ return gfeTiming;
+ }
+
+ private void reset() {
+ responseParams = null;
+ peerInfo = null;
+ gfeTiming = null;
+ }
+
+ void onResponseHeaders(Metadata md, Attributes attributes) {
+ responseParams = extractResponseParams(md);
+ gfeTiming = extractGfeLatency(md);
+ peerInfo = extractPeerInfo(md, gfeTiming, attributes);
+ }
+
+ void onClose(Status status, Metadata trailers) {
+ if (responseParams == null) {
+ responseParams = extractResponseParams(trailers);
+ }
+ }
+
+ @Nullable
+ private static Long extractGfeLatency(Metadata metadata) {
+ String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
+ if (serverTiming == null) {
+ return null;
+ }
+ Matcher matcher = SERVER_TIMING_HEADER_PATTERN.matcher(serverTiming);
+ // this should always be true
+ if (matcher.find()) {
+ return Long.parseLong(matcher.group("dur"));
+ }
+ return null;
+ }
+
+ @Nullable
+ private static PeerInfo extractPeerInfo(
+ Metadata metadata, Long gfeTiming, Attributes attributes) {
+ String encodedStr = metadata.get(PEER_INFO_KEY);
+ if (Strings.isNullOrEmpty(encodedStr)) {
+ return null;
+ }
+
+ try {
+ byte[] decoded = Base64.getUrlDecoder().decode(encodedStr);
+ PeerInfo peerInfo = PeerInfo.parseFrom(decoded);
+ PeerInfo.TransportType effectiveTransport = peerInfo.getTransportType();
+
+ // TODO: remove this once transport_type is being sent by the server
+ // This is a temporary workaround to detect directpath until its available from the server
+ if (effectiveTransport == PeerInfo.TransportType.TRANSPORT_TYPE_UNKNOWN) {
+ boolean isAlts = AltsContextUtil.check(attributes);
+ if (isAlts) {
+ effectiveTransport = PeerInfo.TransportType.TRANSPORT_TYPE_DIRECT_ACCESS;
+ } else if (gfeTiming != null) {
+ effectiveTransport = PeerInfo.TransportType.TRANSPORT_TYPE_CLOUD_PATH;
+ }
+ }
+ if (effectiveTransport != PeerInfo.TransportType.TRANSPORT_TYPE_UNKNOWN) {
+ peerInfo = peerInfo.toBuilder().setTransportType(effectiveTransport).build();
+ }
+ return peerInfo;
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Failed to parse "
+ + PEER_INFO_KEY.name()
+ + " from the response header value: "
+ + encodedStr);
+ }
+ }
+
+ @Nullable
+ private static ResponseParams extractResponseParams(Metadata metadata) {
+ byte[] responseParams = metadata.get(LOCATION_METADATA_KEY);
+ if (responseParams != null) {
+ try {
+ return ResponseParams.parseFrom(responseParams);
+ } catch (InvalidProtocolBufferException e) {
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableGrpcStreamTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableGrpcStreamTracer.java
index a364adbc46..9b220c1de3 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableGrpcStreamTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableGrpcStreamTracer.java
@@ -15,10 +15,8 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsTracer.TransportAttrs;
import io.grpc.ClientStreamTracer;
import io.grpc.Metadata;
-import io.grpc.Status;
/**
* Records the time a request is enqueued in a grpc channel queue. This a bridge between gRPC stream
@@ -26,16 +24,9 @@
* asking gRPC to start an RPC and gRPC actually serializing that RPC.
*/
class BigtableGrpcStreamTracer extends ClientStreamTracer {
- private static final String GRPC_LB_LOCALITY_KEY = "grpc.lb.locality";
- private static final String GRPC_LB_BACKEND_SERVICE_KEY = "grpc.lb.backend_service";
-
- private final StreamInfo info;
private final BigtableTracer tracer;
- private volatile String backendService = null;
- private volatile String locality = null;
- public BigtableGrpcStreamTracer(StreamInfo info, BigtableTracer tracer) {
- this.info = info;
+ public BigtableGrpcStreamTracer(BigtableTracer tracer) {
this.tracer = tracer;
}
@@ -44,26 +35,6 @@ public void outboundMessageSent(int seqNo, long optionalWireSize, long optionalU
tracer.grpcMessageSent();
}
- @Override
- public void addOptionalLabel(String key, String value) {
- switch (key) {
- case GRPC_LB_LOCALITY_KEY:
- this.locality = value;
- break;
- case GRPC_LB_BACKEND_SERVICE_KEY:
- this.backendService = value;
- break;
- }
-
- super.addOptionalLabel(key, value);
- }
-
- @Override
- public void streamClosed(Status status) {
- tracer.setTransportAttrs(TransportAttrs.create(locality, backendService));
- super.streamClosed(status);
- }
-
static class Factory extends ClientStreamTracer.Factory {
private final BigtableTracer tracer;
@@ -75,7 +46,7 @@ static class Factory extends ClientStreamTracer.Factory {
@Override
public ClientStreamTracer newClientStreamTracer(
ClientStreamTracer.StreamInfo info, Metadata headers) {
- return new BigtableGrpcStreamTracer(info, tracer);
+ return new BigtableGrpcStreamTracer(tracer);
}
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java
index 898d743cd9..a1a53b6089 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java
@@ -20,6 +20,7 @@
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.tracing.ApiTracer;
import com.google.api.gax.tracing.BaseApiTracer;
+import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import java.time.Duration;
import javax.annotation.Nullable;
@@ -70,36 +71,12 @@ public int getAttempt() {
return attempt;
}
- /**
- * Record the latency between Google's network receives the RPC and reads back the first byte of
- * the response from server-timing header. If server-timing header is missing, increment the
- * missing header count.
- */
- public void recordGfeMetadata(@Nullable Long latency, @Nullable Throwable throwable) {
- // noop
- }
-
/** Adds an annotation of the total throttled time of a batch. */
public void batchRequestThrottled(long throttledTimeMs) {
// noop
}
- /**
- * Set the Bigtable zone and cluster so metrics can be tagged with location information. This will
- * be called in BuiltinMetricsTracer.
- */
- public void setLocations(String zone, String cluster) {
- // noop
- }
-
- /** Set the underlying transport used to process the attempt */
- public void setTransportAttrs(BuiltinMetricsTracer.TransportAttrs attrs) {}
-
- @Deprecated
- /**
- * @deprecated {@link #grpcMessageSent()} is called instead.
- */
- public void grpcChannelQueuedLatencies(long queuedTimeMs) {
+ public void setSidebandData(MetadataExtractorInterceptor.SidebandData sidebandData) {
// noop
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java
index 13b832b8b1..3cdcdc374e 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java
@@ -16,11 +16,12 @@
package com.google.cloud.bigtable.data.v2.stub.metrics;
import com.google.api.core.InternalApi;
-import com.google.api.gax.grpc.GrpcResponseMetadata;
+import com.google.api.gax.grpc.GrpcCallContext;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ResponseObserver;
import com.google.api.gax.rpc.ServerStreamingCallable;
import com.google.api.gax.rpc.StreamController;
+import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.cloud.bigtable.data.v2.stub.SafeResponseObserver;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
@@ -28,19 +29,8 @@
import javax.annotation.Nonnull;
/**
- * This callable will
- *
-Inject a {@link GrpcResponseMetadata} to access the headers returned by gRPC methods upon
- * completion. The {@link BigtableTracer} will process metrics that were injected in the
- * header/trailer and publish them to OpenCensus. If {@link GrpcResponseMetadata#getMetadata()}
- * returned null, it probably means that the request has never reached GFE, and it'll increment
- * the gfe_header_missing_counter in this case.
- *
-This class will also access trailers from {@link GrpcResponseMetadata} to record zone and
- * cluster ids.
- *
-Call {@link BigtableTracer#onRequest(int)} to record the request events in a stream.
- *
-This class will also inject a {@link BigtableGrpcStreamTracer} that'll record the time an
- * RPC spent in a grpc channel queue.
- *
This class is considered an internal implementation detail and not meant to be used by
- * applications.
+ * This class is considered an internal implementation detail and not meant to be used by
+ * applications.
*/
@InternalApi
public class BigtableTracerStreamingCallable
@@ -56,40 +46,41 @@ public BigtableTracerStreamingCallable(
@Override
public void call(
RequestT request, ResponseObserver responseObserver, ApiCallContext context) {
- final GrpcResponseMetadata responseMetadata = new GrpcResponseMetadata();
+ GrpcCallContext grpcCtx = (GrpcCallContext) context;
+
+ MetadataExtractorInterceptor metadataExtractor = new MetadataExtractorInterceptor();
+ grpcCtx = metadataExtractor.injectInto(grpcCtx);
+
// tracer should always be an instance of bigtable tracer
if (context.getTracer() instanceof BigtableTracer) {
BigtableTracer tracer = (BigtableTracer) context.getTracer();
+ tracer.setSidebandData(metadataExtractor.getSidebandData());
+ grpcCtx =
+ grpcCtx.withCallOptions(
+ grpcCtx
+ .getCallOptions()
+ .withStreamTracerFactory(new BigtableGrpcStreamTracer.Factory(tracer)));
+
BigtableTracerResponseObserver innerObserver =
- new BigtableTracerResponseObserver<>(responseObserver, tracer, responseMetadata);
+ new BigtableTracerResponseObserver<>(responseObserver, tracer);
if (context.getRetrySettings() != null) {
tracer.setTotalTimeoutDuration(context.getRetrySettings().getTotalTimeoutDuration());
}
- innerCallable.call(
- request,
- innerObserver,
- Util.injectBigtableStreamTracer(
- context, responseMetadata, (BigtableTracer) context.getTracer()));
+ innerCallable.call(request, innerObserver, grpcCtx);
} else {
- innerCallable.call(request, responseObserver, context);
+ innerCallable.call(request, responseObserver, grpcCtx);
}
}
private class BigtableTracerResponseObserver extends SafeResponseObserver {
-
private final BigtableTracer tracer;
private final ResponseObserver outerObserver;
- private final GrpcResponseMetadata responseMetadata;
- BigtableTracerResponseObserver(
- ResponseObserver observer,
- BigtableTracer tracer,
- GrpcResponseMetadata metadata) {
+ BigtableTracerResponseObserver(ResponseObserver observer, BigtableTracer tracer) {
super(observer);
this.tracer = tracer;
this.outerObserver = observer;
- this.responseMetadata = metadata;
}
@Override
@@ -107,13 +98,11 @@ protected void onResponseImpl(ResponseT response) {
@Override
protected void onErrorImpl(Throwable t) {
- Util.recordMetricsFromMetadata(responseMetadata, tracer, t);
outerObserver.onError(t);
}
@Override
protected void onCompleteImpl() {
- Util.recordMetricsFromMetadata(responseMetadata, tracer, null);
outerObserver.onComplete();
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java
index 37ba74bfdb..363a69af3d 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java
@@ -16,29 +16,17 @@
package com.google.cloud.bigtable.data.v2.stub.metrics;
import com.google.api.core.ApiFuture;
-import com.google.api.core.ApiFutureCallback;
-import com.google.api.core.ApiFutures;
import com.google.api.core.InternalApi;
-import com.google.api.gax.grpc.GrpcResponseMetadata;
+import com.google.api.gax.grpc.GrpcCallContext;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.UnaryCallable;
+import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.MoreExecutors;
import javax.annotation.Nonnull;
/**
- * This callable will:
- *
- Inject a {@link GrpcResponseMetadata} to access the headers returned by gRPC methods upon
- * completion. The {@link BigtableTracer} will process metrics that were injected in the
- * header/trailer and publish them to OpenCensus. If {@link GrpcResponseMetadata#getMetadata()}
- * returned null, it probably means that the request has never reached GFE, and it'll increment
- * the gfe_header_missing_counter in this case.
- *
-This class will also access trailers from {@link GrpcResponseMetadata} to record zone and
- * cluster ids.
- *
-This class will also inject a {@link BigtableGrpcStreamTracer} that'll record the time an
- * RPC spent in a grpc channel queue.
- *
This class is considered an internal implementation detail and not meant to be used by
- * applications.
+ * This class is considered an internal implementation detail and not meant to be used by
+ * applications.
*/
@InternalApi
public class BigtableTracerUnaryCallable
@@ -52,46 +40,24 @@ public BigtableTracerUnaryCallable(@Nonnull UnaryCallable i
@Override
public ApiFuture futureCall(RequestT request, ApiCallContext context) {
+ MetadataExtractorInterceptor interceptor = new MetadataExtractorInterceptor();
+ GrpcCallContext grpcCtx = interceptor.injectInto((GrpcCallContext) context);
+
// tracer should always be an instance of BigtableTracer
if (context.getTracer() instanceof BigtableTracer) {
BigtableTracer tracer = (BigtableTracer) context.getTracer();
- final GrpcResponseMetadata responseMetadata = new GrpcResponseMetadata();
- BigtableTracerUnaryCallback callback =
- new BigtableTracerUnaryCallback(
- (BigtableTracer) context.getTracer(), responseMetadata);
+ tracer.setSidebandData(interceptor.getSidebandData());
+
+ grpcCtx =
+ grpcCtx.withCallOptions(
+ grpcCtx
+ .getCallOptions()
+ .withStreamTracerFactory(new BigtableGrpcStreamTracer.Factory(tracer)));
+
if (context.getRetrySettings() != null) {
tracer.setTotalTimeoutDuration(context.getRetrySettings().getTotalTimeoutDuration());
}
- ApiFuture future =
- innerCallable.futureCall(
- request,
- Util.injectBigtableStreamTracer(
- context, responseMetadata, (BigtableTracer) context.getTracer()));
- ApiFutures.addCallback(future, callback, MoreExecutors.directExecutor());
- return future;
- } else {
- return innerCallable.futureCall(request, context);
- }
- }
-
- private class BigtableTracerUnaryCallback implements ApiFutureCallback {
-
- private final BigtableTracer tracer;
- private final GrpcResponseMetadata responseMetadata;
-
- BigtableTracerUnaryCallback(BigtableTracer tracer, GrpcResponseMetadata responseMetadata) {
- this.tracer = tracer;
- this.responseMetadata = responseMetadata;
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Util.recordMetricsFromMetadata(responseMetadata, tracer, throwable);
- }
-
- @Override
- public void onSuccess(ResponseT response) {
- Util.recordMetricsFromMetadata(responseMetadata, tracer, null);
}
+ return innerCallable.futureCall(request, grpcCtx);
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
index e6ebad367a..546ea41c9f 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
@@ -34,27 +34,23 @@
import com.google.api.core.ObsoleteApi;
import com.google.api.gax.retrying.ServerStreamingAttemptException;
import com.google.api.gax.tracing.SpanName;
-import com.google.auto.value.AutoValue;
+import com.google.bigtable.v2.PeerInfo;
import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.common.base.Stopwatch;
-import com.google.common.base.Strings;
import com.google.common.math.IntMath;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
import io.grpc.Deadline;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleGauge;
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import java.time.Duration;
-import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Level;
-import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
@@ -62,24 +58,6 @@
* bigtable.googleapis.com/client namespace
*/
class BuiltinMetricsTracer extends BigtableTracer {
- @AutoValue
- abstract static class TransportAttrs {
- @Nullable
- abstract String getLocality();
-
- @Nullable
- abstract String getBackendService();
-
- static TransportAttrs create(@Nullable String locality, @Nullable String backendService) {
- return new AutoValue_BuiltinMetricsTracer_TransportAttrs(locality, backendService);
- }
- }
-
- private static final Logger logger = Logger.getLogger(BuiltinMetricsTracer.class.getName());
- private static final Gson GSON = new Gson();
- private static final TypeToken
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
index b8a514433f..4329e98f63 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java
@@ -450,7 +450,6 @@ public boolean isRefreshingChannel() {
*/
@Deprecated
public Builder setPrimingTableIds(String... tableIds) {
- stubSettings.setPrimedTableIds(tableIds);
return this;
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index 97f4aad495..d71355d6cd 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -128,10 +128,7 @@ public static BigtableClientContext create(EnhancedBigtableStubSettings settings
}
if (transportProvider != null) {
- // Set up cookie holder if routing cookie is enabled
- if (builder.getEnableRoutingCookie()) {
- setupCookieHolder(transportProvider);
- }
+ setupCookieHolder(transportProvider);
ChannelPrimer channelPrimer = NoOpChannelPrimer.create();
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
index 18361f1568..b3cc8d3655 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
@@ -121,7 +121,6 @@
import com.google.cloud.bigtable.data.v2.stub.sql.MetadataErrorHandlingCallable;
import com.google.cloud.bigtable.data.v2.stub.sql.PlanRefreshingCallable;
import com.google.cloud.bigtable.data.v2.stub.sql.SqlRowMergingCallable;
-import com.google.cloud.bigtable.gaxx.retrying.ApiResultRetryAlgorithm;
import com.google.cloud.bigtable.gaxx.retrying.RetryInfoRetryAlgorithm;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
@@ -785,12 +784,9 @@ private UnaryCallable createMutateRowsBas
ServerStreamingCallable withAttemptTracer =
new BigtableTracerStreamingCallable<>(convertException);
- BasicResultRetryAlgorithm resultRetryAlgorithm;
- if (settings.getEnableRetryInfo()) {
- resultRetryAlgorithm = new RetryInfoRetryAlgorithm<>();
- } else {
- resultRetryAlgorithm = new ApiResultRetryAlgorithm<>();
- }
+ BasicResultRetryAlgorithm resultRetryAlgorithm =
+ new RetryInfoRetryAlgorithm<>();
+
MutateRowsPartialErrorRetryAlgorithm mutateRowsPartialErrorRetryAlgorithm =
new MutateRowsPartialErrorRetryAlgorithm(resultRetryAlgorithm);
@@ -810,11 +806,8 @@ private UnaryCallable createMutateRowsBas
settings.bulkMutateRowsSettings().getRetryableCodes(),
retryAlgorithm);
- UnaryCallable withCookie = baseCallable;
-
- if (settings.getEnableRoutingCookie()) {
- withCookie = new CookiesUnaryCallable<>(baseCallable);
- }
+ UnaryCallable withCookie =
+ new CookiesUnaryCallable<>(baseCallable);
UnaryCallable flowControlCallable = null;
if (settings.bulkMutateRowsSettings().isLatencyBasedThrottlingEnabled()) {
@@ -1319,56 +1312,31 @@ ServerStreamingCallSettings convertUnaryToServerStreamingSettings(
private UnaryCallable withRetries(
UnaryCallable innerCallable, UnaryCallSettings, ?> unaryCallSettings) {
- UnaryCallable retrying;
- if (settings.getEnableRetryInfo()) {
- retrying =
- com.google.cloud.bigtable.gaxx.retrying.Callables.retrying(
- innerCallable, unaryCallSettings, bigtableClientContext.getClientContext());
- } else {
- retrying =
- Callables.retrying(
- innerCallable, unaryCallSettings, bigtableClientContext.getClientContext());
- }
- if (settings.getEnableRoutingCookie()) {
- return new CookiesUnaryCallable<>(retrying);
- }
- return retrying;
+ UnaryCallable retrying =
+ com.google.cloud.bigtable.gaxx.retrying.Callables.retrying(
+ innerCallable, unaryCallSettings, bigtableClientContext.getClientContext());
+ return new CookiesUnaryCallable<>(retrying);
}
private ServerStreamingCallable withRetries(
ServerStreamingCallable innerCallable,
ServerStreamingCallSettings serverStreamingCallSettings) {
- ServerStreamingCallable retrying;
- if (settings.getEnableRetryInfo()) {
- retrying =
- com.google.cloud.bigtable.gaxx.retrying.Callables.retrying(
- innerCallable, serverStreamingCallSettings, bigtableClientContext.getClientContext());
- } else {
- retrying =
- Callables.retrying(
- innerCallable, serverStreamingCallSettings, bigtableClientContext.getClientContext());
- }
- if (settings.getEnableRoutingCookie()) {
- return new CookiesServerStreamingCallable<>(retrying);
- }
- return retrying;
+ ServerStreamingCallable retrying =
+ com.google.cloud.bigtable.gaxx.retrying.Callables.retrying(
+ innerCallable, serverStreamingCallSettings, bigtableClientContext.getClientContext());
+
+ return new CookiesServerStreamingCallable<>(retrying);
}
private ServerStreamingCallable largeRowWithRetries(
ServerStreamingCallable innerCallable,
ServerStreamingCallSettings serverStreamingCallSettings) {
- // Retrying algorithm in retryingForLargeRows also takes RetryInfo into consideration, so we
- // skip the check for settings.getEnableRetryInfo here
- ServerStreamingCallable retrying;
- retrying =
+ ServerStreamingCallable retrying =
com.google.cloud.bigtable.gaxx.retrying.Callables.retryingForLargeRows(
innerCallable, serverStreamingCallSettings, bigtableClientContext.getClientContext());
- if (settings.getEnableRoutingCookie()) {
- return new CookiesServerStreamingCallable<>(retrying);
- }
- return retrying;
+ return new CookiesServerStreamingCallable<>(retrying);
}
//
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
index f0c959cc67..003823f5fc 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
@@ -65,7 +65,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.threeten.bp.Duration;
@@ -99,9 +98,6 @@
* }
*/
public class EnhancedBigtableStubSettings extends StubSettings {
- private static final Logger logger =
- Logger.getLogger(EnhancedBigtableStubSettings.class.getName());
-
// The largest message that can be received is a 256 MB ReadRowsResponse.
private static final int MAX_MESSAGE_SIZE = 256 * 1024 * 1024;
private static final String SERVER_DEFAULT_APP_PROFILE_ID = "";
@@ -145,7 +141,6 @@ public class EnhancedBigtableStubSettings extends StubSettings primedTableIds;
- private final boolean enableRoutingCookie;
- private final boolean enableRetryInfo;
private final ServerStreamingCallSettings readRowsSettings;
private final UnaryCallSettings readRowSettings;
@@ -279,7 +269,7 @@ public class EnhancedBigtableStubSettings extends StubSettings getPrimedTableIds() {
- return primedTableIds;
+ return ImmutableList.of();
}
/**
@@ -384,21 +371,19 @@ public MetricsProvider getMetricsProvider() {
}
/**
- * Gets if routing cookie is enabled. If true, client will retry a request with extra metadata
- * server sent back.
+ * @deprecated routing cookies are always on.
*/
- @BetaApi("Routing cookie is not currently stable and may change in the future")
+ @Deprecated
public boolean getEnableRoutingCookie() {
- return enableRoutingCookie;
+ return true;
}
/**
- * Gets if RetryInfo is enabled. If true, client bases retry decision and back off time on server
- * returned RetryInfo value. Otherwise, client uses {@link RetrySettings}.
+ * @deprecated RetryInfo is now always on.
*/
- @BetaApi("RetryInfo is not currently stable and may change in the future")
+ @Deprecated
public boolean getEnableRetryInfo() {
- return enableRetryInfo;
+ return true;
}
/**
@@ -745,10 +730,7 @@ public static class Builder extends StubSettings.Builder primedTableIds;
private String jwtAudience;
- private boolean enableRoutingCookie;
- private boolean enableRetryInfo;
private final ServerStreamingCallSettings.Builder readRowsSettings;
private final UnaryCallSettings.Builder readRowSettings;
@@ -768,7 +750,7 @@ public static class Builder extends StubSettings.Builder
prepareQuerySettings;
- private FeatureFlags.Builder featureFlags;
+ private final FeatureFlags.Builder featureFlags;
private MetricsProvider metricsProvider;
@Nullable private String metricsEndpoint;
@@ -785,10 +767,7 @@ public static class Builder extends StubSettings.Builder getPrimedTableIds() {
- return primedTableIds;
+ return ImmutableList.of();
}
/**
@@ -1159,41 +1134,35 @@ String getJwtAudience() {
}
/**
- * Sets if routing cookie is enabled. If true, client will retry a request with extra metadata
- * server sent back.
+ * @deprecated this now a no-op as routing cookies are always on.
*/
- @BetaApi("Routing cookie is not currently stable and may change in the future")
+ @Deprecated
public Builder setEnableRoutingCookie(boolean enableRoutingCookie) {
- this.enableRoutingCookie = enableRoutingCookie;
return this;
}
/**
- * Gets if routing cookie is enabled. If true, client will retry a request with extra metadata
- * server sent back.
+ * @deprecated routing cookies are always on.
*/
- @BetaApi("Routing cookie is not currently stable and may change in the future")
+ @Deprecated
public boolean getEnableRoutingCookie() {
- return enableRoutingCookie;
+ return true;
}
/**
- * Sets if RetryInfo is enabled. If true, client bases retry decision and back off time on
- * server returned RetryInfo value. Otherwise, client uses {@link RetrySettings}.
+ * @deprecated This is a no-op, RetryInfo is always used now.
*/
- @BetaApi("RetryInfo is not currently stable and may change in the future")
+ @Deprecated
public Builder setEnableRetryInfo(boolean enableRetryInfo) {
- this.enableRetryInfo = enableRetryInfo;
return this;
}
/**
- * Gets if RetryInfo is enabled. If true, client bases retry decision and back off time on
- * server returned RetryInfo value. Otherwise, client uses {@link RetrySettings}.
+ * @deprecated RetryInfo is always on.
*/
- @BetaApi("RetryInfo is not currently stable and may change in the future")
+ @Deprecated
public boolean getEnableRetryInfo() {
- return enableRetryInfo;
+ return true;
}
/** Returns the builder for the settings used for calls to readRows. */
@@ -1283,8 +1252,8 @@ public EnhancedBigtableStubSettings build() {
featureFlags.setMutateRowsRateLimit2(true);
}
- featureFlags.setRoutingCookie(this.getEnableRoutingCookie());
- featureFlags.setRetryInfo(this.getEnableRetryInfo());
+ featureFlags.setRoutingCookie(true);
+ featureFlags.setRetryInfo(true);
// client_Side_metrics_enabled feature flag is only set when a user is running with a
// DefaultMetricsProvider. This may cause false negatives when a user registered the
// metrics on their CustomOpenTelemetryMetricsProvider.
@@ -1325,9 +1294,6 @@ public String toString() {
.add("instanceId", instanceId)
.add("appProfileId", appProfileId)
.add("isRefreshingChannel", isRefreshingChannel)
- .add("primedTableIds", primedTableIds)
- .add("enableRoutingCookie", enableRoutingCookie)
- .add("enableRetryInfo", enableRetryInfo)
.add("readRowsSettings", readRowsSettings)
.add("readRowSettings", readRowSettings)
.add("sampleRowKeysSettings", sampleRowKeysSettings)
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CookiesHolderTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CookiesHolderTest.java
index bf02ce447a..648cff4809 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CookiesHolderTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CookiesHolderTest.java
@@ -69,7 +69,6 @@
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -673,58 +672,6 @@ public void testCookieSetWithBigtableClientFactory() throws Exception {
}
}
- @Test
- public void testDisableRoutingCookie() throws IOException {
- // This test disables routing cookie in the client settings and ensures that none of the routing
- // cookie
- // is added.
- settings.stubSettings().setEnableRoutingCookie(false);
- try (BigtableDataClient client = BigtableDataClient.create(settings.build())) {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored = Lists.newArrayList(client.readRows(Query.create("fake-table")));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- client.mutateRow(RowMutation.create("fake-table", "key").setCell("cf", "q", "v"));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- client.bulkMutateRows(
- BulkMutation.create("fake-table")
- .add(RowMutationEntry.create("key").setCell("cf", "q", "v")));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- client.sampleRowKeys("fake-table");
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- client.checkAndMutateRow(
- ConditionalRowMutation.create("fake-table", "key")
- .then(Mutation.create().setCell("cf", "q", "v")));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- client.readModifyWriteRow(
- ReadModifyWriteRow.create("fake-table", "key").append("cf", "q", "v"));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored2 =
- Lists.newArrayList(client.generateInitialChangeStreamPartitions("fake-table"));
- assertThat(fakeService.count.get()).isEqualTo(2);
- fakeService.count.set(0);
-
- for (ChangeStreamRecord record :
- client.readChangeStream(ReadChangeStreamQuery.create("fake-table"))) {}
-
- assertThat(fakeService.count.get()).isEqualTo(2);
-
- assertThat(methods).isEmpty();
- }
- }
-
static class FakeService extends BigtableGrpc.BigtableImplBase {
private volatile boolean returnCookie = true;
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
index aecad0cc12..9de6319182 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
@@ -94,8 +94,6 @@ public void settingsAreNotLostTest() {
.setCredentialsProvider(credentialsProvider)
.setStreamWatchdogProvider(watchdogProvider)
.setStreamWatchdogCheckInterval(watchdogInterval)
- .setEnableRoutingCookie(enableRoutingCookie)
- .setEnableRetryInfo(enableRetryInfo)
.setMetricsEndpoint(metricsEndpoint);
verifyBuilder(
@@ -160,8 +158,6 @@ private void verifyBuilder(
assertThat(builder.getCredentialsProvider()).isEqualTo(credentialsProvider);
assertThat(builder.getStreamWatchdogProvider()).isSameInstanceAs(watchdogProvider);
assertThat(builder.getStreamWatchdogCheckInterval()).isEqualTo(watchdogInterval);
- assertThat(builder.getEnableRoutingCookie()).isEqualTo(enableRoutingCookie);
- assertThat(builder.getEnableRetryInfo()).isEqualTo(enableRetryInfo);
assertThat(builder.getMetricsEndpoint()).isEqualTo(metricsEndpoint);
}
@@ -186,8 +182,6 @@ private void verifySettings(
assertThat(settings.getCredentialsProvider()).isEqualTo(credentialsProvider);
assertThat(settings.getStreamWatchdogProvider()).isSameInstanceAs(watchdogProvider);
assertThat(settings.getStreamWatchdogCheckInterval()).isEqualTo(watchdogInterval);
- assertThat(settings.getEnableRoutingCookie()).isEqualTo(enableRoutingCookie);
- assertThat(settings.getEnableRetryInfo()).isEqualTo(enableRetryInfo);
assertThat(settings.getMetricsEndpoint()).isEqualTo(metricsEndpoint);
}
@@ -920,81 +914,11 @@ public void isRefreshingChannelFalseValueTest() {
assertThat(builder.build().toBuilder().isRefreshingChannel()).isFalse();
}
- @Test
- public void routingCookieIsEnabled() throws IOException {
- String dummyProjectId = "my-project";
- String dummyInstanceId = "my-instance";
- CredentialsProvider credentialsProvider = Mockito.mock(CredentialsProvider.class);
- Mockito.when(credentialsProvider.getCredentials()).thenReturn(new FakeCredentials());
- EnhancedBigtableStubSettings.Builder builder =
- EnhancedBigtableStubSettings.newBuilder()
- .setProjectId(dummyProjectId)
- .setInstanceId(dummyInstanceId)
- .setCredentialsProvider(credentialsProvider);
-
- assertThat(builder.getEnableRoutingCookie()).isTrue();
- assertThat(builder.build().getEnableRoutingCookie()).isTrue();
- assertThat(builder.build().toBuilder().getEnableRoutingCookie()).isTrue();
- }
-
- @Test
- public void enableRetryInfoDefaultValueTest() throws IOException {
- String dummyProjectId = "my-project";
- String dummyInstanceId = "my-instance";
- CredentialsProvider credentialsProvider = Mockito.mock(CredentialsProvider.class);
- Mockito.when(credentialsProvider.getCredentials()).thenReturn(new FakeCredentials());
- EnhancedBigtableStubSettings.Builder builder =
- EnhancedBigtableStubSettings.newBuilder()
- .setProjectId(dummyProjectId)
- .setInstanceId(dummyInstanceId)
- .setCredentialsProvider(credentialsProvider);
- assertThat(builder.getEnableRetryInfo()).isTrue();
- assertThat(builder.build().getEnableRetryInfo()).isTrue();
- assertThat(builder.build().toBuilder().getEnableRetryInfo()).isTrue();
- }
-
- @Test
- public void routingCookieFalseValueSet() throws IOException {
- String dummyProjectId = "my-project";
- String dummyInstanceId = "my-instance";
- CredentialsProvider credentialsProvider = Mockito.mock(CredentialsProvider.class);
- Mockito.when(credentialsProvider.getCredentials()).thenReturn(new FakeCredentials());
- EnhancedBigtableStubSettings.Builder builder =
- EnhancedBigtableStubSettings.newBuilder()
- .setProjectId(dummyProjectId)
- .setInstanceId(dummyInstanceId)
- .setEnableRoutingCookie(false)
- .setCredentialsProvider(credentialsProvider);
- assertThat(builder.getEnableRoutingCookie()).isFalse();
- assertThat(builder.build().getEnableRoutingCookie()).isFalse();
- assertThat(builder.build().toBuilder().getEnableRoutingCookie()).isFalse();
- }
-
- @Test
- public void enableRetryInfoFalseValueTest() throws IOException {
- String dummyProjectId = "my-project";
- String dummyInstanceId = "my-instance";
- CredentialsProvider credentialsProvider = Mockito.mock(CredentialsProvider.class);
- Mockito.when(credentialsProvider.getCredentials()).thenReturn(new FakeCredentials());
- EnhancedBigtableStubSettings.Builder builder =
- EnhancedBigtableStubSettings.newBuilder()
- .setProjectId(dummyProjectId)
- .setInstanceId(dummyInstanceId)
- .setEnableRetryInfo(false)
- .setCredentialsProvider(credentialsProvider);
- assertThat(builder.getEnableRetryInfo()).isFalse();
- assertThat(builder.build().getEnableRetryInfo()).isFalse();
- assertThat(builder.build().toBuilder().getEnableRetryInfo()).isFalse();
- }
-
static final String[] SETTINGS_LIST = {
"projectId",
"instanceId",
"appProfileId",
"isRefreshingChannel",
- "primedTableIds",
- "enableRoutingCookie",
- "enableRetryInfo",
"readRowsSettings",
"readRowSettings",
"sampleRowKeysSettings",
@@ -1025,17 +949,12 @@ public void testToString() {
.build();
checkToString(defaultSettings);
- assertThat(defaultSettings.toString()).contains("primedTableIds=[]");
EnhancedBigtableStubSettings settings =
- defaultSettings.toBuilder()
- .setPrimedTableIds("2", "12", "85", "06")
- .setEndpoint("example.com:1234")
- .build();
+ defaultSettings.toBuilder().setEndpoint("example.com:1234").build();
checkToString(settings);
assertThat(settings.toString()).contains("endpoint=example.com:1234");
- assertThat(settings.toString()).contains("primedTableIds=[2, 12, 85, 06]");
int nonStaticFields = 0;
for (Field field : EnhancedBigtableStubSettings.class.getDeclaredFields()) {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
index fbafe50f47..1531506a11 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
@@ -698,10 +698,7 @@ public void testBulkMutationFlowControllerConfigured() throws Exception {
public void testCallContextPropagatedInMutationBatcher()
throws IOException, InterruptedException, ExecutionException {
EnhancedBigtableStubSettings settings =
- defaultSettings.toBuilder()
- .setRefreshingChannel(true)
- .setPrimedTableIds("table1", "table2")
- .build();
+ defaultSettings.toBuilder().setRefreshingChannel(true).build();
try (EnhancedBigtableStub stub = EnhancedBigtableStub.create(settings)) {
// clear the previous contexts
@@ -728,10 +725,7 @@ public void testCallContextPropagatedInMutationBatcher()
public void testCallContextPropagatedInReadBatcher()
throws IOException, InterruptedException, ExecutionException {
EnhancedBigtableStubSettings settings =
- defaultSettings.toBuilder()
- .setRefreshingChannel(true)
- .setPrimedTableIds("table1", "table2")
- .build();
+ defaultSettings.toBuilder().setRefreshingChannel(true).build();
try (EnhancedBigtableStub stub = EnhancedBigtableStub.create(settings)) {
// clear the previous contexts
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RetryInfoTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RetryInfoTest.java
index ea4b46a713..c206eb20a6 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RetryInfoTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/RetryInfoTest.java
@@ -243,29 +243,11 @@ public void testReadRowNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(() -> client.readRow("table", "row"), false);
}
- @Test
- public void testReadRowDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(() -> newClient.readRow("table", "row"));
- }
- }
-
@Test
public void testReadRowServerNotReturningRetryInfo() {
verifyNoRetryInfo(() -> client.readRow("table", "row"), true);
}
- @Test
- public void testReadRowServerNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(() -> newClient.readRow("table", "row"), true);
- }
- }
-
@Test
public void testReadRowsNonRetraybleErrorWithRetryInfo() {
verifyRetryInfoIsUsed(
@@ -276,19 +258,6 @@ public void testReadRowsNonRetraybleErrorWithRetryInfo() {
false);
}
- @Test
- public void testReadRowsDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored = Lists.newArrayList(newClient.readRows(Query.create("table")));
- });
- }
- }
-
@Test
public void testReadRowsServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -299,20 +268,6 @@ public void testReadRowsServerNotReturningRetryInfo() {
true);
}
- @Test
- public void testReadRowsServerNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored = Lists.newArrayList(newClient.readRows(Query.create("table")));
- },
- true);
- }
- }
-
@Test
public void testMutateRowsNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(
@@ -323,19 +278,6 @@ public void testMutateRowsNonRetryableErrorWithRetryInfo() {
false);
}
- @Test
- public void testMutateRowsDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(
- () ->
- newClient.bulkMutateRows(
- BulkMutation.create("fake-table")
- .add(RowMutationEntry.create("row-key-1").setCell("cf", "q", "v"))));
- }
- }
-
@Test
public void testMutateRowsServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -346,101 +288,28 @@ public void testMutateRowsServerNotReturningRetryInfo() {
true);
}
- @Test
- public void testMutateRowsServerNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () ->
- newClient.bulkMutateRows(
- BulkMutation.create("fake-table")
- .add(RowMutationEntry.create("row-key-1").setCell("cf", "q", "v"))),
- true);
- }
- }
-
@Test
public void testMutateRowNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(
() -> client.mutateRow(RowMutation.create("table", "key").setCell("cf", "q", "v")), false);
}
- @Test
- public void testMutateRowDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
-
- verifyRetryInfoCanBeDisabled(
- () -> newClient.mutateRow(RowMutation.create("table", "key").setCell("cf", "q", "v")));
- }
- }
-
@Test
public void testMutateRowServerNotReturningRetryInfo() {
verifyNoRetryInfo(
() -> client.mutateRow(RowMutation.create("table", "key").setCell("cf", "q", "v")), true);
}
- @Test
- public void testMutateRowServerNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () -> newClient.mutateRow(RowMutation.create("table", "key").setCell("cf", "q", "v")),
- true);
- }
- }
-
@Test
public void testSampleRowKeysNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(() -> client.sampleRowKeys("table"), false);
}
- @Test
- public void testSampleRowKeysDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(() -> newClient.sampleRowKeys("table"));
- }
- }
-
@Test
public void testSampleRowKeysServerNotReturningRetryInfo() {
verifyNoRetryInfo(() -> client.sampleRowKeys("table"), true);
}
- @Test
- public void testSampleRowKeysServerNotReturningRetryInfoClientDisabledHandling()
- throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(() -> newClient.sampleRowKeys("table"), true);
- }
- }
-
- @Test
- public void testCheckAndMutateDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient client = BigtableDataClient.create(settings.build())) {
- ApiException exception = enqueueNonRetryableExceptionWithDelay(defaultDelay);
- try {
- client.checkAndMutateRow(
- ConditionalRowMutation.create("table", "key")
- .condition(Filters.FILTERS.value().regex("old-value"))
- .then(Mutation.create().setCell("cf", "q", "v")));
- } catch (ApiException e) {
- assertThat(e.getStatusCode()).isEqualTo(exception.getStatusCode());
- }
- assertThat(attemptCounter.get()).isEqualTo(1);
- }
- }
-
@Test
public void testCheckAndMutateServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -452,37 +321,6 @@ public void testCheckAndMutateServerNotReturningRetryInfo() {
false);
}
- @Test
- public void testCheckAndMutateServerNotReturningRetryInfoClientDisabledHandling()
- throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () ->
- newClient.checkAndMutateRow(
- ConditionalRowMutation.create("table", "key")
- .condition(Filters.FILTERS.value().regex("old-value"))
- .then(Mutation.create().setCell("cf", "q", "v"))),
- false);
- }
- }
-
- @Test
- public void testReadModifyWriteDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient client = BigtableDataClient.create(settings.build())) {
- ApiException exception = enqueueNonRetryableExceptionWithDelay(defaultDelay);
- try {
- client.readModifyWriteRow(ReadModifyWriteRow.create("table", "row").append("cf", "q", "v"));
- } catch (ApiException e) {
- assertThat(e.getStatusCode()).isEqualTo(exception.getStatusCode());
- }
- assertThat(attemptCounter.get()).isEqualTo(1);
- }
- }
-
@Test
public void testReadModifyWriteServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -492,19 +330,6 @@ public void testReadModifyWriteServerNotReturningRetryInfo() {
false);
}
- @Test
- public void testReadModifyWriteNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () ->
- newClient.readModifyWriteRow(
- ReadModifyWriteRow.create("table", "row").append("cf", "q", "v")),
- false);
- }
- }
-
@Test
public void testReadChangeStreamNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(
@@ -516,21 +341,6 @@ public void testReadChangeStreamNonRetryableErrorWithRetryInfo() {
false);
}
- @Test
- public void testReadChangeStreamDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored =
- Lists.newArrayList(
- newClient.readChangeStream(ReadChangeStreamQuery.create("table")));
- });
- }
- }
-
@Test
public void testReadChangeStreamServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -542,23 +352,6 @@ public void testReadChangeStreamServerNotReturningRetryInfo() {
true);
}
- @Test
- public void testReadChangeStreamNotReturningRetryInfoClientDisabledHandling() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored =
- Lists.newArrayList(
- newClient.readChangeStream(ReadChangeStreamQuery.create("table")));
- },
- true,
- com.google.protobuf.Duration.newBuilder().setSeconds(5).setNanos(0).build());
- }
- }
-
@Test
public void testGenerateInitialChangeStreamPartitionNonRetryableError() {
verifyRetryInfoIsUsed(
@@ -570,20 +363,6 @@ public void testGenerateInitialChangeStreamPartitionNonRetryableError() {
false);
}
- @Test
- public void testGenerateInitialChangeStreamPartitionDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyRetryInfoCanBeDisabled(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored =
- Lists.newArrayList(newClient.generateInitialChangeStreamPartitions("table"));
- });
- }
- }
-
@Test
public void testGenerateInitialChangeStreamServerNotReturningRetryInfo() {
verifyNoRetryInfo(
@@ -595,55 +374,17 @@ public void testGenerateInitialChangeStreamServerNotReturningRetryInfo() {
true);
}
- @Test
- public void testGenerateInitialChangeStreamServerNotReturningRetryInfoClientDisabledHandling()
- throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () -> {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList ignored =
- Lists.newArrayList(newClient.generateInitialChangeStreamPartitions("table"));
- },
- true);
- }
- }
-
@Test
public void testPrepareQueryNonRetryableErrorWithRetryInfo() {
verifyRetryInfoIsUsed(
() -> client.prepareStatement("SELECT * FROM table", new HashMap<>()), false);
}
- @Test
- public void testPrepareQueryDisableRetryInfo() throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
-
- verifyRetryInfoCanBeDisabled(
- () -> newClient.prepareStatement("SELECT * FROM table", new HashMap<>()));
- }
- }
-
@Test
public void testPrepareQueryServerNotReturningRetryInfo() {
verifyNoRetryInfo(() -> client.prepareStatement("SELECT * FROM table", new HashMap<>()), true);
}
- @Test
- public void testPrepareQueryServerNotReturningRetryInfoClientDisabledHandling()
- throws IOException {
- settings.stubSettings().setEnableRetryInfo(false);
-
- try (BigtableDataClient newClient = BigtableDataClient.create(settings.build())) {
- verifyNoRetryInfo(
- () -> newClient.prepareStatement("SELECT * FROM table", new HashMap<>()), true);
- }
- }
-
// Test the case where server returns retry info and client enables handling of retry info
private void verifyRetryInfoIsUsed(Runnable runnable, boolean retryableError) {
if (retryableError) {
From 99b14129bff404ef396a12df0d332cd4f6021dd2 Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Mon, 23 Feb 2026 15:44:27 -0500
Subject: [PATCH 09/33] chore: factor out per operation settings into a
separate class (#2796)
Currently Stubs get access to both BigtableClientContext and EnhancedStubSettings. And since BigtableClientContext is derived from EnhancedStubSettings there is quite a bit of overlap settings which makes it hard to keep them consistent. This PR builds towards fixing this by removing access to EnhancedStubSettings and replacing it with disjoint derivative classes.
This PR moves all of the per-op settings into a separate class. The next step will be to copy the remaining bits to BigtableClientContext and have EnhancedBigtableStub depend solely on BigtableClientContext and ClientOperationSettings.
---
.../data/v2/stub/ClientOperationSettings.java | 404 ++++++++++++++++++
.../v2/stub/EnhancedBigtableStubSettings.java | 391 ++---------------
.../EnhancedBigtableStubSettingsTest.java | 14 +-
3 files changed, 438 insertions(+), 371 deletions(-)
create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ClientOperationSettings.java
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ClientOperationSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ClientOperationSettings.java
new file mode 100644
index 0000000000..8252b4b22a
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ClientOperationSettings.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.stub;
+
+import com.google.api.gax.batching.BatchingSettings;
+import com.google.api.gax.batching.FlowControlSettings;
+import com.google.api.gax.batching.FlowController;
+import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.rpc.ServerStreamingCallSettings;
+import com.google.api.gax.rpc.StatusCode;
+import com.google.api.gax.rpc.UnaryCallSettings;
+import com.google.bigtable.v2.PingAndWarmRequest;
+import com.google.cloud.bigtable.data.v2.internal.PrepareQueryRequest;
+import com.google.cloud.bigtable.data.v2.internal.PrepareResponse;
+import com.google.cloud.bigtable.data.v2.internal.SqlRow;
+import com.google.cloud.bigtable.data.v2.models.ChangeStreamRecord;
+import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation;
+import com.google.cloud.bigtable.data.v2.models.KeyOffset;
+import com.google.cloud.bigtable.data.v2.models.Query;
+import com.google.cloud.bigtable.data.v2.models.Range;
+import com.google.cloud.bigtable.data.v2.models.ReadChangeStreamQuery;
+import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow;
+import com.google.cloud.bigtable.data.v2.models.Row;
+import com.google.cloud.bigtable.data.v2.models.RowMutation;
+import com.google.cloud.bigtable.data.v2.models.sql.BoundStatement;
+import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsBatchingDescriptor;
+import com.google.cloud.bigtable.data.v2.stub.readrows.ReadRowsBatchingDescriptor;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.Set;
+import org.threeten.bp.Duration;
+
+class ClientOperationSettings {
+ private static final Set IDEMPOTENT_RETRY_CODES =
+ ImmutableSet.of(StatusCode.Code.DEADLINE_EXCEEDED, StatusCode.Code.UNAVAILABLE);
+
+ // Copy of default retrying settings in the yaml
+ private static final RetrySettings IDEMPOTENT_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setInitialRpcTimeout(Duration.ofSeconds(20))
+ .setRpcTimeoutMultiplier(1.0)
+ .setMaxRpcTimeout(Duration.ofSeconds(20))
+ .setTotalTimeout(Duration.ofMinutes(10))
+ .build();
+
+ // Allow retrying ABORTED statuses. These will be returned by the server when the client is
+ // too slow to read the rows. This makes sense for the java client because retries happen
+ // after the row merging logic. Which means that the retry will not be invoked until the
+ // current buffered chunks are consumed.
+ private static final Set READ_ROWS_RETRY_CODES =
+ ImmutableSet.builder()
+ .addAll(IDEMPOTENT_RETRY_CODES)
+ .add(StatusCode.Code.ABORTED)
+ .build();
+
+ // Priming request should have a shorter timeout
+ private static final Duration PRIME_REQUEST_TIMEOUT = Duration.ofSeconds(30);
+
+ private static final RetrySettings READ_ROWS_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2.0)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setMaxAttempts(10)
+ .setInitialRpcTimeout(Duration.ofMinutes(30))
+ .setRpcTimeoutMultiplier(2.0)
+ .setMaxRpcTimeout(Duration.ofMinutes(30))
+ .setTotalTimeout(Duration.ofHours(12))
+ .build();
+
+ private static final RetrySettings MUTATE_ROWS_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setInitialRpcTimeout(Duration.ofMinutes(1))
+ .setRpcTimeoutMultiplier(1.0)
+ .setMaxRpcTimeout(Duration.ofMinutes(1))
+ .setTotalTimeout(Duration.ofMinutes(10))
+ .build();
+
+ private static final Set GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_CODES =
+ ImmutableSet.builder()
+ .addAll(IDEMPOTENT_RETRY_CODES)
+ .add(StatusCode.Code.ABORTED)
+ .build();
+
+ private static final RetrySettings GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2.0)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setMaxAttempts(10)
+ .setInitialRpcTimeout(Duration.ofMinutes(1))
+ .setRpcTimeoutMultiplier(2.0)
+ .setMaxRpcTimeout(Duration.ofMinutes(10))
+ .setTotalTimeout(Duration.ofMinutes(60))
+ .build();
+
+ // Allow retrying ABORTED statuses. These will be returned by the server when the client is
+ // too slow to read the change stream records. This makes sense for the java client because
+ // retries happen after the mutation merging logic. Which means that the retry will not be
+ // invoked until the current buffered change stream mutations are consumed.
+ private static final Set READ_CHANGE_STREAM_RETRY_CODES =
+ ImmutableSet.builder()
+ .addAll(IDEMPOTENT_RETRY_CODES)
+ .add(StatusCode.Code.ABORTED)
+ .build();
+
+ private static final RetrySettings READ_CHANGE_STREAM_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2.0)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setMaxAttempts(10)
+ .setInitialRpcTimeout(Duration.ofMinutes(5))
+ .setRpcTimeoutMultiplier(2.0)
+ .setMaxRpcTimeout(Duration.ofMinutes(5))
+ .setTotalTimeout(Duration.ofHours(12))
+ .build();
+
+ // Allow retrying ABORTED statuses. These will be returned by the server when the client is
+ // too slow to read the responses.
+ private static final Set EXECUTE_QUERY_RETRY_CODES =
+ ImmutableSet.builder()
+ .addAll(IDEMPOTENT_RETRY_CODES)
+ .add(StatusCode.Code.ABORTED)
+ .build();
+
+ // We use the same configuration as READ_ROWS
+ private static final RetrySettings EXECUTE_QUERY_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2.0)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setMaxAttempts(10)
+ .setInitialRpcTimeout(Duration.ofMinutes(30))
+ .setRpcTimeoutMultiplier(1.0)
+ .setMaxRpcTimeout(Duration.ofMinutes(30))
+ .setTotalTimeout(Duration.ofHours(12))
+ .build();
+
+ // Similar to IDEMPOTENT but with a lower initial rpc timeout since we expect
+ // these calls to be quick in most circumstances
+ private static final RetrySettings PREPARE_QUERY_RETRY_SETTINGS =
+ RetrySettings.newBuilder()
+ .setInitialRetryDelay(Duration.ofMillis(10))
+ .setRetryDelayMultiplier(2)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ // TODO: fix the settings: initial attempt deadline: 5s, max is 20s but multiplier is 1
+ .setInitialRpcTimeout(Duration.ofSeconds(5))
+ .setRpcTimeoutMultiplier(1.0)
+ .setMaxRpcTimeout(Duration.ofSeconds(20))
+ .setTotalTimeout(Duration.ofMinutes(10))
+ .build();
+
+ final ServerStreamingCallSettings readRowsSettings;
+ final UnaryCallSettings readRowSettings;
+ final UnaryCallSettings> sampleRowKeysSettings;
+ final UnaryCallSettings mutateRowSettings;
+ final BigtableBatchingCallSettings bulkMutateRowsSettings;
+ final BigtableBulkReadRowsCallSettings bulkReadRowsSettings;
+ final UnaryCallSettings checkAndMutateRowSettings;
+ final UnaryCallSettings readModifyWriteRowSettings;
+ final ServerStreamingCallSettings
+ generateInitialChangeStreamPartitionsSettings;
+ final ServerStreamingCallSettings
+ readChangeStreamSettings;
+ final UnaryCallSettings pingAndWarmSettings;
+ final ServerStreamingCallSettings executeQuerySettings;
+ final UnaryCallSettings prepareQuerySettings;
+
+ ClientOperationSettings(Builder builder) {
+ // Since point reads, streaming reads, bulk reads share the same base callable that converts
+ // grpc errors into ApiExceptions, they must have the same retry codes.
+ Preconditions.checkState(
+ builder
+ .readRowSettings
+ .getRetryableCodes()
+ .equals(builder.readRowsSettings.getRetryableCodes()),
+ "Single ReadRow retry codes must match ReadRows retry codes");
+ Preconditions.checkState(
+ builder
+ .bulkReadRowsSettings
+ .getRetryableCodes()
+ .equals(builder.readRowsSettings.getRetryableCodes()),
+ "Bulk ReadRow retry codes must match ReadRows retry codes");
+
+ // Per method settings.
+ readRowsSettings = builder.readRowsSettings.build();
+ readRowSettings = builder.readRowSettings.build();
+ sampleRowKeysSettings = builder.sampleRowKeysSettings.build();
+ mutateRowSettings = builder.mutateRowSettings.build();
+ bulkMutateRowsSettings = builder.bulkMutateRowsSettings.build();
+ bulkReadRowsSettings = builder.bulkReadRowsSettings.build();
+ checkAndMutateRowSettings = builder.checkAndMutateRowSettings.build();
+ readModifyWriteRowSettings = builder.readModifyWriteRowSettings.build();
+ generateInitialChangeStreamPartitionsSettings =
+ builder.generateInitialChangeStreamPartitionsSettings.build();
+ readChangeStreamSettings = builder.readChangeStreamSettings.build();
+ pingAndWarmSettings = builder.pingAndWarmSettings.build();
+ executeQuerySettings = builder.executeQuerySettings.build();
+ prepareQuerySettings = builder.prepareQuerySettings.build();
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("readRowsSettings", readRowsSettings)
+ .add("readRowSettings", readRowSettings)
+ .add("sampleRowKeysSettings", sampleRowKeysSettings)
+ .add("mutateRowSettings", mutateRowSettings)
+ .add("bulkMutateRowsSettings", bulkMutateRowsSettings)
+ .add("bulkReadRowsSettings", bulkReadRowsSettings)
+ .add("checkAndMutateRowSettings", checkAndMutateRowSettings)
+ .add("readModifyWriteRowSettings", readModifyWriteRowSettings)
+ .add(
+ "generateInitialChangeStreamPartitionsSettings",
+ generateInitialChangeStreamPartitionsSettings)
+ .add("readChangeStreamSettings", readChangeStreamSettings)
+ .add("pingAndWarmSettings", pingAndWarmSettings)
+ .add("executeQuerySettings", executeQuerySettings)
+ .add("prepareQuerySettings", prepareQuerySettings)
+ .toString();
+ }
+
+ static class Builder {
+ ServerStreamingCallSettings.Builder readRowsSettings;
+ UnaryCallSettings.Builder readRowSettings;
+ UnaryCallSettings.Builder> sampleRowKeysSettings;
+ UnaryCallSettings.Builder mutateRowSettings;
+ BigtableBatchingCallSettings.Builder bulkMutateRowsSettings;
+ BigtableBulkReadRowsCallSettings.Builder bulkReadRowsSettings;
+ UnaryCallSettings.Builder checkAndMutateRowSettings;
+ UnaryCallSettings.Builder readModifyWriteRowSettings;
+ ServerStreamingCallSettings.Builder
+ generateInitialChangeStreamPartitionsSettings;
+ ServerStreamingCallSettings.Builder
+ readChangeStreamSettings;
+ UnaryCallSettings.Builder pingAndWarmSettings;
+ ServerStreamingCallSettings.Builder executeQuerySettings;
+ UnaryCallSettings.Builder prepareQuerySettings;
+
+ Builder() {
+ BigtableStubSettings.Builder baseDefaults = BigtableStubSettings.newBuilder();
+
+ readRowsSettings = ServerStreamingCallSettings.newBuilder();
+
+ readRowsSettings
+ .setRetryableCodes(READ_ROWS_RETRY_CODES)
+ .setRetrySettings(READ_ROWS_RETRY_SETTINGS)
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setWaitTimeout(Duration.ofMinutes(5));
+
+ readRowSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ readRowSettings
+ .setRetryableCodes(readRowsSettings.getRetryableCodes())
+ .setRetrySettings(IDEMPOTENT_RETRY_SETTINGS);
+
+ sampleRowKeysSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ sampleRowKeysSettings
+ .setRetryableCodes(IDEMPOTENT_RETRY_CODES)
+ .setRetrySettings(
+ IDEMPOTENT_RETRY_SETTINGS.toBuilder()
+ .setInitialRpcTimeout(Duration.ofMinutes(5))
+ .setMaxRpcTimeout(Duration.ofMinutes(5))
+ .build());
+
+ mutateRowSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ copyRetrySettings(baseDefaults.mutateRowSettings(), mutateRowSettings);
+
+ long maxBulkMutateElementPerBatch = 100L;
+ long maxBulkMutateOutstandingElementCount = 20_000L;
+
+ bulkMutateRowsSettings =
+ BigtableBatchingCallSettings.newBuilder(new MutateRowsBatchingDescriptor())
+ .setRetryableCodes(IDEMPOTENT_RETRY_CODES)
+ .setRetrySettings(MUTATE_ROWS_RETRY_SETTINGS)
+ .setBatchingSettings(
+ BatchingSettings.newBuilder()
+ .setIsEnabled(true)
+ .setElementCountThreshold(maxBulkMutateElementPerBatch)
+ .setRequestByteThreshold(20L * 1024 * 1024)
+ .setDelayThreshold(Duration.ofSeconds(1))
+ .setFlowControlSettings(
+ FlowControlSettings.newBuilder()
+ .setLimitExceededBehavior(FlowController.LimitExceededBehavior.Block)
+ .setMaxOutstandingRequestBytes(100L * 1024 * 1024)
+ .setMaxOutstandingElementCount(maxBulkMutateOutstandingElementCount)
+ .build())
+ .build());
+
+ long maxBulkReadElementPerBatch = 100L;
+ long maxBulkReadRequestSizePerBatch = 400L * 1024L;
+ long maxBulkReadOutstandingElementCount = 20_000L;
+
+ bulkReadRowsSettings =
+ BigtableBulkReadRowsCallSettings.newBuilder(new ReadRowsBatchingDescriptor())
+ .setRetryableCodes(readRowsSettings.getRetryableCodes())
+ .setRetrySettings(IDEMPOTENT_RETRY_SETTINGS)
+ .setBatchingSettings(
+ BatchingSettings.newBuilder()
+ .setElementCountThreshold(maxBulkReadElementPerBatch)
+ .setRequestByteThreshold(maxBulkReadRequestSizePerBatch)
+ .setDelayThreshold(Duration.ofSeconds(1))
+ .setFlowControlSettings(
+ FlowControlSettings.newBuilder()
+ .setLimitExceededBehavior(FlowController.LimitExceededBehavior.Block)
+ .setMaxOutstandingElementCount(maxBulkReadOutstandingElementCount)
+ .build())
+ .build());
+
+ checkAndMutateRowSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ copyRetrySettings(baseDefaults.checkAndMutateRowSettings(), checkAndMutateRowSettings);
+
+ readModifyWriteRowSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ copyRetrySettings(baseDefaults.readModifyWriteRowSettings(), readModifyWriteRowSettings);
+
+ generateInitialChangeStreamPartitionsSettings = ServerStreamingCallSettings.newBuilder();
+ generateInitialChangeStreamPartitionsSettings
+ .setRetryableCodes(GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_CODES)
+ .setRetrySettings(GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_SETTINGS)
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setWaitTimeout(Duration.ofMinutes(1));
+
+ readChangeStreamSettings = ServerStreamingCallSettings.newBuilder();
+ readChangeStreamSettings
+ .setRetryableCodes(READ_CHANGE_STREAM_RETRY_CODES)
+ .setRetrySettings(READ_CHANGE_STREAM_RETRY_SETTINGS)
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setWaitTimeout(Duration.ofMinutes(1));
+
+ pingAndWarmSettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ pingAndWarmSettings.setRetrySettings(
+ RetrySettings.newBuilder()
+ .setMaxAttempts(1)
+ .setInitialRpcTimeout(PRIME_REQUEST_TIMEOUT)
+ .setMaxRpcTimeout(PRIME_REQUEST_TIMEOUT)
+ .setTotalTimeout(PRIME_REQUEST_TIMEOUT)
+ .build());
+
+ executeQuerySettings = ServerStreamingCallSettings.newBuilder();
+ executeQuerySettings
+ .setRetryableCodes(EXECUTE_QUERY_RETRY_CODES)
+ .setRetrySettings(EXECUTE_QUERY_RETRY_SETTINGS)
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setWaitTimeout(Duration.ofMinutes(5));
+
+ prepareQuerySettings = UnaryCallSettings.newUnaryCallSettingsBuilder();
+ prepareQuerySettings
+ .setRetryableCodes(IDEMPOTENT_RETRY_CODES)
+ .setRetrySettings(PREPARE_QUERY_RETRY_SETTINGS);
+ }
+
+ Builder(ClientOperationSettings settings) {
+ readRowsSettings = settings.readRowsSettings.toBuilder();
+ readRowSettings = settings.readRowSettings.toBuilder();
+ sampleRowKeysSettings = settings.sampleRowKeysSettings.toBuilder();
+ mutateRowSettings = settings.mutateRowSettings.toBuilder();
+ bulkMutateRowsSettings = settings.bulkMutateRowsSettings.toBuilder();
+ bulkReadRowsSettings = settings.bulkReadRowsSettings.toBuilder();
+ checkAndMutateRowSettings = settings.checkAndMutateRowSettings.toBuilder();
+ readModifyWriteRowSettings = settings.readModifyWriteRowSettings.toBuilder();
+ generateInitialChangeStreamPartitionsSettings =
+ settings.generateInitialChangeStreamPartitionsSettings.toBuilder();
+ readChangeStreamSettings = settings.readChangeStreamSettings.toBuilder();
+ pingAndWarmSettings = settings.pingAndWarmSettings.toBuilder();
+ executeQuerySettings = settings.executeQuerySettings.toBuilder();
+ prepareQuerySettings = settings.prepareQuerySettings.toBuilder();
+ }
+
+ /**
+ * Copies settings from unary RPC to another. This is necessary when modifying request and
+ * response types while trying to retain retry settings.
+ */
+ private static void copyRetrySettings(
+ UnaryCallSettings.Builder, ?> source, UnaryCallSettings.Builder, ?> dest) {
+ dest.setRetryableCodes(source.getRetryableCodes());
+ dest.setRetrySettings(source.getRetrySettings());
+ }
+
+ ClientOperationSettings build() {
+ return new ClientOperationSettings(this);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
index 003823f5fc..0ce0c7b299 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
@@ -21,7 +21,6 @@
import com.google.api.gax.batching.BatchingSettings;
import com.google.api.gax.batching.FlowControlSettings;
import com.google.api.gax.batching.FlowController;
-import com.google.api.gax.batching.FlowController.LimitExceededBehavior;
import com.google.api.gax.core.GoogleCredentialsProvider;
import com.google.api.gax.grpc.ChannelPoolSettings;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
@@ -50,13 +49,10 @@
import com.google.cloud.bigtable.data.v2.models.sql.BoundStatement;
import com.google.cloud.bigtable.data.v2.stub.metrics.DefaultMetricsProvider;
import com.google.cloud.bigtable.data.v2.stub.metrics.MetricsProvider;
-import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsBatchingDescriptor;
-import com.google.cloud.bigtable.data.v2.stub.readrows.ReadRowsBatchingDescriptor;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -64,7 +60,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.threeten.bp.Duration;
@@ -110,120 +105,6 @@ public class EnhancedBigtableStubSettings extends StubSettings IDEMPOTENT_RETRY_CODES =
- ImmutableSet.of(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE);
-
- // Copy of default retrying settings in the yaml
- private static final RetrySettings IDEMPOTENT_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setInitialRpcTimeout(Duration.ofSeconds(20))
- .setRpcTimeoutMultiplier(1.0)
- .setMaxRpcTimeout(Duration.ofSeconds(20))
- .setTotalTimeout(Duration.ofMinutes(10))
- .build();
-
- // Allow retrying ABORTED statuses. These will be returned by the server when the client is
- // too slow to read the rows. This makes sense for the java client because retries happen
- // after the row merging logic. Which means that the retry will not be invoked until the
- // current buffered chunks are consumed.
- private static final Set READ_ROWS_RETRY_CODES =
- ImmutableSet.builder().addAll(IDEMPOTENT_RETRY_CODES).add(Code.ABORTED).build();
-
- // Priming request should have a shorter timeout
- private static Duration PRIME_REQUEST_TIMEOUT = Duration.ofSeconds(30);
-
- private static final RetrySettings READ_ROWS_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2.0)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setMaxAttempts(10)
- .setInitialRpcTimeout(Duration.ofMinutes(30))
- .setRpcTimeoutMultiplier(2.0)
- .setMaxRpcTimeout(Duration.ofMinutes(30))
- .setTotalTimeout(Duration.ofHours(12))
- .build();
-
- private static final RetrySettings MUTATE_ROWS_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setInitialRpcTimeout(Duration.ofMinutes(1))
- .setRpcTimeoutMultiplier(1.0)
- .setMaxRpcTimeout(Duration.ofMinutes(1))
- .setTotalTimeout(Duration.ofMinutes(10))
- .build();
-
- private static final Set GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_CODES =
- ImmutableSet.builder().addAll(IDEMPOTENT_RETRY_CODES).add(Code.ABORTED).build();
-
- private static final RetrySettings GENERATE_INITIAL_CHANGE_STREAM_PARTITIONS_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2.0)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setMaxAttempts(10)
- .setInitialRpcTimeout(Duration.ofMinutes(1))
- .setRpcTimeoutMultiplier(2.0)
- .setMaxRpcTimeout(Duration.ofMinutes(10))
- .setTotalTimeout(Duration.ofMinutes(60))
- .build();
-
- // Allow retrying ABORTED statuses. These will be returned by the server when the client is
- // too slow to read the change stream records. This makes sense for the java client because
- // retries happen after the mutation merging logic. Which means that the retry will not be
- // invoked until the current buffered change stream mutations are consumed.
- private static final Set READ_CHANGE_STREAM_RETRY_CODES =
- ImmutableSet.builder().addAll(IDEMPOTENT_RETRY_CODES).add(Code.ABORTED).build();
-
- private static final RetrySettings READ_CHANGE_STREAM_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2.0)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setMaxAttempts(10)
- .setJittered(true)
- .setInitialRpcTimeout(Duration.ofMinutes(5))
- .setRpcTimeoutMultiplier(2.0)
- .setMaxRpcTimeout(Duration.ofMinutes(5))
- .setTotalTimeout(Duration.ofHours(12))
- .build();
-
- // Allow retrying ABORTED statuses. These will be returned by the server when the client is
- // too slow to read the responses.
- private static final Set EXECUTE_QUERY_RETRY_CODES =
- ImmutableSet.builder().addAll(IDEMPOTENT_RETRY_CODES).add(Code.ABORTED).build();
-
- // We use the same configuration as READ_ROWS
- private static final RetrySettings EXECUTE_QUERY_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2.0)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setMaxAttempts(10)
- .setInitialRpcTimeout(Duration.ofMinutes(30))
- .setRpcTimeoutMultiplier(1.0)
- .setMaxRpcTimeout(Duration.ofMinutes(30))
- .setTotalTimeout(Duration.ofHours(12))
- .build();
-
- // Similar to IDEMPOTENT but with a lower initial rpc timeout since we expect
- // these calls to be quick in most circumstances
- private static final RetrySettings PREPARE_QUERY_RETRY_SETTINGS =
- RetrySettings.newBuilder()
- .setInitialRetryDelay(Duration.ofMillis(10))
- .setRetryDelayMultiplier(2)
- .setMaxRetryDelay(Duration.ofMinutes(1))
- .setInitialRpcTimeout(Duration.ofSeconds(5))
- .setRpcTimeoutMultiplier(1.0)
- .setMaxRpcTimeout(Duration.ofSeconds(20))
- .setTotalTimeout(Duration.ofMinutes(10))
- .build();
-
/**
* Scopes that are equivalent to JWT's audience.
*
@@ -249,21 +130,7 @@ public class EnhancedBigtableStubSettings extends StubSettings readRowsSettings;
- private final UnaryCallSettings readRowSettings;
- private final UnaryCallSettings> sampleRowKeysSettings;
- private final UnaryCallSettings mutateRowSettings;
- private final BigtableBatchingCallSettings bulkMutateRowsSettings;
- private final BigtableBulkReadRowsCallSettings bulkReadRowsSettings;
- private final UnaryCallSettings checkAndMutateRowSettings;
- private final UnaryCallSettings readModifyWriteRowSettings;
- private final ServerStreamingCallSettings
- generateInitialChangeStreamPartitionsSettings;
- private final ServerStreamingCallSettings
- readChangeStreamSettings;
- private final UnaryCallSettings pingAndWarmSettings;
- private final ServerStreamingCallSettings executeQuerySettings;
- private final UnaryCallSettings prepareQuerySettings;
+ private final ClientOperationSettings perOpSettings;
private final FeatureFlags featureFlags;
@@ -275,21 +142,6 @@ public class EnhancedBigtableStubSettings extends StubSettings
*/
public ServerStreamingCallSettings readRowsSettings() {
- return readRowsSettings;
+ return perOpSettings.readRowsSettings;
}
/**
@@ -500,7 +338,7 @@ public ServerStreamingCallSettings readRowsSettings() {
*
*/
public UnaryCallSettings> sampleRowKeysSettings() {
- return sampleRowKeysSettings;
+ return perOpSettings.sampleRowKeysSettings;
}
/**
@@ -525,7 +363,7 @@ public UnaryCallSettings> sampleRowKeysSettings() {
* @see RetrySettings for more explanation.
*/
public UnaryCallSettings readRowSettings() {
- return readRowSettings;
+ return perOpSettings.readRowSettings;
}
/**
@@ -550,7 +388,7 @@ public UnaryCallSettings readRowSettings() {
* @see RetrySettings for more explanation.
*/
public UnaryCallSettings mutateRowSettings() {
- return mutateRowSettings;
+ return perOpSettings.mutateRowSettings;
}
/**
@@ -597,7 +435,7 @@ public UnaryCallSettings mutateRowSettings() {
* related configuration explanation.
*/
public BigtableBatchingCallSettings bulkMutateRowsSettings() {
- return bulkMutateRowsSettings;
+ return perOpSettings.bulkMutateRowsSettings;
}
/**
@@ -638,7 +476,7 @@ public BigtableBatchingCallSettings bulkMutateRowsSettings() {
* @see BatchingSettings for batch related configuration explanation.
*/
public BigtableBulkReadRowsCallSettings bulkReadRowsSettings() {
- return bulkReadRowsSettings;
+ return perOpSettings.bulkReadRowsSettings;
}
/**
@@ -652,7 +490,7 @@ public BigtableBulkReadRowsCallSettings bulkReadRowsSettings() {
* @see RetrySettings for more explanation.
*/
public UnaryCallSettings checkAndMutateRowSettings() {
- return checkAndMutateRowSettings;
+ return perOpSettings.checkAndMutateRowSettings;
}
/**
@@ -666,21 +504,21 @@ public UnaryCallSettings checkAndMutateRowSetti
* @see RetrySettings for more explanation.
*/
public UnaryCallSettings readModifyWriteRowSettings() {
- return readModifyWriteRowSettings;
+ return perOpSettings.readModifyWriteRowSettings;
}
public ServerStreamingCallSettings
generateInitialChangeStreamPartitionsSettings() {
- return generateInitialChangeStreamPartitionsSettings;
+ return perOpSettings.generateInitialChangeStreamPartitionsSettings;
}
public ServerStreamingCallSettings
readChangeStreamSettings() {
- return readChangeStreamSettings;
+ return perOpSettings.readChangeStreamSettings;
}
public ServerStreamingCallSettings executeQuerySettings() {
- return executeQuerySettings;
+ return perOpSettings.executeQuerySettings;
}
/**
@@ -706,7 +544,7 @@ public ServerStreamingCallSettings executeQuerySettings(
* @see RetrySettings for more explanation.
*/
public UnaryCallSettings prepareQuerySettings() {
- return prepareQuerySettings;
+ return perOpSettings.prepareQuerySettings;
}
/**
@@ -715,7 +553,7 @@ public UnaryCallSettings prepareQuerySetti
*
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+ org.mockitomockito-coretest
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+ com.google.guavaguava-testlib
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java
new file mode 100644
index 0000000000..f485e79e4f
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlFactor;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlTargetQps;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientChannelPoolOutstandingRpcs;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientDpCompatGuage;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientPerConnectionErrorCount;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.GrpcMetric;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.MetricWrapper;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.PacemakerDelay;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableApplicationBlockingLatency;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency2;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableClientBlockingLatency;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableConnectivityErrorCount;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableDebugTagCount;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableFirstResponseLatency;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableOperationLatency;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRemainingDeadline;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRetryCount;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableServerLatency;
+import com.google.common.collect.ImmutableList;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.metrics.MeterProvider;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Repository for all client metrics. This class has 2 audiences:
+ *
+ *
+ *
VRpcTracer, which reference each metric directly
+ *
Exporter, which will look up each metric by name and use the {@link MetricWrapper}
+ * interface to augment the {@code MonitoredResource} and {@code Metric Labels}
+ *
+ */
+public class MetricRegistry {
+ static final String METER_NAME = "bigtable.googleapis.com/internal/client/";
+
+ final TableOperationLatency operationLatencyMetric;
+ final TableAttemptLatency attemptLatencyMetric;
+ final TableAttemptLatency2 attemptLatency2Metric;
+ final TableRetryCount retryCountMetric;
+ final TableFirstResponseLatency firstResponseLantencyMetric;
+ final TableServerLatency serverLatencyMetric;
+ final ClientChannelPoolOutstandingRpcs channelPoolOutstandingRpcsMetric;
+ final TableConnectivityErrorCount connectivityErrorCountMetric;
+ final ClientDpCompatGuage dpCompatGuageMetric;
+ final TableApplicationBlockingLatency applicationBlockingLatencyMetric;
+ final TableClientBlockingLatency clientBlockingLatencyMetric;
+ final ClientPerConnectionErrorCount perConnectionErrorCountMetric;
+ final TableRemainingDeadline remainingDeadlineMetric;
+ final ClientBatchWriteFlowControlFactor batchWriteFlowControlFactorMetric;
+ final ClientBatchWriteFlowControlTargetQps batchWriteFlowControlTargetQpsMetric;
+
+ final TableDebugTagCount debugTagCountMetric;
+ final PacemakerDelay pacemakerDelayMetric;
+
+ private final Map> metrics = new HashMap<>();
+ private final List grpcMetricNames = new ArrayList<>();
+
+ public MetricRegistry() {
+ operationLatencyMetric = register(new TableOperationLatency());
+ attemptLatencyMetric = register(new TableAttemptLatency());
+ attemptLatency2Metric = register(new TableAttemptLatency2());
+ retryCountMetric = register(new TableRetryCount());
+ firstResponseLantencyMetric = register(new TableFirstResponseLatency());
+ serverLatencyMetric = register(new TableServerLatency());
+ channelPoolOutstandingRpcsMetric = register(new ClientChannelPoolOutstandingRpcs());
+ connectivityErrorCountMetric = register(new TableConnectivityErrorCount());
+ applicationBlockingLatencyMetric = register(new TableApplicationBlockingLatency());
+ clientBlockingLatencyMetric = register(new TableClientBlockingLatency());
+ perConnectionErrorCountMetric = register(new ClientPerConnectionErrorCount());
+ dpCompatGuageMetric = register(new ClientDpCompatGuage());
+ remainingDeadlineMetric = register(new TableRemainingDeadline());
+ batchWriteFlowControlFactorMetric = register(new ClientBatchWriteFlowControlFactor());
+ batchWriteFlowControlTargetQpsMetric = register(new ClientBatchWriteFlowControlTargetQps());
+
+ debugTagCountMetric = register(new TableDebugTagCount());
+ pacemakerDelayMetric = register(new PacemakerDelay());
+
+ // From
+ // https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java#L138-L165
+ registerGrpcMetric(
+ "grpc.client.attempt.duration",
+ ImmutableList.of("grpc.lb.locality", "grpc.status", "grpc.method", "grpc.target"));
+ registerGrpcMetric(
+ "grpc.lb.rls.default_target_picks",
+ ImmutableList.of(
+ "grpc.target",
+ "grpc.lb.rls.server_target",
+ "grpc.lb.rls.data_plane_target",
+ "grpc.lb.pick_result"));
+ registerGrpcMetric(
+ "grpc.lb.rls.target_picks",
+ ImmutableList.of(
+ "grpc.target",
+ "grpc.lb.rls.server_target",
+ "grpc.lb.rls.data_plane_target",
+ "grpc.lb.pick_result"));
+ registerGrpcMetric(
+ "grpc.lb.rls.failed_picks", ImmutableList.of("grpc.target", "grpc.lb.rls.server_target"));
+
+ // From
+ // https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java#L67-L94
+ // TODO: "grpc.xds_client.connected"
+ registerGrpcMetric(
+ "grpc.xds_client.server_failure", ImmutableList.of("grpc.target", "grpc.xds.server"));
+ // TODO: "grpc.xds_client.resource_updates_valid",
+ registerGrpcMetric(
+ "grpc.xds_client.resource_updates_invalid",
+ ImmutableList.of("grpc.target", "grpc.xds.server", "grpc.xds.resource_type"));
+ // TODO: "grpc.xds_client.resources"
+
+ // From
+ // https://github.com/grpc/proposal/blob/86990145a7cef9e5473a132709b2556fec00c4c6/A94-subchannel-otel-metrics.md
+ registerGrpcMetric(
+ "grpc.subchannel.disconnections",
+ ImmutableList.of(
+ "grpc.target", "grpc.lb.backend_service", "grpc.lb.locality", "grpc.disconnect_error"));
+
+ registerGrpcMetric(
+ "grpc.subchannel.connection_attempts_succeeded",
+ ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality"));
+
+ registerGrpcMetric(
+ "grpc.subchannel.connection_attempts_failed",
+ ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality"));
+
+ registerGrpcMetric(
+ "grpc.subchannel.open_connections",
+ ImmutableList.of(
+ "grpc.target", "grpc.security_level", "grpc.lb.backend_service", "grpc.lb.locality"));
+ }
+
+ private void registerGrpcMetric(String name, List labels) {
+ grpcMetricNames.add(name);
+ register(new GrpcMetric(name, labels));
+ }
+
+ private > T register(T instrument) {
+ metrics.put(instrument.getName(), instrument);
+ return instrument;
+ }
+
+ List getGrpcMetricNames() {
+ return ImmutableList.copyOf(grpcMetricNames);
+ }
+
+ MetricWrapper> getMetric(String name) {
+ return metrics.get(name);
+ }
+
+ public RecorderRegistry newRecorderRegistry(MeterProvider meterProvider) {
+ return new RecorderRegistry(meterProvider.get(METER_NAME));
+ }
+
+ public class RecorderRegistry {
+ public final TableOperationLatency.Recorder operationLatency;
+ public final TableAttemptLatency.Recorder attemptLatency;
+ public final TableAttemptLatency2.Recorder attemptLatency2;
+ public final TableRetryCount.Recorder retryCount;
+ public final TableFirstResponseLatency.Recorder firstResponseLantency;
+ public final TableServerLatency.Recorder serverLatency;
+ public final ClientChannelPoolOutstandingRpcs.Recorder channelPoolOutstandingRpcs;
+ public final TableConnectivityErrorCount.Recorder connectivityErrorCount;
+ public final ClientDpCompatGuage.Recorder dpCompatGuage;
+ public final TableApplicationBlockingLatency.Recorder applicationBlockingLatency;
+ public final TableClientBlockingLatency.Recorder clientBlockingLatency;
+ public final ClientPerConnectionErrorCount.Recorder perConnectionErrorCount;
+ public final TableRemainingDeadline.Recorder remainingDeadline;
+ public final ClientBatchWriteFlowControlTargetQps.Recorder batchWriteFlowControlTargetQps;
+ public final ClientBatchWriteFlowControlFactor.Recorder batchWriteFlowControlFactor;
+
+ public final TableDebugTagCount.Recorder debugTagCount;
+
+ final PacemakerDelay.Recorder pacemakerDelay;
+
+ private RecorderRegistry(Meter meter) {
+ operationLatency = operationLatencyMetric.newRecorder(meter);
+ attemptLatency = attemptLatencyMetric.newRecorder(meter);
+ attemptLatency2 = attemptLatency2Metric.newRecorder(meter);
+ retryCount = retryCountMetric.newRecorder(meter);
+ firstResponseLantency = firstResponseLantencyMetric.newRecorder(meter);
+ serverLatency = serverLatencyMetric.newRecorder(meter);
+ channelPoolOutstandingRpcs = channelPoolOutstandingRpcsMetric.newRecorder(meter);
+ connectivityErrorCount = connectivityErrorCountMetric.newRecorder(meter);
+ dpCompatGuage = dpCompatGuageMetric.newRecorder(meter);
+ applicationBlockingLatency = applicationBlockingLatencyMetric.newRecorder(meter);
+ clientBlockingLatency = clientBlockingLatencyMetric.newRecorder(meter);
+ perConnectionErrorCount = perConnectionErrorCountMetric.newRecorder(meter);
+ remainingDeadline = remainingDeadlineMetric.newRecorder(meter);
+ batchWriteFlowControlTargetQps = batchWriteFlowControlTargetQpsMetric.newRecorder(meter);
+ batchWriteFlowControlFactor = batchWriteFlowControlFactorMetric.newRecorder(meter);
+
+ debugTagCount = debugTagCountMetric.newRecorder(meter);
+ pacemakerDelay = pacemakerDelayMetric.newRecorder(meter);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Pacemaker.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Pacemaker.java
new file mode 100644
index 0000000000..cb2a0c9f19
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Pacemaker.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry.RecorderRegistry;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import java.time.Duration;
+import java.time.Instant;
+
+class Pacemaker implements Runnable {
+
+ static final Duration PACEMAKER_INTERVAL = Duration.ofMillis(100);
+
+ private final RecorderRegistry registry;
+ private final ClientInfo clientInfo;
+ private final String executorName;
+
+ private Instant prev;
+
+ Pacemaker(RecorderRegistry registry, ClientInfo clientInfo, String name) {
+ this.prev = Instant.now();
+ this.registry = registry;
+ this.clientInfo = clientInfo;
+ this.executorName = name;
+ }
+
+ @Override
+ public void run() {
+ Instant current = Instant.now();
+ Duration delta = Duration.between(prev, current).minus(PACEMAKER_INTERVAL);
+ prev = current;
+ registry.pacemakerDelay.record(
+ clientInfo, executorName, delta.isNegative() ? Duration.ZERO : delta);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java
new file mode 100644
index 0000000000..0d4717dfe9
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import com.google.auto.value.AutoValue;
+import com.google.bigtable.v2.InstanceName;
+import com.google.cloud.bigtable.Version;
+
+/**
+ * A value class to capture parameters that the client was instantiated with. These parameters will
+ * be used by the Exporter to derive MonitoredResource for GrpcMetrics.
+ */
+@AutoValue
+public abstract class ClientInfo {
+ /** The name and version of the client. */
+ public abstract String getClientName();
+
+ /** A unique identifier to disambiguate TimeSeries from multiple processes on the same VM. */
+ public abstract InstanceName getInstanceName();
+
+ public abstract String getAppProfileId();
+
+ public abstract Builder toBuilder();
+
+ public static Builder builder() {
+ return new AutoValue_ClientInfo.Builder().setClientName("java-bigtable/" + Version.VERSION);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ protected abstract Builder setClientName(String name);
+
+ public abstract Builder setInstanceName(InstanceName name);
+
+ public abstract Builder setAppProfileId(String appProfileId);
+
+ public abstract ClientInfo build();
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
new file mode 100644
index 0000000000..cfc9182881
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import com.google.auto.value.AutoValue;
+import com.google.cloud.opentelemetry.detection.AttributeKeys;
+import com.google.cloud.opentelemetry.detection.DetectedPlatform;
+import com.google.cloud.opentelemetry.detection.GCPPlatformDetector;
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Environment attributes, lazily extracted by the Exporter.
+ *
+ *
The information will be extracted from the GCE metadata service and environment.
+ */
+@AutoValue
+public abstract class EnvInfo {
+ private static final Logger logger = Logger.getLogger(EnvInfo.class.getName());
+
+ private static final Map SUPPORTED_PLATFORM_MAP =
+ ImmutableMap.of(
+ GCPPlatformDetector.SupportedPlatform.GOOGLE_COMPUTE_ENGINE, "gcp_compute_engine",
+ GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE, "gcp_kubernetes_engine");
+
+ private static final AtomicLong uidSuffix = new AtomicLong(0);
+
+ public abstract String getUid();
+
+ /** The Google platform running this client. ie. gcp_compute_engine */
+ public abstract String getPlatform();
+
+ /** The Google project that the VM belongs to. */
+ public abstract String getProject();
+
+ /** The geographic region that the VM is located in. */
+ public abstract String getRegion();
+
+ /** The numeric GCE vm instance id. */
+ public abstract String getHostId();
+
+ /** The hostname of the vm or container running the client. For gke, this will be the pod name. */
+ public abstract String getHostName();
+
+ public static Builder builder() {
+ return new AutoValue_EnvInfo.Builder().setUid(computeUid() + "-" + uidSuffix.getAndIncrement());
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ protected abstract Builder setUid(String uid);
+
+ public abstract Builder setPlatform(String platform);
+
+ public abstract Builder setProject(String project);
+
+ public abstract Builder setRegion(String region);
+
+ public abstract Builder setHostId(String hostId);
+
+ public abstract Builder setHostName(String hostName);
+
+ public abstract EnvInfo build();
+ }
+
+ private static String computeUid() {
+ final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ // If jvm doesn't have the expected format, fallback to the local hostname
+ if (jvmName.indexOf('@') < 1) {
+ String hostname = "localhost";
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ logger.log(Level.INFO, "Unable to get the hostname.", e);
+ }
+ // Generate a random number and use the same format "random_number@hostname".
+ return "java-" + UUID.randomUUID() + "@" + hostname;
+ }
+ return "java-" + UUID.randomUUID() + jvmName;
+ }
+
+ public static EnvInfo detect() {
+ return detect(
+ GCPPlatformDetector.DEFAULT_INSTANCE.detectPlatform(),
+ System::getenv,
+ () -> {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @Nullable
+ static EnvInfo detect(
+ DetectedPlatform detectedPlatform,
+ Function envGetter,
+ Supplier hostnameSupplier) {
+ @Nullable
+ String cloud_platform = SUPPORTED_PLATFORM_MAP.get(detectedPlatform.getSupportedPlatform());
+ if (cloud_platform == null) {
+ return EnvInfo.builder()
+ .setPlatform("unknown")
+ .setHostName(detectHostname(envGetter, hostnameSupplier))
+ .setRegion("global")
+ .setProject("")
+ .setHostId("")
+ .build();
+ }
+
+ Map attrs = detectedPlatform.getAttributes();
+ ImmutableList locationKeys =
+ ImmutableList.of(
+ AttributeKeys.GCE_CLOUD_REGION,
+ AttributeKeys.GCE_AVAILABILITY_ZONE,
+ AttributeKeys.GKE_LOCATION_TYPE_REGION,
+ AttributeKeys.GKE_CLUSTER_LOCATION);
+
+ String region =
+ locationKeys.stream().map(attrs::get).filter(Objects::nonNull).findFirst().orElse("global");
+
+ // Deal with possibility of a zone. Zones are of the form us-east1-c, but we want a region
+ // which, which is us-east1.
+ region = Splitter.on('-').splitToStream(region).limit(2).collect(Collectors.joining("-"));
+
+ String hostname = attrs.get(AttributeKeys.GCE_INSTANCE_NAME);
+ // TODO: add support for cloud run & gae by looking at SERVERLESS_COMPUTE_NAME & GAE_MODULE_NAME
+ if (hostname == null) {
+ hostname = detectHostname(envGetter, hostnameSupplier);
+ }
+
+ String hostId = Optional.ofNullable(attrs.get(AttributeKeys.GCE_INSTANCE_ID)).orElse("");
+
+ return builder()
+ .setPlatform(cloud_platform)
+ .setProject(detectedPlatform.getProjectId())
+ .setRegion(region)
+ .setHostId(hostId)
+ .setHostName(hostname)
+ .build();
+ }
+
+ private static String detectHostname(
+ Function envGetter, Supplier hostnameSupplier) {
+ String hostname = envGetter.apply("HOSTNAME");
+
+ if (hostname == null) {
+ try {
+ hostname = hostnameSupplier.get();
+ } catch (RuntimeException e) {
+ logger.log(Level.WARNING, "failed to detect hostname", e);
+ }
+ }
+ if (hostname == null) {
+ hostname = "";
+ }
+ return hostname;
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/MethodInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/MethodInfo.java
new file mode 100644
index 0000000000..122e5fe5ba
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/MethodInfo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import com.google.auto.value.AutoValue;
+
+/** Method specific attributes. */
+@AutoValue
+public abstract class MethodInfo {
+
+ /** The name of the method. ie "Bigtable.ReadRow" */
+ public abstract String getName();
+
+ /** If the method is streaming (ie a scan). */
+ public abstract boolean getStreaming();
+
+ public static Builder builder() {
+ return new AutoValue_MethodInfo.Builder();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setName(String name);
+
+ public abstract Builder setStreaming(boolean isStreaming);
+
+ public abstract MethodInfo build();
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
new file mode 100644
index 0000000000..cf9c2a114e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import com.google.bigtable.v2.PeerInfo.TransportType;
+import com.google.common.base.Preconditions;
+import java.util.Locale;
+
+public class Util {
+ static final String TRANSPORT_TYPE_PREFIX = "TRANSPORT_TYPE_";
+
+ public static String transportTypeToString(TransportType transportType) {
+
+ Preconditions.checkArgument(
+ transportType.name().startsWith(TRANSPORT_TYPE_PREFIX)
+ || transportType == TransportType.UNRECOGNIZED,
+ "TransportType values must start with %s",
+ TRANSPORT_TYPE_PREFIX);
+
+ if (transportType == TransportType.TRANSPORT_TYPE_UNKNOWN) {
+ return "session_none";
+ }
+ if (transportType == TransportType.UNRECOGNIZED) {
+ return "session_unrecognized";
+ }
+
+ return transportType
+ .name()
+ .substring(TRANSPORT_TYPE_PREFIX.length())
+ .toLowerCase(Locale.ENGLISH);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlFactor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlFactor.java
new file mode 100644
index 0000000000..2c5a989d51
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlFactor.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.api.metrics.Meter;
+
+public class ClientBatchWriteFlowControlFactor extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/batch_write_flow_control_factor";
+
+ public ClientBatchWriteFlowControlFactor() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleGauge instrument;
+
+ private Recorder(Meter meter) {
+ this.instrument =
+ meter
+ .gaugeBuilder(NAME)
+ .setDescription(
+ "The distribution of batch write flow control factors received from the server.")
+ .setUnit("1")
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ Status.Code code,
+ boolean applied,
+ MethodInfo methodInfo,
+ double factor) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.APPLIED_KEY, applied)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .build();
+
+ instrument.set(factor, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlTargetQps.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlTargetQps.java
new file mode 100644
index 0000000000..fb6f55894f
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientBatchWriteFlowControlTargetQps.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.api.metrics.Meter;
+
+public class ClientBatchWriteFlowControlTargetQps extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/batch_write_flow_control_target_qps";
+
+ public ClientBatchWriteFlowControlTargetQps() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleGauge instrument;
+
+ private Recorder(Meter meter) {
+ this.instrument =
+ meter
+ .gaugeBuilder(NAME)
+ .setDescription(
+ "The current target QPS of the client under batch write flow control.")
+ .setUnit("1")
+ .build();
+ }
+
+ public void record(ClientInfo clientInfo, MethodInfo methodInfo, double qps) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .build();
+
+ instrument.set(qps, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientChannelPoolOutstandingRpcs.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientChannelPoolOutstandingRpcs.java
new file mode 100644
index 0000000000..addd28a533
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientChannelPoolOutstandingRpcs.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.PeerInfo.TransportType;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings.LoadBalancingStrategy;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ClientChannelPoolOutstandingRpcs extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/connection_pool/outstanding_rpcs";
+
+ private static final List BUCKETS =
+ Buckets.generateLinearSeq(0d, 200d, 5).stream()
+ .map(Double::longValue)
+ .collect(Collectors.toList());
+
+ public ClientChannelPoolOutstandingRpcs() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongHistogram instrument;
+
+ private Recorder(Meter meter) {
+ this.instrument =
+ meter
+ .histogramBuilder(NAME)
+ .ofLongs()
+ .setExplicitBucketBoundariesAdvice(BUCKETS)
+ .setDescription(
+ "A distribution of the number of outstanding RPCs per connection in the client"
+ + " pool, sampled periodically.")
+ .setUnit("1")
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ TransportType transportType,
+ LoadBalancingStrategy lbPolicy,
+ boolean isStreaming,
+ long rpcCount) {
+ instrument.record(
+ rpcCount,
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.TRANSPORT_TYPE, Util.transportTypeToString(transportType))
+ .put(MetricLabels.CHANNEL_POOL_LB_POLICY, lbPolicy.name())
+ .put(MetricLabels.STREAMING_KEY, isStreaming)
+ .build());
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java
new file mode 100644
index 0000000000..9746e67448
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import io.opentelemetry.api.metrics.LongGauge;
+import io.opentelemetry.api.metrics.Meter;
+
+public class ClientDpCompatGuage extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/direct_access/compatible";
+
+ public ClientDpCompatGuage() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongGauge instrument;
+
+ private Recorder(Meter meter) {
+ this.instrument =
+ meter
+ .gaugeBuilder(NAME)
+ .ofLongs()
+ .setDescription(
+ "Reports 1 if the environment is eligible for DirectPath, 0 otherwise. Based on"
+ + " an attempt at startup.")
+ .setUnit("1")
+ .build();
+ }
+
+ // TODO: replace ipPreference with an enum
+ public void recordSuccess(ClientInfo clientInfo, String ipPreference) {
+ instrument.set(
+ 1,
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.DP_REASON_KEY, "")
+ .put(MetricLabels.DP_IP_PREFERENCE_KEY, ipPreference)
+ .build());
+ }
+
+ // TODO: replace reason with an enum
+ public void recordFailure(ClientInfo clientInfo, String reason) {
+ instrument.set(
+ 1,
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.DP_REASON_KEY, reason)
+ .put(MetricLabels.DP_IP_PREFERENCE_KEY, "")
+ .build());
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientPerConnectionErrorCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientPerConnectionErrorCount.java
new file mode 100644
index 0000000000..25ede477fb
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientPerConnectionErrorCount.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.List;
+import java.util.Set;
+
+public class ClientPerConnectionErrorCount extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/per_connection_error_count";
+
+ static final List BUCKETS =
+ ImmutableList.builder()
+ .add(0L)
+ .addAll(Buckets.generateGeometricSeq(1, 64))
+ .addAll(Buckets.generateGeometricSeq(125, 1_000_000L))
+ .build();
+ // This metric migrated from gce/gke schemas to bigtable_client
+ // So a lot of the metric labels overlap with the resource labels.
+ // we need special handling since the logic in MetricWrapper assumes that there is no
+ // overlap.
+ private static final Set> METRIC_LABELS =
+ ImmutableSet.of(
+ MetricLabels.BIGTABLE_PROJECT_ID_KEY,
+ MetricLabels.CLIENT_UID,
+ MetricLabels.INSTANCE_ID_KEY,
+ MetricLabels.CLIENT_NAME,
+ MetricLabels.APP_PROFILE_KEY);
+
+ public ClientPerConnectionErrorCount() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ // Override the default metric labels to account for backwards compatibility.
+ // This metric used to live under bigtable_table, and has moved to bigtable_client
+ // The new schema duplicates some of the metric labels. However the default implementation
+ // in MetricWrapper will remove all resource labels from the metric labels.
+ // To maintain backwards compatibility, this metric override the extractMetricLabels
+ // to always emit the duplicate metric labels.
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ metricAttrs.forEach(
+ (k, v) -> {
+ if (METRIC_LABELS.contains(k) && v != null) {
+ builder.put(k.getKey(), v.toString());
+ }
+ });
+ builder.put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid());
+ return builder.build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .ofLongs()
+ .setDescription("Distribution of counts of channels per 'error count per minute'.")
+ .setUnit("1")
+ .setExplicitBucketBoundariesAdvice(BUCKETS)
+ .build();
+ }
+
+ public void record(ClientInfo clientInfo, long value) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.BIGTABLE_PROJECT_ID_KEY, clientInfo.getInstanceName().getProject())
+ .put(MetricLabels.INSTANCE_ID_KEY, clientInfo.getInstanceName().getInstance())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .build();
+ instrument.record(value, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/Constants.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/Constants.java
new file mode 100644
index 0000000000..768f451e0e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/Constants.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.common.collect.ImmutableList;
+import io.opentelemetry.api.common.AttributeKey;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class Constants {
+ private Constants() {}
+
+ public static final class MetricLabels {
+ private MetricLabels() {}
+
+ // TODO: remove overlapping attributes
+ // Project & Instance overlap with resource labels because they were migrated from
+ // an old gce/gke schema to support per_connection_error_count metric
+ @Deprecated
+ public static final AttributeKey BIGTABLE_PROJECT_ID_KEY =
+ AttributeKey.stringKey("project_id");
+
+ @Deprecated
+ public static final AttributeKey INSTANCE_ID_KEY = AttributeKey.stringKey("instance");
+
+ public static final AttributeKey TRANSPORT_TYPE =
+ AttributeKey.stringKey("transport_type");
+ public static final AttributeKey TRANSPORT_REGION =
+ AttributeKey.stringKey("transport_region");
+ public static final AttributeKey TRANSPORT_ZONE =
+ AttributeKey.stringKey("transport_zone");
+ public static final AttributeKey TRANSPORT_SUBZONE =
+ AttributeKey.stringKey("transport_subzone");
+
+ public static final AttributeKey CLIENT_UID = AttributeKey.stringKey("client_uid");
+ public static final AttributeKey CLIENT_NAME = AttributeKey.stringKey("client_name");
+ public static final AttributeKey METHOD_KEY = AttributeKey.stringKey("method");
+ public static final AttributeKey STREAMING_KEY = AttributeKey.booleanKey("streaming");
+ public static final AttributeKey APP_PROFILE_KEY =
+ AttributeKey.stringKey("app_profile");
+ public static final AttributeKey DEBUG_TAG_KEY = AttributeKey.stringKey("tag");
+
+ static final AttributeKey APPLIED_KEY = AttributeKey.booleanKey("applied");
+
+ static final AttributeKey CHANNEL_POOL_LB_POLICY = AttributeKey.stringKey("lb_policy");
+ static final AttributeKey DP_REASON_KEY = AttributeKey.stringKey("reason");
+ static final AttributeKey DP_IP_PREFERENCE_KEY = AttributeKey.stringKey("reason");
+
+ public static final AttributeKey STATUS_KEY = AttributeKey.stringKey("status");
+
+ static final AttributeKey EXECUTOR_KEY = AttributeKey.stringKey("executor");
+ }
+
+ static final class Units {
+ private Units() {}
+
+ static final String MILLISECOND = "ms";
+ static final String MICROSECOND = "us";
+ static final String COUNT = "1";
+ }
+
+ static final class Buckets {
+ static final List AGGREGATION_WITH_MILLIS_HISTOGRAM =
+ ImmutableList.builder()
+ // Match `bigtable.googleapis.com/frontend_server/handler_latencies` buckets
+ .addAll(generateLinearSeq(0, 3.0, 0.1))
+ .add(4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0)
+ .add(100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0, 800.0, 900.0)
+ .add(1000.0, 2000.0, 3000.0, 4000.0, 5000.0, 6000.0, 10000.0, 20000.0, 50000.0)
+ .add(100000.0, 200000.0, 500000.0, 1000000.0, 2000000.0, 5000000.0)
+ .build();
+
+ @SuppressWarnings("SameParameterValue")
+ static List generateLinearSeq(double start, double end, double increment) {
+ ImmutableList.Builder builder = ImmutableList.builder();
+ for (int i = 0; true; i++) {
+ double next = start + (increment * i);
+ if (next > end) {
+ break;
+ }
+ builder.add(next);
+ }
+
+ return builder.build();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ static List generateExponentialSeq(double start, int count, double factor) {
+ List buckets = new ArrayList<>();
+
+ for (int i = 0; i < count; i++) {
+ buckets.add(start);
+ start *= factor;
+ }
+
+ return buckets;
+ }
+
+ static List generateGeometricSeq(long startClose, long endClosed) {
+ ImmutableList.Builder builder = ImmutableList.builder();
+ for (long i = startClose; i <= endClosed; i *= 2) {
+ builder.add(i);
+ }
+ return builder.build();
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/GrpcMetric.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/GrpcMetric.java
new file mode 100644
index 0000000000..e4ddc12165
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/GrpcMetric.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.GrpcClientSchema;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import java.util.List;
+
+/**
+ * Base class for gRpc metrics that are exported using bigtable_client schema.
+ *
+ *
gRPC doesn't record the bigtable specific metric labels, so they must be passed to the
+ * exporter via a side channel.
+ */
+public class GrpcMetric extends MetricWrapper {
+ public static final String METER_SCOPE = "grpc-java";
+
+ private final List> metricKeys;
+
+ public GrpcMetric(String name, List metricKeys) {
+ super(GrpcClientSchema.INSTANCE, name);
+ this.metricKeys =
+ metricKeys.stream().map(AttributeKey::stringKey).collect(ImmutableList.toImmutableList());
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo ignored1, ClientInfo ignored2) {
+ ImmutableMap.Builder attributes = ImmutableMap.builder();
+
+ for (AttributeKey> key : metricKeys) {
+ String newKeyName = key.getKey().replace('.', '_');
+ Object value = metricAttrs.get(key);
+ if (value != null) {
+ attributes.put(newKeyName, value.toString());
+ }
+ }
+
+ return attributes.build();
+ }
+
+ @Override
+ public String getExternalName() {
+ return "bigtable.googleapis.com/internal/client/" + getName().replace('.', '/');
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/MetricWrapper.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/MetricWrapper.java
new file mode 100644
index 0000000000..a6c882d820
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/MetricWrapper.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.api.MonitoredResource;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.Schema;
+import com.google.common.collect.ImmutableMap;
+import io.opentelemetry.api.common.Attributes;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for all the metrics.
+ *
+ *
Each metric is composed of an OpenTelemetry instrument (ie histogram), and a set of resource
+ * and metric attributes. Since some of the resource attributes are dynamic, all the resource
+ * attributes are sent to the instrument as metric {@link Attributes}. Then during the export phase,
+ * a {@link MonitoredResource} and a set of metric labels are extracted from the collected
+ * attributes.
+ *
+ *
This base class implements the foundation of this lifecycle:
+ *
+ *
+ *
The instrument for recording is passed in during construction
+ *
The concrete subclass will define a metric specific typesafe record method to populate the
+ * metric labels for the instrument
+ *
The list of resource attribute keys are defined by a resource specific subclass and passed
+ * in during construction. These will be used by {@code MetricWrapper.createMonitoredResource}
+ * to create the monitored resource during the export phase
+ *
The remaining attributes will be added as metric labels
+ *
+ */
+public abstract class MetricWrapper {
+ private final SchemaT schema;
+ private final String name;
+
+ public MetricWrapper(SchemaT schema, String name) {
+ this.schema = schema;
+ this.name = name;
+ }
+
+ public SchemaT getSchema() {
+ return schema;
+ }
+
+ /**
+ * Used by the Exporter to compose metric labels to be sent to Cloud Monitoring.
+ *
+ *
Extracts metric labels from metric {@link Attributes}. By default, all keys that are not
+ * listed in {@code resourceKeys} are extracted. However, subclasses can override this method to
+ * inject data from {@link EnvInfo} and {@link ClientInfo}.
+ */
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ metricAttrs.forEach(
+ (k, v) -> {
+ if (!getSchema().getResourceKeys().contains(k) && v != null) {
+ builder.put(k.getKey(), v.toString());
+ }
+ });
+ return builder.build();
+ }
+
+ /**
+ * Used by the Exporter to match an instance of this class to the aggregated timeseries to export.
+ *
+ *
Gets the name of the metric. This is used by the exporter to look up this metric definition
+ * in MetricRegistry during export.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Used by the exporter to post process the metric name from grpc conventions to Cloud Monitoring.
+ */
+ public String getExternalName() {
+ return getName();
+ }
+
+ /** Converts a duration in fractional milliseconds. */
+ protected static double toMillis(Duration duration) {
+ return Math.round(((double) duration.toNanos()) / TimeUnit.MILLISECONDS.toNanos(1) * 100.0)
+ / 100.0;
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/PacemakerDelay.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/PacemakerDelay.java
new file mode 100644
index 0000000000..ec081f2afd
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/PacemakerDelay.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.ClientSchema;
+import com.google.common.collect.ImmutableList;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Pacemaker delay records the delta between the pacemaker scheduled time and the actual time. When
+ * the delay is high, it could indicate issues with the machine that the client is running on like
+ * CPU saturation.
+ */
+public class PacemakerDelay extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/pacemaker_delays";
+
+ private static final List BUCKETS =
+ ImmutableList.builder()
+ // Up to 67,108,864, ~1 minute in microseconds
+ .addAll(Buckets.generateExponentialSeq(1.0, 13, 4))
+ .build();
+
+ public PacemakerDelay() {
+ super(ClientSchema.INSTANCE, NAME);
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "Distribution of the delay between the pacemaker firing and the pacemaker task"
+ + " being scheduled.")
+ .setUnit(Units.MICROSECOND)
+ .setExplicitBucketBoundariesAdvice(BUCKETS)
+ .build();
+ }
+
+ public void record(ClientInfo clientInfo, String executorName, Duration delta) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.EXECUTOR_KEY, executorName)
+ .build();
+ instrument.record(delta.toNanos() / 1000.0, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java
new file mode 100644
index 0000000000..90e390304e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableApplicationBlockingLatency extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/application_latencies";
+
+ public TableApplicationBlockingLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ this.instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "The latency of the client application consuming available response data.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java
new file mode 100644
index 0000000000..2ba86e89c9
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableAttemptLatency extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/attempt_latencies";
+
+ public TableAttemptLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription("Client observed latency per RPC attempt.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ ResponseParams clusterInfo,
+ MethodInfo methodInfo,
+ Status.Code code,
+ Duration latency) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STREAMING_KEY, methodInfo.getStreaming())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(latency), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java
new file mode 100644
index 0000000000..0570559610
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.PeerInfo;
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableAttemptLatency2 extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/attempt_latencies2";
+
+ public TableAttemptLatency2() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription("Client observed latency per RPC attempt.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ PeerInfo peerInfo,
+ ResponseParams clusterInfo,
+ MethodInfo methodInfo,
+ Status.Code code,
+ Duration latency) {
+
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(
+ MetricLabels.TRANSPORT_TYPE,
+ Util.transportTypeToString(peerInfo.getTransportType()))
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.TRANSPORT_REGION, "")
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .put(MetricLabels.TRANSPORT_ZONE, peerInfo.getApplicationFrontendZone())
+ .put(MetricLabels.TRANSPORT_SUBZONE, peerInfo.getApplicationFrontendSubzone())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.STREAMING_KEY, methodInfo.getStreaming())
+ .build();
+
+ instrument.record(toMillis(latency), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java
new file mode 100644
index 0000000000..1d8deca639
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableClientBlockingLatency extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/throttling_latencies";
+
+ public TableClientBlockingLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "The latency introduced by the client queuing the RPC due to an unavailable"
+ + " transport or overload.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java
new file mode 100644
index 0000000000..95d8fca949
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+
+public class TableConnectivityErrorCount extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/connectivity_error_count";
+
+ public TableConnectivityErrorCount() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongCounter instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .counterBuilder(NAME)
+ .setDescription(
+ "Number of requests that failed to reach the Google datacenter. (Requests without"
+ + " google response headers)")
+ .setUnit(Units.COUNT)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ long count) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.add(count, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java
new file mode 100644
index 0000000000..f8bfc25fb5
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+
+public class TableDebugTagCount extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/debug_tags";
+
+ public TableDebugTagCount() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongCounter instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .counterBuilder(NAME)
+ .setDescription("A counter of internal client events used for debugging.")
+ .setUnit(Units.COUNT)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ String tag,
+ ResponseParams clusterInfo,
+ long amount) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ .put(MetricLabels.DEBUG_TAG_KEY, tag)
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .build();
+ instrument.add(amount, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java
new file mode 100644
index 0000000000..af5909c054
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableFirstResponseLatency extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/first_response_latencies";
+
+ public TableFirstResponseLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "Latency from operation start until the response headers were received. The"
+ + " publishing of the measurement will be delayed until the attempt response"
+ + " has been received.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java
new file mode 100644
index 0000000000..b6323cce8b
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableOperationLatency extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/operation_latencies";
+
+ public TableOperationLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "Total time until final operation success or failure, including retries and"
+ + " backoff.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STREAMING_KEY, methodInfo.getStreaming())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRemainingDeadline.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRemainingDeadline.java
new file mode 100644
index 0000000000..3e911d42e6
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRemainingDeadline.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableRemainingDeadline extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/remaining_deadline";
+
+ public TableRemainingDeadline() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "The remaining deadline when the request is sent to grpc. This will either be the"
+ + " operation timeout, or the remaining deadline from operation timeout after"
+ + " retries and back offs.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.STATUS_KEY, code.name())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.STREAMING_KEY, methodInfo.getStreaming())
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java
new file mode 100644
index 0000000000..de7b608b4e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+
+public class TableRetryCount extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/retry_count";
+
+ public TableRetryCount() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final LongCounter instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .counterBuilder(NAME)
+ .setDescription("The number of additional RPCs sent after the initial attempt.")
+ .setUnit(Units.COUNT)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ long amount) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+ instrument.add(amount, attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java
new file mode 100644
index 0000000000..b759591113
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Buckets;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.MetricLabels;
+import com.google.cloud.bigtable.data.v2.internal.csm.metrics.Constants.Units;
+import com.google.cloud.bigtable.data.v2.internal.csm.schema.TableSchema;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Status;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+public class TableServerLatency extends MetricWrapper {
+ private static final String NAME = "bigtable.googleapis.com/internal/client/server_latencies";
+
+ public TableServerLatency() {
+ super(TableSchema.INSTANCE, NAME);
+ }
+
+ @Override
+ public ImmutableMap extractMetricLabels(
+ Attributes metricAttrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ImmutableMap.builder()
+ .putAll(super.extractMetricLabels(metricAttrs, envInfo, clientInfo))
+ .put(MetricLabels.CLIENT_UID.getKey(), envInfo.getUid())
+ .build();
+ }
+
+ public Recorder newRecorder(Meter meter) {
+ return new Recorder(meter);
+ }
+
+ public class Recorder {
+ private final DoubleHistogram instrument;
+
+ private Recorder(Meter meter) {
+ instrument =
+ meter
+ .histogramBuilder(NAME)
+ .setDescription(
+ "The latency measured from the moment that the RPC entered the Google data center"
+ + " until the RPC was completed.")
+ .setUnit(Units.MILLISECOND)
+ .setExplicitBucketBoundariesAdvice(Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .build();
+ }
+
+ public void record(
+ ClientInfo clientInfo,
+ String tableId,
+ MethodInfo methodInfo,
+ ResponseParams clusterInfo,
+ Status.Code code,
+ Duration duration) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo, tableId, clusterInfo)
+ .put(MetricLabels.METHOD_KEY, methodInfo.getName())
+ .put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(MetricLabels.STREAMING_KEY, methodInfo.getStreaming())
+ .put(MetricLabels.STATUS_KEY, code.name())
+ .put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
+ // To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
+ .build();
+
+ instrument.record(toMillis(duration), attributes);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/package-info.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/package-info.java
new file mode 100644
index 0000000000..e6e4fb388c
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
+
+// Implements the metrics from bigtable_googleapis_com/metrics/aliased_metrics.gcl &
+// cloud_pulse_monarch/bigtable/metrics
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/ClientSchema.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/ClientSchema.java
new file mode 100644
index 0000000000..11cf90c445
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/ClientSchema.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.schema;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.common.collect.ImmutableList;
+import com.google.monitoring.v3.ProjectName;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+
+/**
+ * The attributes for this schema are partially populated during the record phase and finalized
+ * during the export phase with {@link EnvInfo}. This is necessary because resolving {@link EnvInfo}
+ * is slow and should not happen during client startup.
+ */
+public final class ClientSchema extends Schema {
+ // This implements the `bigtable_client` resource defined in
+ // bigtable_googleapis_com/metrics/resource_types.gcl
+
+ public static final AttributeKey BIGTABLE_PROJECT_ID_KEY =
+ AttributeKey.stringKey("project_id");
+ // Resource labels passed during recording
+ public static final AttributeKey INSTANCE_ID_KEY = AttributeKey.stringKey("instance");
+ public static final AttributeKey APP_PROFILE_KEY = AttributeKey.stringKey("app_profile");
+ public static final AttributeKey CLIENT_NAME = AttributeKey.stringKey("client_name");
+
+ // Resource labels injected during export
+ private static final DeferredAttr CLIENT_PROJECT =
+ DeferredAttr.fromEnv("client_project", EnvInfo::getProject);
+ private static final DeferredAttr CLIENT_REGION =
+ DeferredAttr.fromEnv("region", EnvInfo::getRegion);
+ private static final DeferredAttr CLOUD_PLATFORM =
+ DeferredAttr.fromEnv("cloud_platform", EnvInfo::getPlatform);
+ private static final DeferredAttr HOST_ID = DeferredAttr.fromEnv("host_id", EnvInfo::getHostId);
+ private static final DeferredAttr HOST_NAME =
+ DeferredAttr.fromEnv("host_name", EnvInfo::getHostName);
+ private static final DeferredAttr UUID = DeferredAttr.fromEnv("uuid", EnvInfo::getUid);
+
+ // Must come after all other static members
+ public static final ClientSchema INSTANCE = new ClientSchema();
+
+ public ClientSchema() {
+ super(
+ "bigtable_client",
+ ImmutableList.of(BIGTABLE_PROJECT_ID_KEY, INSTANCE_ID_KEY, APP_PROFILE_KEY, CLIENT_NAME),
+ ImmutableList.of(CLIENT_PROJECT, CLIENT_REGION, CLOUD_PLATFORM, HOST_ID, HOST_NAME, UUID));
+ }
+
+ @Override
+ public ProjectName extractProjectName(Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ProjectName.of(clientInfo.getInstanceName().getProject());
+ }
+
+ public AttributesBuilder createResourceAttrs(ClientInfo clientInfo) {
+ return Attributes.builder()
+ .put(BIGTABLE_PROJECT_ID_KEY, clientInfo.getInstanceName().getProject())
+ .put(INSTANCE_ID_KEY, clientInfo.getInstanceName().getInstance())
+ .put(APP_PROFILE_KEY, clientInfo.getAppProfileId())
+ .put(CLIENT_NAME, clientInfo.getClientName());
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/GrpcClientSchema.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/GrpcClientSchema.java
new file mode 100644
index 0000000000..62a8df1d3c
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/GrpcClientSchema.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.schema;
+
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.common.collect.ImmutableList;
+import com.google.monitoring.v3.ProjectName;
+import io.opentelemetry.api.common.Attributes;
+
+/**
+ * The attributes for this schema are partially populated during the record phase and finalized
+ * during the export phase with {@link EnvInfo}. This is necessary because resolving {@link EnvInfo}
+ * is slow and should not happen during client startup.
+ */
+public final class GrpcClientSchema extends Schema {
+ // Unlike the normal ClientSchema, the bigtable resource ids must be injected during export time
+ private static final DeferredAttr BIGTABLE_PROJECT_ID =
+ DeferredAttr.fromClientInfo("project_id", ci -> ci.getInstanceName().getProject());
+ private static final DeferredAttr INSTANCE_ID =
+ DeferredAttr.fromClientInfo("instance", ci -> ci.getInstanceName().getInstance());
+ private static final DeferredAttr APP_PROFILE_ID =
+ DeferredAttr.fromClientInfo("app_profile", ClientInfo::getAppProfileId);
+ private static final DeferredAttr CLIENT_NAME =
+ DeferredAttr.fromClientInfo("client_name", ClientInfo::getClientName);
+
+ private static final DeferredAttr CLIENT_PROJECT =
+ DeferredAttr.fromEnv("client_project", EnvInfo::getProject);
+ private static final DeferredAttr CLIENT_REGION =
+ DeferredAttr.fromEnv("region", EnvInfo::getRegion);
+ private static final DeferredAttr CLOUD_PLATFORM =
+ DeferredAttr.fromEnv("cloud_platform", EnvInfo::getPlatform);
+ private static final DeferredAttr HOST_ID = DeferredAttr.fromEnv("host_id", EnvInfo::getHostId);
+ private static final DeferredAttr HOST_NAME =
+ DeferredAttr.fromEnv("host_name", EnvInfo::getHostName);
+ private static final DeferredAttr UUID = DeferredAttr.fromEnv("uuid", EnvInfo::getUid);
+
+ // Must come after all other static members
+ public static final GrpcClientSchema INSTANCE = new GrpcClientSchema();
+
+ private GrpcClientSchema() {
+ super(
+ "bigtable_client",
+ ImmutableList.of(),
+ ImmutableList.of(
+ BIGTABLE_PROJECT_ID,
+ INSTANCE_ID,
+ APP_PROFILE_ID,
+ CLIENT_NAME,
+ // Same as ClientSchema
+ CLIENT_PROJECT,
+ CLIENT_REGION,
+ CLOUD_PLATFORM,
+ HOST_ID,
+ HOST_NAME,
+ UUID));
+ }
+
+ @Override
+ public ProjectName extractProjectName(
+ Attributes ignored, EnvInfo ignored2, ClientInfo clientInfo) {
+ return ProjectName.of(clientInfo.getInstanceName().getProject());
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/Schema.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/Schema.java
new file mode 100644
index 0000000000..a5d621acbc
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/Schema.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.schema;
+
+import com.google.api.MonitoredResource;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.common.collect.ImmutableList;
+import com.google.monitoring.v3.ProjectName;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+public abstract class Schema {
+ private final String name;
+ private final List> resourceKeys;
+ private final List deferredAttrs;
+
+ Schema(String name, List> resourceKeys) {
+ this(name, resourceKeys, ImmutableList.of());
+ }
+
+ Schema(String name, List> resourceKeys, List deferredAttrs) {
+ this.name = name;
+ this.resourceKeys = resourceKeys;
+ this.deferredAttrs = deferredAttrs;
+ }
+
+ public List> getResourceKeys() {
+ return resourceKeys;
+ }
+
+ public abstract ProjectName extractProjectName(
+ Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo);
+
+ public MonitoredResource extractMonitoredResource(
+ Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ MonitoredResource.Builder builder = MonitoredResource.newBuilder().setType(name);
+
+ for (AttributeKey> key : resourceKeys) {
+ Object value = attrs.get(key);
+ if (value != null) {
+ builder.putLabels(key.getKey(), value.toString());
+ }
+ }
+ for (DeferredAttr a : deferredAttrs) {
+ builder.putLabels(a.getKey().getKey(), a.getValue(envInfo, clientInfo));
+ }
+ return builder.build();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ static class DeferredAttr {
+ private final AttributeKey name;
+ private BiFunction extractor;
+
+ static DeferredAttr fromEnv(String name, Function envExtractor) {
+ return new DeferredAttr(
+ AttributeKey.stringKey(name), (envInfo, ignored) -> envExtractor.apply(envInfo));
+ }
+
+ static DeferredAttr fromClientInfo(String name, Function envExtractor) {
+ return new DeferredAttr(
+ AttributeKey.stringKey(name), (ignored, clientInfo) -> envExtractor.apply(clientInfo));
+ }
+
+ private DeferredAttr(
+ AttributeKey name, BiFunction extractor) {
+ this.name = name;
+ this.extractor = extractor;
+ }
+
+ AttributeKey getKey() {
+ return name;
+ }
+
+ String getValue(EnvInfo envInfo, ClientInfo clientInfo) {
+ return extractor.apply(envInfo, clientInfo);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java
new file mode 100644
index 0000000000..f536f73837
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.schema;
+
+import com.google.bigtable.v2.ResponseParams;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.common.collect.ImmutableList;
+import com.google.monitoring.v3.ProjectName;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+
+public final class TableSchema extends Schema {
+ // This implements the `bigtable_client_raw` resource defined in
+ // bigtable_googleapis_com/metrics/resource_types.gcl
+
+ public static final AttributeKey BIGTABLE_PROJECT_ID_KEY =
+ AttributeKey.stringKey("project_id");
+ public static final AttributeKey INSTANCE_ID_KEY = AttributeKey.stringKey("instance");
+ public static final AttributeKey TABLE_ID_KEY = AttributeKey.stringKey("table");
+ public static final AttributeKey CLUSTER_ID_KEY = AttributeKey.stringKey("cluster");
+ public static final AttributeKey ZONE_ID_KEY = AttributeKey.stringKey("zone");
+
+ // Must come after all other static members
+ public static final TableSchema INSTANCE = new TableSchema();
+
+ public TableSchema() {
+ super(
+ "bigtable_client_raw",
+ ImmutableList.of(
+ BIGTABLE_PROJECT_ID_KEY, INSTANCE_ID_KEY, TABLE_ID_KEY, CLUSTER_ID_KEY, ZONE_ID_KEY));
+ }
+
+ @Override
+ public ProjectName extractProjectName(Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo) {
+ return ProjectName.of(attrs.get(BIGTABLE_PROJECT_ID_KEY));
+ }
+
+ public AttributesBuilder createResourceAttrs(
+ ClientInfo clientInfo, String tableId, ResponseParams clusterInfo) {
+ return Attributes.builder()
+ .put(BIGTABLE_PROJECT_ID_KEY, clientInfo.getInstanceName().getProject())
+ .put(INSTANCE_ID_KEY, clientInfo.getInstanceName().getInstance())
+ .put(TABLE_ID_KEY, tableId)
+ .put(CLUSTER_ID_KEY, clusterInfo.getClusterId())
+ .put(ZONE_ID_KEY, clusterInfo.getZoneId());
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfoTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfoTest.java
new file mode 100644
index 0000000000..283c26f514
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfoTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.bigtable.v2.InstanceName;
+import org.junit.jupiter.api.Test;
+
+class ClientInfoTest {
+ @Test
+ void testName() {
+ ClientInfo clientInfo =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of("fake-project", "fake-instance"))
+ .setAppProfileId("fake-app-profile")
+ .build();
+ assertThat(clientInfo.getClientName()).containsMatch("java-bigtable/\\d+\\.\\d+\\.\\d+.*");
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfoTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfoTest.java
new file mode 100644
index 0000000000..8ab52111aa
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfoTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.opentelemetry.detection.DetectedPlatform;
+import com.google.cloud.opentelemetry.detection.GCPPlatformDetector.SupportedPlatform;
+import com.google.common.base.Function;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class EnvInfoTest {
+ private static final Supplier NULL_HOST = Suppliers.ofInstance(null);
+
+ @SuppressWarnings("UnnecessaryLambda")
+ private static final Function NULL_ENV = (ignored) -> null;
+
+ @Mock private DetectedPlatform detectedPlatform;
+
+ @Test
+ void testUid() {
+ when(detectedPlatform.getSupportedPlatform()).thenReturn(SupportedPlatform.UNKNOWN_PLATFORM);
+
+ EnvInfo info1 = EnvInfo.detect(detectedPlatform, NULL_ENV, NULL_HOST);
+ EnvInfo info2 = EnvInfo.detect(detectedPlatform, NULL_ENV, NULL_HOST);
+
+ assertThat(info1.getUid()).isNotEmpty();
+ assertThat(info2.getUid()).isNotEmpty();
+ assertThat(info1.getUid()).isNotEqualTo(info2.getUid());
+ }
+
+ @Test
+ void testUnknown() {
+ when(detectedPlatform.getSupportedPlatform()).thenReturn(SupportedPlatform.UNKNOWN_PLATFORM);
+ EnvInfo envInfo = EnvInfo.detect(detectedPlatform, NULL_ENV, NULL_HOST);
+ assertThat(envInfo.getHostName()).isEmpty();
+ assertThat(envInfo.getHostId()).isEmpty();
+ assertThat(envInfo.getPlatform()).isEqualTo("unknown");
+ assertThat(envInfo.getRegion()).isEqualTo("global");
+ }
+
+ @Test
+ void testGce() {
+ when(detectedPlatform.getSupportedPlatform())
+ .thenReturn(SupportedPlatform.GOOGLE_COMPUTE_ENGINE);
+ when(detectedPlatform.getProjectId()).thenReturn("my-project");
+ when(detectedPlatform.getAttributes())
+ .thenReturn(
+ ImmutableMap.of(
+ "machine_type", "n2-standard-8",
+ "availability_zone", "us-central1-c",
+ "instance_id", "1234567890",
+ "instance_name", "my-vm-name",
+ "cloud_region", "us-central1",
+ "instance_hostname", "my-vm-name.us-central1-c.c.my-project.google.com.internal"));
+ EnvInfo envInfo = EnvInfo.detect(detectedPlatform, NULL_ENV, NULL_HOST);
+ assertThat(envInfo.getPlatform()).isEqualTo("gcp_compute_engine");
+ assertThat(envInfo.getProject()).isEqualTo("my-project");
+ assertThat(envInfo.getRegion()).isEqualTo("us-central1");
+ assertThat(envInfo.getHostId()).isEqualTo("1234567890");
+ assertThat(envInfo.getHostName()).isEqualTo("my-vm-name");
+ }
+
+ @Test
+ void testGke() {
+ when(detectedPlatform.getSupportedPlatform())
+ .thenReturn(SupportedPlatform.GOOGLE_KUBERNETES_ENGINE);
+ when(detectedPlatform.getProjectId()).thenReturn("my-project");
+ when(detectedPlatform.getAttributes())
+ .thenReturn(
+ ImmutableMap.of(
+ "gke_cluster_name", "my-cluster",
+ "gke_cluster_location", "us-central1",
+ "gke_cluster_location_type", "country-region",
+ "instance_id", "1234567890"));
+ Map env = ImmutableMap.of("HOSTNAME", "my-hostname");
+
+ EnvInfo envInfo = EnvInfo.detect(detectedPlatform, env::get, NULL_HOST);
+ assertThat(envInfo.getPlatform()).isEqualTo("gcp_kubernetes_engine");
+ assertThat(envInfo.getProject()).isEqualTo("my-project");
+ assertThat(envInfo.getRegion()).isEqualTo("us-central1");
+ assertThat(envInfo.getHostId()).isEqualTo("1234567890");
+ assertThat(envInfo.getHostName()).isEqualTo("my-hostname");
+ }
+
+ @Test
+ void testGkeHostanmeFallback() {
+ when(detectedPlatform.getSupportedPlatform())
+ .thenReturn(SupportedPlatform.GOOGLE_KUBERNETES_ENGINE);
+ when(detectedPlatform.getProjectId()).thenReturn("my-project");
+ when(detectedPlatform.getAttributes())
+ .thenReturn(
+ ImmutableMap.of(
+ "gke_cluster_name", "my-cluster",
+ "gke_cluster_location", "us-central1",
+ "gke_cluster_location_type", "country-region",
+ "instance_id", "1234567890"));
+ EnvInfo envInfo = EnvInfo.detect(detectedPlatform, NULL_ENV, () -> "my-hostname");
+ assertThat(envInfo.getPlatform()).isEqualTo("gcp_kubernetes_engine");
+ assertThat(envInfo.getProject()).isEqualTo("my-project");
+ assertThat(envInfo.getRegion()).isEqualTo("us-central1");
+ assertThat(envInfo.getHostId()).isEqualTo("1234567890");
+ assertThat(envInfo.getHostName()).isEqualTo("my-hostname");
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java
new file mode 100644
index 0000000000..f75bb81727
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+
+import com.google.bigtable.v2.PeerInfo.TransportType;
+import org.junit.jupiter.api.Test;
+
+class UtilTest {
+ @Test
+ void ensureAllTransportTypeHaveExpectedPrefix() {
+ for (TransportType type : TransportType.values()) {
+ // Ensure that no variant throws an error
+ Util.transportTypeToString(type);
+ }
+ }
+}
From 5de9d30ad136e784e9e5405c0257cd435acd80c8 Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Wed, 25 Feb 2026 11:04:02 -0500
Subject: [PATCH 14/33] chore: plumb ClientInfo (#2803)
* chore: plumb ClientInfo
Change-Id: I01806d0bbaa6ba95b3a8e66d9b3fa24806c928f0
* wip
Change-Id: If6dd6db3cdc3624ff0a4397fad30fa6a47e935b0
* chore: generate libraries at Tue Feb 24 21:47:05 UTC 2026
* chore: generate libraries at Tue Feb 24 21:49:58 UTC 2026
---------
Co-authored-by: cloud-java-bot
---
.../data/v2/BigtableDataClientFactory.java | 6 +-
.../data/v2/internal/RequestContext.java | 8 ++
.../data/v2/stub/BigtableClientContext.java | 39 ++++-----
.../data/v2/stub/EnhancedBigtableStub.java | 6 +-
.../metrics/BuiltinMetricsTracerFactory.java | 24 +++++-
.../bigtable/data/v2/stub/metrics/Util.java | 46 +++++-----
.../metrics/BuiltinMetricsTracerTest.java | 85 ++++++++++---------
7 files changed, 114 insertions(+), 100 deletions(-)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java
index d73fbe2a12..d529f02eb2 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java
@@ -109,7 +109,8 @@ public BigtableDataClient createDefault() {
try {
BigtableClientContext ctx =
sharedClientContext.createChild(
- sharedClientContext.getInstanceName(), sharedClientContext.getAppProfileId());
+ sharedClientContext.getClientInfo().getInstanceName(),
+ sharedClientContext.getClientInfo().getAppProfileId());
return new BigtableDataClient(new EnhancedBigtableStub(perOpSettings, ctx));
} catch (IOException e) {
@@ -130,7 +131,8 @@ public BigtableDataClient createDefault() {
*/
public BigtableDataClient createForAppProfile(@Nonnull String appProfileId) throws IOException {
BigtableClientContext ctx =
- sharedClientContext.createChild(sharedClientContext.getInstanceName(), appProfileId);
+ sharedClientContext.createChild(
+ sharedClientContext.getClientInfo().getInstanceName(), appProfileId);
return new BigtableDataClient(new EnhancedBigtableStub(perOpSettings, ctx));
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RequestContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RequestContext.java
index fc015186aa..2c3213d003 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RequestContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RequestContext.java
@@ -17,6 +17,7 @@
import com.google.api.core.InternalApi;
import com.google.auto.value.AutoValue;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import java.io.Serializable;
/**
@@ -33,6 +34,13 @@
@AutoValue
public abstract class RequestContext implements Serializable {
+ public static RequestContext create(ClientInfo clientInfo) {
+ return create(
+ clientInfo.getInstanceName().getProject(),
+ clientInfo.getInstanceName().getInstance(),
+ clientInfo.getAppProfileId());
+ }
+
/** Creates a new instance of the {@link RequestContext}. */
public static RequestContext create(String projectId, String instanceId, String appProfileId) {
return new AutoValue_RequestContext(projectId, instanceId, appProfileId);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index c89f368190..511dd61c70 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -29,6 +29,7 @@
import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials;
import com.google.bigtable.v2.InstanceName;
import com.google.cloud.bigtable.data.v2.internal.JwtCredentialsWithAudience;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
import com.google.cloud.bigtable.data.v2.stub.metrics.ChannelPoolMetricsTracer;
import com.google.cloud.bigtable.data.v2.stub.metrics.CompositeTracerFactory;
@@ -63,8 +64,7 @@ public class BigtableClientContext {
private static final Logger logger = Logger.getLogger(BigtableClientContext.class.getName());
private final boolean isChild;
- private final InstanceName instanceName;
- private final String appProfileId;
+ private final ClientInfo clientInfo;
private final ApiTracerFactory userTracerFactory;
@Nullable private final OpenTelemetrySdk builtinOpenTelemetry;
@Nullable private final OpenTelemetry userOpenTelemetry;
@@ -83,8 +83,11 @@ public static BigtableClientContext create(EnhancedBigtableStubSettings settings
public static BigtableClientContext create(
EnhancedBigtableStubSettings settings, Tagger ocTagger, StatsRecorder ocRecorder)
throws IOException {
- InstanceName instanceName = InstanceName.of(settings.getProjectId(), settings.getInstanceId());
- String appProfileId = settings.getAppProfileId();
+ ClientInfo clientInfo =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of(settings.getProjectId(), settings.getInstanceId()))
+ .setAppProfileId(settings.getAppProfileId())
+ .build();
EnhancedBigtableStubSettings.Builder builder = settings.toBuilder();
@@ -184,8 +187,7 @@ public static BigtableClientContext create(
return new BigtableClientContext(
false,
- instanceName,
- appProfileId,
+ clientInfo,
clientContext,
userTracerFactory,
builtinOtel,
@@ -224,8 +226,7 @@ private static void configureGrpcOtel(
private BigtableClientContext(
boolean isChild,
- InstanceName instanceName,
- String appProfileId,
+ ClientInfo clientInfo,
ClientContext clientContext,
ApiTracerFactory userTracerFactory,
@Nullable OpenTelemetrySdk builtinOtel,
@@ -235,8 +236,7 @@ private BigtableClientContext(
ExecutorProvider backgroundExecutorProvider)
throws IOException {
this.isChild = isChild;
- this.instanceName = instanceName;
- this.appProfileId = appProfileId;
+ this.clientInfo = clientInfo;
this.userTracerFactory = userTracerFactory;
this.builtinOpenTelemetry = builtinOtel;
@@ -247,15 +247,15 @@ private BigtableClientContext(
ImmutableList.Builder tracerFactories = ImmutableList.builder();
tracerFactories
- .add(Util.createOCTracingFactory(instanceName, appProfileId))
- .add(Util.createOCMetricsFactory(instanceName, appProfileId, ocTagger, ocRecorder))
+ .add(Util.createOCTracingFactory(clientInfo))
+ .add(Util.createOCMetricsFactory(clientInfo, ocTagger, ocRecorder))
.add(userTracerFactory);
if (builtinOtel != null) {
- tracerFactories.add(Util.createOtelMetricsFactory(builtinOtel, instanceName, appProfileId));
+ tracerFactories.add(Util.createOtelMetricsFactory(builtinOtel, clientInfo));
}
if (userOtel != null) {
- tracerFactories.add(Util.createOtelMetricsFactory(userOtel, instanceName, appProfileId));
+ tracerFactories.add(Util.createOtelMetricsFactory(userOtel, clientInfo));
}
this.clientContext =
@@ -264,12 +264,8 @@ private BigtableClientContext(
.build();
}
- public InstanceName getInstanceName() {
- return instanceName;
- }
-
- public String getAppProfileId() {
- return appProfileId;
+ public ClientInfo getClientInfo() {
+ return clientInfo;
}
@Nullable
@@ -290,8 +286,7 @@ public BigtableClientContext createChild(InstanceName instanceName, String appPr
throws IOException {
return new BigtableClientContext(
true,
- instanceName,
- appProfileId,
+ clientInfo.toBuilder().setInstanceName(instanceName).setAppProfileId(appProfileId).build(),
clientContext,
userTracerFactory,
builtinOpenTelemetry,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
index 6f0ffdc60f..d28d41ecbc 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
@@ -182,11 +182,7 @@ public EnhancedBigtableStub(
ClientOperationSettings perOpSettings, BigtableClientContext clientContext) {
this.perOpSettings = perOpSettings;
this.bigtableClientContext = clientContext;
- this.requestContext =
- RequestContext.create(
- clientContext.getInstanceName().getProject(),
- clientContext.getInstanceName().getInstance(),
- clientContext.getAppProfileId());
+ this.requestContext = RequestContext.create(clientContext.getClientInfo());
this.bulkMutationFlowController =
new FlowController(perOpSettings.bulkMutateRowsSettings.getDynamicFlowControlSettings());
this.bulkMutationDynamicFlowControlStats = new DynamicFlowControlStats();
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
index eb8089b1c6..3d83659a28 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
@@ -16,13 +16,17 @@
package com.google.cloud.bigtable.data.v2.stub.metrics;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APPLICATION_BLOCKING_LATENCIES_NAME;
+import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APP_PROFILE_KEY;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES2_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME;
+import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_BLOCKING_LATENCIES_NAME;
+import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_NAME_KEY;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CONNECTIVITY_ERROR_COUNT_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.FIRST_RESPONSE_LATENCIES_NAME;
+import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.INSTANCE_ID_KEY;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME;
import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME;
@@ -34,6 +38,8 @@
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.BaseApiTracerFactory;
import com.google.api.gax.tracing.SpanName;
+import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleGauge;
@@ -68,14 +74,24 @@ public class BuiltinMetricsTracerFactory extends BaseApiTracerFactory {
private final DoubleHistogram batchWriteFlowControlFactorHistogram;
public static BuiltinMetricsTracerFactory create(
- OpenTelemetry openTelemetry, Attributes attributes) throws IOException {
- return new BuiltinMetricsTracerFactory(openTelemetry, attributes);
+ OpenTelemetry openTelemetry, ClientInfo clientInfo) throws IOException {
+ return new BuiltinMetricsTracerFactory(openTelemetry, clientInfo);
}
- BuiltinMetricsTracerFactory(OpenTelemetry openTelemetry, Attributes attributes) {
- this.attributes = attributes;
+ BuiltinMetricsTracerFactory(OpenTelemetry openTelemetry, ClientInfo clientInfo) {
Meter meter = openTelemetry.getMeter(METER_NAME);
+ this.attributes =
+ Attributes.of(
+ BIGTABLE_PROJECT_ID_KEY,
+ clientInfo.getInstanceName().getProject(),
+ INSTANCE_ID_KEY,
+ clientInfo.getInstanceName().getInstance(),
+ APP_PROFILE_KEY,
+ clientInfo.getAppProfileId(),
+ CLIENT_NAME_KEY,
+ "bigtable-java/" + Version.VERSION);
+
operationLatenciesHistogram =
meter
.histogramBuilder(OPERATION_LATENCIES_NAME)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
index cc341c994e..862e288a4a 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
@@ -15,11 +15,6 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APP_PROFILE_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_NAME_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.INSTANCE_ID_KEY;
-
import com.google.api.core.InternalApi;
import com.google.api.gax.grpc.GaxGrpcProperties;
import com.google.api.gax.rpc.ApiCallContext;
@@ -45,6 +40,7 @@
import com.google.bigtable.v2.TableName;
import com.google.cloud.bigtable.Version;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
@@ -58,7 +54,6 @@
import io.opencensus.tags.TagValue;
import io.opencensus.tags.Tagger;
import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.InstrumentSelector;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
@@ -241,14 +236,18 @@ public static String formatZoneIdMetricLabel(
.orElse("global");
}
- public static ApiTracerFactory createOCTracingFactory(
- InstanceName instanceName, String appProfileId) {
+ public static ApiTracerFactory createOCTracingFactory(ClientInfo clientInfo) {
return new OpencensusTracerFactory(
ImmutableMap.builder()
// Annotate traces with the same tags as metrics
- .put(RpcMeasureConstants.BIGTABLE_PROJECT_ID.getName(), instanceName.getProject())
- .put(RpcMeasureConstants.BIGTABLE_INSTANCE_ID.getName(), instanceName.getInstance())
- .put(RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID.getName(), appProfileId)
+ .put(
+ RpcMeasureConstants.BIGTABLE_PROJECT_ID.getName(),
+ clientInfo.getInstanceName().getProject())
+ .put(
+ RpcMeasureConstants.BIGTABLE_INSTANCE_ID.getName(),
+ clientInfo.getInstanceName().getInstance())
+ .put(
+ RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID.getName(), clientInfo.getAppProfileId())
// Also annotate traces with library versions
.put("gax", GaxGrpcProperties.getGaxGrpcVersion())
.put("grpc", GaxGrpcProperties.getGrpcVersion())
@@ -257,32 +256,25 @@ public static ApiTracerFactory createOCTracingFactory(
}
public static ApiTracerFactory createOCMetricsFactory(
- InstanceName instanceName, String appProfileId, Tagger tagger, StatsRecorder stats) {
+ ClientInfo clientInfo, Tagger tagger, StatsRecorder stats) {
ImmutableMap attributes =
ImmutableMap.builder()
.put(
- RpcMeasureConstants.BIGTABLE_PROJECT_ID, TagValue.create(instanceName.getProject()))
+ RpcMeasureConstants.BIGTABLE_PROJECT_ID,
+ TagValue.create(clientInfo.getInstanceName().getProject()))
.put(
RpcMeasureConstants.BIGTABLE_INSTANCE_ID,
- TagValue.create(instanceName.getInstance()))
- .put(RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID, TagValue.create(appProfileId))
+ TagValue.create(clientInfo.getInstanceName().getInstance()))
+ .put(
+ RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID,
+ TagValue.create(clientInfo.getAppProfileId()))
.build();
return MetricsTracerFactory.create(tagger, stats, attributes);
}
public static BuiltinMetricsTracerFactory createOtelMetricsFactory(
- OpenTelemetry otel, InstanceName instanceName, String appProfileId) throws IOException {
- Attributes attributes =
- Attributes.of(
- BIGTABLE_PROJECT_ID_KEY,
- instanceName.getProject(),
- INSTANCE_ID_KEY,
- instanceName.getInstance(),
- APP_PROFILE_KEY,
- appProfileId,
- CLIENT_NAME_KEY,
- "bigtable-java/" + Version.VERSION);
- return BuiltinMetricsTracerFactory.create(otel, attributes);
+ OpenTelemetry otel, ClientInfo clientInfo) {
+ return new BuiltinMetricsTracerFactory(otel, clientInfo);
}
}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
index 1ffccab7dd..2aaea4a5e5 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
@@ -54,6 +54,7 @@
import com.google.api.gax.rpc.ResponseObserver;
import com.google.api.gax.rpc.StreamController;
import com.google.bigtable.v2.BigtableGrpc;
+import com.google.bigtable.v2.InstanceName;
import com.google.bigtable.v2.MutateRowRequest;
import com.google.bigtable.v2.MutateRowResponse;
import com.google.bigtable.v2.MutateRowsRequest;
@@ -64,6 +65,7 @@
import com.google.cloud.bigtable.Version;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
import com.google.cloud.bigtable.data.v2.FakeServiceBuilder;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId;
import com.google.cloud.bigtable.data.v2.models.Query;
import com.google.cloud.bigtable.data.v2.models.Row;
@@ -163,7 +165,17 @@ public class BuiltinMetricsTracerTest {
private int batchElementCount = 2;
- private Attributes baseAttributes;
+ private ClientInfo clientInfo =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of(PROJECT_ID, INSTANCE_ID))
+ .setAppProfileId(APP_PROFILE_ID)
+ .build();
+ private Attributes expectedBaseAttributes =
+ Attributes.builder()
+ .put(BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY, PROJECT_ID)
+ .put(BuiltinMetricsConstants.INSTANCE_ID_KEY, INSTANCE_ID)
+ .put(BuiltinMetricsConstants.APP_PROFILE_KEY, APP_PROFILE_ID)
+ .build();
private InMemoryMetricReader metricReader;
@@ -175,13 +187,6 @@ public class BuiltinMetricsTracerTest {
public void setUp() throws Exception {
metricReader = InMemoryMetricReader.create();
- baseAttributes =
- Attributes.builder()
- .put(BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY, PROJECT_ID)
- .put(BuiltinMetricsConstants.INSTANCE_ID_KEY, INSTANCE_ID)
- .put(BuiltinMetricsConstants.APP_PROFILE_KEY, APP_PROFILE_ID)
- .build();
-
SdkMeterProviderBuilder meterProvider =
SdkMeterProvider.builder().registerMetricReader(metricReader);
@@ -192,7 +197,7 @@ public void setUp() throws Exception {
OpenTelemetrySdk otel =
OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
- BuiltinMetricsTracerFactory facotry = BuiltinMetricsTracerFactory.create(otel, baseAttributes);
+ BuiltinMetricsTracerFactory facotry = new BuiltinMetricsTracerFactory(otel, clientInfo);
// Add an interceptor to add server-timing in headers
ServerInterceptor trailersInterceptor =
@@ -302,7 +307,7 @@ public void testReadRowsOperationLatencies() {
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -327,7 +332,7 @@ public void testReadRowsOperationLatenciesOnAuthorizedView() {
long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -372,7 +377,7 @@ public void onComplete() {}
});
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, FIRST_RESPONSE_TABLE_ID)
.put(ZONE_ID_KEY, ZONE)
@@ -392,7 +397,7 @@ public void testGfeMetrics() {
Lists.newArrayList(stub.readRowsCallable().call(Query.create(TABLE)));
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -409,7 +414,7 @@ public void testGfeMetrics() {
MetricData connectivityErrorCountMetricData =
getMetricData(metricReader, CONNECTIVITY_ERROR_COUNT_NAME);
Attributes expected1 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "UNAVAILABLE")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, "global")
@@ -418,7 +423,7 @@ public void testGfeMetrics() {
.put(CLIENT_NAME_KEY, CLIENT_NAME)
.build();
Attributes expected2 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -473,7 +478,7 @@ public void onComplete() {
getMetricData(metricReader, APPLICATION_BLOCKING_LATENCIES_NAME);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
.put(CLUSTER_ID_KEY, CLUSTER)
@@ -508,7 +513,7 @@ public void testReadRowsApplicationLatencyWithManualFlowControl() throws Excepti
getMetricData(metricReader, APPLICATION_BLOCKING_LATENCIES_NAME);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
.put(CLUSTER_ID_KEY, CLUSTER)
@@ -537,7 +542,7 @@ public void testRetryCount() throws InterruptedException {
MetricData metricData = getMetricData(metricReader, RETRY_COUNT_NAME);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
.put(CLUSTER_ID_KEY, CLUSTER)
@@ -559,7 +564,7 @@ public void testMutateRowAttemptsTagValues() throws InterruptedException {
MetricData metricData = getMetricData(metricReader, ATTEMPT_LATENCIES_NAME);
Attributes expected1 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "UNAVAILABLE")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, "global")
@@ -570,7 +575,7 @@ public void testMutateRowAttemptsTagValues() throws InterruptedException {
.build();
Attributes expected2 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -598,7 +603,7 @@ public void testMutateRowsPartialError() throws InterruptedException {
MetricData metricData = getMetricData(metricReader, ATTEMPT_LATENCIES_NAME);
Attributes expected =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -626,7 +631,7 @@ public void testMutateRowsRpcError() {
MetricData metricData = getMetricData(metricReader, ATTEMPT_LATENCIES_NAME);
Attributes expected =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "NOT_FOUND")
.put(TABLE_ID_KEY, BAD_TABLE_ID)
.put(ZONE_ID_KEY, "global")
@@ -646,7 +651,7 @@ public void testReadRowsAttemptsTagValues() {
MetricData metricData = getMetricData(metricReader, ATTEMPT_LATENCIES_NAME);
Attributes expected1 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "UNAVAILABLE")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, "global")
@@ -657,7 +662,7 @@ public void testReadRowsAttemptsTagValues() {
.build();
Attributes expected2 =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -686,7 +691,7 @@ public void testBatchBlockingLatencies() throws InterruptedException {
MetricData applicationLatency = getMetricData(metricReader, CLIENT_BLOCKING_LATENCIES_NAME);
Attributes expectedAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
.put(CLUSTER_ID_KEY, CLUSTER)
@@ -712,7 +717,7 @@ public void testQueuedOnChannelServerStreamLatencies() throws Exception {
MetricData clientLatency = getMetricData(metricReader, CLIENT_BLOCKING_LATENCIES_NAME);
Attributes attributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(CLUSTER_ID_KEY, CLUSTER)
.put(ZONE_ID_KEY, ZONE)
@@ -739,7 +744,7 @@ public void testQueuedOnChannelUnaryLatencies() throws Exception {
MetricData clientLatency = getMetricData(metricReader, CLIENT_BLOCKING_LATENCIES_NAME);
Attributes attributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(TABLE_ID_KEY, TABLE)
.put(CLUSTER_ID_KEY, CLUSTER)
.put(ZONE_ID_KEY, ZONE)
@@ -765,7 +770,7 @@ public void testPermanentFailure() {
MetricData attemptLatency = getMetricData(metricReader, ATTEMPT_LATENCIES_NAME);
Attributes expected =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "NOT_FOUND")
.put(TABLE_ID_KEY, BAD_TABLE_ID)
.put(CLUSTER_ID_KEY, "")
@@ -787,7 +792,7 @@ public void testRemainingDeadline() {
MetricData deadlineMetric = getMetricData(metricReader, REMAINING_DEADLINE_NAME);
Attributes retryAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "UNAVAILABLE")
.put(TABLE_ID_KEY, TABLE)
.put(METHOD_KEY, "Bigtable.ReadRows")
@@ -807,7 +812,7 @@ public void testRemainingDeadline() {
assertThat(retryRemainingDeadline).isEqualTo(9000);
Attributes okAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(STATUS_KEY, "OK")
.put(TABLE_ID_KEY, TABLE)
.put(ZONE_ID_KEY, ZONE)
@@ -840,14 +845,14 @@ public void testBatchWriteFlowControlTargetQpsIncreased() throws InterruptedExce
MetricData targetQpsMetric =
getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME);
Attributes targetQpsAttributes =
- baseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
+ expectedBaseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
double actual_qps = getAggregatedDoubleValue(targetQpsMetric, targetQpsAttributes);
double expected_qps = 12;
assertThat(expected_qps).isEqualTo(actual_qps);
MetricData factorMetric = getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME);
Attributes factorAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(METHOD_KEY, "Bigtable.MutateRows")
.put(APPLIED_KEY, true)
.put(STATUS_KEY, "OK")
@@ -870,14 +875,14 @@ public void testBatchWriteFlowControlTargetQpsDecreased() throws InterruptedExce
MetricData targetQpsMetric =
getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME);
Attributes targetQpsAttributes =
- baseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
+ expectedBaseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
double actual_qps = getAggregatedDoubleValue(targetQpsMetric, targetQpsAttributes);
double expected_qps = 8.0;
assertThat(expected_qps).isEqualTo(actual_qps);
MetricData factorMetric = getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME);
Attributes factorAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(METHOD_KEY, "Bigtable.MutateRows")
.put(APPLIED_KEY, true)
.put(STATUS_KEY, "OK")
@@ -900,7 +905,7 @@ public void testBatchWriteFlowControlTargetQpsCappedOnMaxFactor() throws Interru
MetricData targetQpsMetric =
getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME);
Attributes targetQpsAttributes =
- baseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
+ expectedBaseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
double actual_qps = getAggregatedDoubleValue(targetQpsMetric, targetQpsAttributes);
// Factor is 1.8 but capped at 1.3 so updated QPS is 13.
double expected_qps = 13;
@@ -908,7 +913,7 @@ public void testBatchWriteFlowControlTargetQpsCappedOnMaxFactor() throws Interru
MetricData factorMetric = getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME);
Attributes factorAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(METHOD_KEY, "Bigtable.MutateRows")
.put(APPLIED_KEY, true)
.put(STATUS_KEY, "OK")
@@ -932,7 +937,7 @@ public void testBatchWriteFlowControlTargetQpsCappedOnMinFactor() throws Interru
MetricData targetQpsMetric =
getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME);
Attributes targetQpsAttributes =
- baseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
+ expectedBaseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
double actual_qps = getAggregatedDoubleValue(targetQpsMetric, targetQpsAttributes);
// Factor is 0.5 but capped at 0.7 so updated QPS is 7.
double expected_qps = 7;
@@ -940,7 +945,7 @@ public void testBatchWriteFlowControlTargetQpsCappedOnMinFactor() throws Interru
MetricData factorMetric = getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME);
Attributes factorAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(METHOD_KEY, "Bigtable.MutateRows")
.put(APPLIED_KEY, true)
.put(STATUS_KEY, "OK")
@@ -965,7 +970,7 @@ public void testBatchWriteFlowControlTargetQpsDecreasedForError() throws Interru
MetricData targetQpsMetric =
getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME);
Attributes targetQpsAttributes =
- baseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
+ expectedBaseAttributes.toBuilder().put(METHOD_KEY, "Bigtable.MutateRows").build();
double actual_qps = getAggregatedDoubleValue(targetQpsMetric, targetQpsAttributes);
// On error, min factor is applied.
double expected_qps = 7;
@@ -973,7 +978,7 @@ public void testBatchWriteFlowControlTargetQpsDecreasedForError() throws Interru
MetricData factorMetric = getMetricData(metricReader, BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME);
Attributes factorAttributes =
- baseAttributes.toBuilder()
+ expectedBaseAttributes.toBuilder()
.put(METHOD_KEY, "Bigtable.MutateRows")
.put(APPLIED_KEY, true)
.put(STATUS_KEY, "UNAVAILABLE")
From a915fb74df199b2ba4db130a5cc342ab041c0aa5 Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Wed, 25 Feb 2026 12:50:28 -0500
Subject: [PATCH 15/33] chore: internalize converters in the metrics exporter
(#2804)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Rollback plan is reviewed and LGTMed
- [ ] All new data plane features have a completed end to end testing plan
Fixes # ☕️
If you write sample code, please follow the [samples format](
https://togithub.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
---
.../data/v2/stub/BigtableClientContext.java | 3 +--
.../BigtableCloudMonitoringExporter.java | 11 ++++++++-
.../stub/metrics/BigtableExporterUtils.java | 15 ++++++------
.../bigtable/data/v2/stub/metrics/Util.java | 23 ++++---------------
4 files changed, 23 insertions(+), 29 deletions(-)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index 511dd61c70..6f005a0408 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -124,8 +124,7 @@ public static BigtableClientContext create(
if (settings.areInternalMetricsEnabled()) {
builtinOtel =
Util.createBuiltinOtel(
- InstanceName.of(settings.getProjectId(), settings.getInstanceId()),
- settings.getAppProfileId(),
+ clientInfo,
credentials,
settings.getMetricsEndpoint(),
universeDomain,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
index 9043a351ab..67cc5ba134 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
@@ -39,11 +39,13 @@
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.rpc.PermissionDeniedException;
import com.google.auth.Credentials;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.monitoring.v3.MetricServiceClient;
import com.google.cloud.monitoring.v3.MetricServiceSettings;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -112,10 +114,10 @@ public final class BigtableCloudMonitoringExporter implements MetricExporter {
private final AtomicBoolean exportFailureLogged = new AtomicBoolean(false);
static BigtableCloudMonitoringExporter create(
+ ClientInfo clientInfo,
@Nullable Credentials credentials,
@Nullable String endpoint,
String universeDomain,
- List converters,
@Nullable ScheduledExecutorService executorService)
throws IOException {
Preconditions.checkNotNull(universeDomain);
@@ -152,6 +154,13 @@ static BigtableCloudMonitoringExporter create(
// it as not retried for now.
settingsBuilder.createServiceTimeSeriesSettings().setSimpleTimeoutNoRetriesDuration(timeout);
+ ImmutableList converters =
+ ImmutableList.of(
+ new PublicTimeSeriesConverter(),
+ new InternalTimeSeriesConverter(
+ Suppliers.memoize(
+ () -> BigtableExporterUtils.createInternalMonitoredResource(clientInfo))));
+
return new BigtableCloudMonitoringExporter(
MetricServiceClient.create(settingsBuilder.build()), converters);
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
index 3b95ed1819..f27c2b56f8 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableExporterUtils.java
@@ -38,8 +38,8 @@
import com.google.api.Distribution;
import com.google.api.Metric;
import com.google.api.MonitoredResource;
-import com.google.bigtable.v2.InstanceName;
import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.opentelemetry.detection.AttributeKeys;
import com.google.cloud.opentelemetry.detection.DetectedPlatform;
import com.google.cloud.opentelemetry.detection.GCPPlatformDetector;
@@ -182,10 +182,9 @@ static List convertToApplicationResourceTimeSeries(
}
@Nullable
- static MonitoredResource createInternalMonitoredResource(
- InstanceName instanceName, String appProfileId) {
+ static MonitoredResource createInternalMonitoredResource(ClientInfo clientInfo) {
try {
- MonitoredResource monitoredResource = detectResource(instanceName, appProfileId);
+ MonitoredResource monitoredResource = detectResource(clientInfo);
logger.log(Level.FINE, "Internal metrics monitored resource: %s", monitoredResource);
return monitoredResource;
} catch (Exception e) {
@@ -198,7 +197,7 @@ static MonitoredResource createInternalMonitoredResource(
}
@Nullable
- private static MonitoredResource detectResource(InstanceName instanceName, String appProfileId) {
+ private static MonitoredResource detectResource(ClientInfo clientInfo) {
GCPPlatformDetector detector = GCPPlatformDetector.DEFAULT_INSTANCE;
DetectedPlatform detectedPlatform = detector.detectPlatform();
@@ -245,9 +244,9 @@ private static MonitoredResource detectResource(InstanceName instanceName, Strin
return MonitoredResource.newBuilder()
.setType("bigtable_client")
- .putLabels("project_id", instanceName.getProject())
- .putLabels("instance", instanceName.getInstance())
- .putLabels("app_profile", appProfileId)
+ .putLabels("project_id", clientInfo.getInstanceName().getProject())
+ .putLabels("instance", clientInfo.getInstanceName().getInstance())
+ .putLabels("app_profile", clientInfo.getAppProfileId())
.putLabels("client_project", detectedPlatform.getProjectId())
.putLabels("region", region)
.putLabels("cloud_platform", cloud_platform)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
index 862e288a4a..f37a4191b1 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
@@ -27,7 +27,6 @@
import com.google.bigtable.v2.AuthorizedViewName;
import com.google.bigtable.v2.CheckAndMutateRowRequest;
import com.google.bigtable.v2.GenerateInitialChangeStreamPartitionsRequest;
-import com.google.bigtable.v2.InstanceName;
import com.google.bigtable.v2.MaterializedViewName;
import com.google.bigtable.v2.MutateRowRequest;
import com.google.bigtable.v2.MutateRowsRequest;
@@ -42,8 +41,6 @@
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
-import com.google.common.base.Suppliers;
-import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.grpc.Metadata;
import io.grpc.Status;
@@ -162,8 +159,7 @@ static Map> createStatsHeaders(ApiCallContext apiCallContex
}
public static OpenTelemetrySdk createBuiltinOtel(
- InstanceName instanceName,
- String appProfileId,
+ ClientInfo clientInfo,
@Nullable Credentials defaultCredentials,
@Nullable String metricsEndpoint,
String universeDomain,
@@ -189,17 +185,7 @@ public static OpenTelemetrySdk createBuiltinOtel(
MetricExporter publicExporter =
BigtableCloudMonitoringExporter.create(
- credentials,
- metricsEndpoint,
- universeDomain,
- ImmutableList.of(
- new BigtableCloudMonitoringExporter.PublicTimeSeriesConverter(),
- new BigtableCloudMonitoringExporter.InternalTimeSeriesConverter(
- Suppliers.memoize(
- () ->
- BigtableExporterUtils.createInternalMonitoredResource(
- instanceName, appProfileId)))),
- executor);
+ clientInfo, credentials, metricsEndpoint, universeDomain, executor);
PeriodicMetricReaderBuilder readerBuilder =
PeriodicMetricReader.builder(publicExporter).setExecutor(executor);
meterProvider.registerMetricReader(readerBuilder.build());
@@ -274,7 +260,8 @@ public static ApiTracerFactory createOCMetricsFactory(
}
public static BuiltinMetricsTracerFactory createOtelMetricsFactory(
- OpenTelemetry otel, ClientInfo clientInfo) {
- return new BuiltinMetricsTracerFactory(otel, clientInfo);
+ OpenTelemetry otel, ClientInfo clientInfo) throws IOException {
+
+ return BuiltinMetricsTracerFactory.create(otel, clientInfo);
}
}
From be51db4ba3b817aa7416a64de6d5f2625244b29c Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Wed, 25 Feb 2026 14:30:29 -0500
Subject: [PATCH 16/33] chore: clean up Util to return Status.Code instead of
string (#2805)
This in prep for migrating to the strongly typed metrics
---
.../v2/stub/metrics/BuiltinMetricsTracer.java | 27 ++++++-------
.../data/v2/stub/metrics/MetricsTracer.java | 4 +-
.../bigtable/data/v2/stub/metrics/Util.java | 38 +++++++++----------
.../data/v2/stub/metrics/UtilTest.java | 4 +-
4 files changed, 36 insertions(+), 37 deletions(-)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
index 546ea41c9f..74d09f5834 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
@@ -40,6 +40,7 @@
import com.google.common.base.Stopwatch;
import com.google.common.math.IntMath;
import io.grpc.Deadline;
+import io.grpc.Status;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.DoubleGauge;
import io.opentelemetry.api.metrics.DoubleHistogram;
@@ -336,9 +337,9 @@ public void disableFlowControl() {
flowControlIsDisabled = true;
}
- private void recordOperationCompletion(@Nullable Throwable status) {
+ private void recordOperationCompletion(@Nullable Throwable throwable) {
if (operationFinishedEarly.get()) {
- status = null; // force an ok
+ throwable = null; // force an ok
}
if (!opFinished.compareAndSet(false, true)) {
@@ -347,7 +348,7 @@ private void recordOperationCompletion(@Nullable Throwable status) {
long operationLatencyNano = operationTimer.elapsed(TimeUnit.NANOSECONDS);
boolean isStreaming = operationType == OperationType.ServerStreaming;
- String statusStr = extractStatus(status);
+ Status.Code code = extractStatus(throwable);
// Publish metric data with all the attributes. The attributes get filtered in
// BuiltinMetricsConstants when we construct the views.
@@ -359,7 +360,7 @@ private void recordOperationCompletion(@Nullable Throwable status) {
.put(METHOD_KEY, spanName.toString())
.put(CLIENT_NAME_KEY, NAME)
.put(STREAMING_KEY, isStreaming)
- .put(STATUS_KEY, statusStr)
+ .put(STATUS_KEY, code.name())
.build();
// Only record when retry count is greater than 0 so the retry
@@ -381,9 +382,9 @@ private void recordOperationCompletion(@Nullable Throwable status) {
}
}
- private void recordAttemptCompletion(@Nullable Throwable status) {
+ private void recordAttemptCompletion(@Nullable Throwable throwable) {
if (operationFinishedEarly.get()) {
- status = null; // force an ok
+ throwable = null; // force an ok
}
// If the attempt failed, the time spent in retry should be counted in application latency.
// Stop the stopwatch and decrement requestLeft.
@@ -397,14 +398,14 @@ private void recordAttemptCompletion(@Nullable Throwable status) {
boolean isStreaming = operationType == OperationType.ServerStreaming;
- // Patch the status until it's fixed in gax. When an attempt failed,
+ // Patch the throwable until it's fixed in gax. When an attempt failed,
// it'll throw a ServerStreamingAttemptException. Unwrap the exception
// so it could get processed by extractStatus
- if (status instanceof ServerStreamingAttemptException) {
- status = status.getCause();
+ if (throwable instanceof ServerStreamingAttemptException) {
+ throwable = throwable.getCause();
}
- String statusStr = extractStatus(status);
+ Status.Code code = extractStatus(throwable);
Attributes attributes =
baseAttributes.toBuilder()
@@ -414,7 +415,7 @@ private void recordAttemptCompletion(@Nullable Throwable status) {
.put(METHOD_KEY, spanName.toString())
.put(CLIENT_NAME_KEY, NAME)
.put(STREAMING_KEY, isStreaming)
- .put(STATUS_KEY, statusStr)
+ .put(STATUS_KEY, code.name())
.build();
totalClientBlockingTime.addAndGet(grpcMessageSentDelay.get());
@@ -477,11 +478,11 @@ public void setBatchWriteFlowControlTargetQps(double targetQps) {
@Override
public void addBatchWriteFlowControlFactor(
- double factor, @Nullable Throwable status, boolean applied) {
+ double factor, @Nullable Throwable throwable, boolean applied) {
Attributes attributes =
baseAttributes.toBuilder()
.put(METHOD_KEY, spanName.toString())
- .put(STATUS_KEY, extractStatus(status))
+ .put(STATUS_KEY, extractStatus(throwable).name())
.put(APPLIED_KEY, applied)
.build();
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
index 53b4ca87a8..73f54ad810 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
@@ -133,7 +133,7 @@ private void recordOperationCompletion(@Nullable Throwable throwable) {
newTagCtxBuilder()
.putLocal(
RpcMeasureConstants.BIGTABLE_STATUS,
- TagValue.create(Util.extractStatus(throwable)));
+ TagValue.create(Util.extractStatus(throwable).name()));
measures.record(tagCtx.build());
}
@@ -216,7 +216,7 @@ private void recordAttemptCompletion(@Nullable Throwable throwable) {
newTagCtxBuilder()
.putLocal(
RpcMeasureConstants.BIGTABLE_STATUS,
- TagValue.create(Util.extractStatus(throwable)));
+ TagValue.create(Util.extractStatus(throwable).name()));
measures.record(tagCtx.build());
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
index f37a4191b1..dc6155f88a 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
@@ -17,10 +17,9 @@
import com.google.api.core.InternalApi;
import com.google.api.gax.grpc.GaxGrpcProperties;
+import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ApiException;
-import com.google.api.gax.rpc.StatusCode;
-import com.google.api.gax.rpc.StatusCode.Code;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.OpencensusTracerFactory;
import com.google.auth.Credentials;
@@ -44,8 +43,6 @@
import com.google.common.collect.ImmutableMap;
import io.grpc.Metadata;
import io.grpc.Status;
-import io.grpc.StatusException;
-import io.grpc.StatusRuntimeException;
import io.opencensus.stats.StatsRecorder;
import io.opencensus.tags.TagKey;
import io.opencensus.tags.TagValue;
@@ -79,25 +76,26 @@ public class Util {
static final Metadata.Key ATTEMPT_EPOCH_KEY =
Metadata.Key.of("bigtable-client-attempt-epoch-usec", Metadata.ASCII_STRING_MARSHALLER);
- /** Convert an exception into a value that can be used to create an OpenCensus tag value. */
- public static String extractStatus(@Nullable Throwable error) {
- final String statusString;
-
+ public static Status.Code extractStatus(@Nullable Throwable error) {
if (error == null) {
- return StatusCode.Code.OK.toString();
- } else if (error instanceof CancellationException) {
- statusString = Status.Code.CANCELLED.toString();
- } else if (error instanceof ApiException) {
- statusString = ((ApiException) error).getStatusCode().getCode().toString();
- } else if (error instanceof StatusRuntimeException) {
- statusString = ((StatusRuntimeException) error).getStatus().getCode().toString();
- } else if (error instanceof StatusException) {
- statusString = ((StatusException) error).getStatus().getCode().toString();
- } else {
- statusString = Code.UNKNOWN.toString();
+ return Status.Code.OK;
+ }
+ // Handle java CancellationException as if it was a gax CancelledException
+ if (error instanceof CancellationException) {
+ return Status.Code.CANCELLED;
+ }
+ if (error instanceof ApiException) {
+ ApiException apiException = (ApiException) error;
+ if (apiException.getStatusCode() instanceof GrpcStatusCode) {
+ return ((GrpcStatusCode) apiException.getStatusCode()).getTransportCode();
+ }
}
- return statusString;
+ Status s = Status.fromThrowable(error);
+ if (s != null) {
+ return s.getCode();
+ }
+ return Status.Code.UNKNOWN;
}
static String extractTableId(Object request) {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/UtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/UtilTest.java
index 824d8be307..f1e98e03a4 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/UtilTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/UtilTest.java
@@ -29,7 +29,7 @@
public class UtilTest {
@Test
public void testOk() {
- TagValue tagValue = TagValue.create(Util.extractStatus((Throwable) null));
+ TagValue tagValue = TagValue.create(Util.extractStatus(null).name());
assertThat(tagValue.asString()).isEqualTo("OK");
}
@@ -38,7 +38,7 @@ public void testError() {
DeadlineExceededException error =
new DeadlineExceededException(
"Deadline exceeded", null, GrpcStatusCode.of(Status.Code.DEADLINE_EXCEEDED), true);
- TagValue tagValue = TagValue.create(Util.extractStatus(error));
+ TagValue tagValue = TagValue.create(Util.extractStatus(error).name());
assertThat(tagValue.asString()).isEqualTo("DEADLINE_EXCEEDED");
}
}
From b84ff59f361c6f3bf531ae41da0e7731bfad35fa Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Wed, 25 Feb 2026 20:22:36 -0500
Subject: [PATCH 17/33] =?UTF-8?q?chore:=20start=20pulling=20all=20metrics?=
=?UTF-8?q?=20related=20things=20together=20under=20Metric=E2=80=A6=20(#28?=
=?UTF-8?q?07)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
…sImpl
Change-Id: I78a5d0fed976381bd952ca405c36ce22e8c6178f
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Rollback plan is reviewed and LGTMed
- [ ] All new data plane features have a completed end to end testing plan
Fixes # ☕️
If you write sample code, please follow the [samples format](
https://togithub.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
---
.../data/v2/internal/csm/Metrics.java | 38 +++
.../data/v2/internal/csm/MetricsImpl.java | 236 ++++++++++++++++++
.../data/v2/stub/BigtableClientContext.java | 132 +++-------
.../BigtableCloudMonitoringExporter.java | 2 +-
.../metrics/BuiltinMetricsTracerFactory.java | 3 +-
.../bigtable/data/v2/stub/metrics/Util.java | 100 --------
6 files changed, 308 insertions(+), 203 deletions(-)
create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java
create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java
new file mode 100644
index 0000000000..d5e1dbf5b3
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/Metrics.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm;
+
+import com.google.api.gax.tracing.ApiTracerFactory;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.stub.metrics.ChannelPoolMetricsTracer;
+import io.grpc.ManagedChannelBuilder;
+import java.io.Closeable;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+public interface Metrics extends Closeable {
+ ApiTracerFactory createTracerFactory(ClientInfo clientInfo) throws IOException;
+
+ > T configureGrpcChannel(T channelBuilder);
+
+ @Nullable
+ ChannelPoolMetricsTracer getChannelPoolMetricsTracer();
+
+ void start();
+
+ @Override
+ void close();
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
new file mode 100644
index 0000000000..139ea6727e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.internal.csm;
+
+import com.google.api.gax.grpc.GaxGrpcProperties;
+import com.google.api.gax.tracing.ApiTracerFactory;
+import com.google.api.gax.tracing.OpencensusTracerFactory;
+import com.google.auth.Credentials;
+import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableCloudMonitoringExporter;
+import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
+import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsTracerFactory;
+import com.google.cloud.bigtable.data.v2.stub.metrics.ChannelPoolMetricsTracer;
+import com.google.cloud.bigtable.data.v2.stub.metrics.CompositeTracerFactory;
+import com.google.cloud.bigtable.data.v2.stub.metrics.MetricsTracerFactory;
+import com.google.cloud.bigtable.data.v2.stub.metrics.RpcMeasureConstants;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.opentelemetry.GrpcOpenTelemetry;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.InstrumentSelector;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
+import io.opentelemetry.sdk.metrics.View;
+import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
+import io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import javax.annotation.Nullable;
+
+public class MetricsImpl implements Metrics, Closeable {
+ private final ApiTracerFactory userTracerFactory;
+ private final @Nullable OpenTelemetrySdk internalOtel;
+ private final @Nullable OpenTelemetry userOtel;
+ private final ScheduledExecutorService executor;
+ private final Tagger ocTagger;
+ private final StatsRecorder ocRecorder;
+
+ @Nullable private final GrpcOpenTelemetry grpcOtel;
+ @Nullable private final ChannelPoolMetricsTracer channelPoolMetricsTracer;
+ private final List> tasks = new ArrayList<>();
+
+ public MetricsImpl(
+ ApiTracerFactory userTracerFactory,
+ @Nullable OpenTelemetrySdk internalOtel,
+ @Nullable OpenTelemetry userOtel,
+ Tagger ocTagger,
+ StatsRecorder ocRecorder,
+ ScheduledExecutorService executor) {
+ this.userTracerFactory = Preconditions.checkNotNull(userTracerFactory);
+
+ this.internalOtel = internalOtel;
+ this.userOtel = userOtel;
+
+ this.ocTagger = ocTagger;
+ this.ocRecorder = ocRecorder;
+
+ this.executor = executor;
+
+ if (internalOtel != null) {
+ this.grpcOtel =
+ GrpcOpenTelemetry.newBuilder()
+ .sdk(internalOtel)
+ .addOptionalLabel("grpc.lb.locality")
+ // Disable default grpc metrics
+ .disableAllMetrics()
+ // Enable specific grpc metrics
+ .enableMetrics(BuiltinMetricsConstants.GRPC_METRICS.keySet())
+ .build();
+ } else {
+ this.grpcOtel = null;
+ }
+
+ if (internalOtel != null) {
+ this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalOtel);
+ } else {
+ this.channelPoolMetricsTracer = null;
+ }
+ }
+
+ @Override
+ public void close() {
+ for (ScheduledFuture> task : tasks) {
+ task.cancel(false);
+ }
+ if (internalOtel != null) {
+ internalOtel.close();
+ }
+ }
+
+ @Override
+ public void start() {
+ if (channelPoolMetricsTracer != null) {
+ tasks.add(channelPoolMetricsTracer.start(executor));
+ }
+ }
+
+ @Override
+ public > T configureGrpcChannel(T channelBuilder) {
+ if (grpcOtel == null) {
+ return channelBuilder;
+ }
+ grpcOtel.configureChannelBuilder(channelBuilder);
+ return channelBuilder;
+ }
+
+ @Override
+ public ApiTracerFactory createTracerFactory(ClientInfo clientInfo) {
+ ImmutableList.Builder tracerFactories = ImmutableList.builder();
+ tracerFactories
+ .add(createOCTracingFactory(clientInfo))
+ .add(createOCMetricsFactory(clientInfo, ocTagger, ocRecorder))
+ .add(userTracerFactory);
+
+ if (internalOtel != null) {
+ tracerFactories.add(createOtelMetricsFactory(internalOtel, clientInfo));
+ }
+ if (userOtel != null) {
+ tracerFactories.add(createOtelMetricsFactory(userOtel, clientInfo));
+ }
+
+ return new CompositeTracerFactory(tracerFactories.build());
+ }
+
+ @Override
+ @Nullable
+ public ChannelPoolMetricsTracer getChannelPoolMetricsTracer() {
+ return channelPoolMetricsTracer;
+ }
+
+ public static OpenTelemetrySdk createBuiltinOtel(
+ ClientInfo clientInfo,
+ @Nullable Credentials defaultCredentials,
+ @Nullable String metricsEndpoint,
+ String universeDomain,
+ ScheduledExecutorService executor)
+ throws IOException {
+
+ Credentials credentials =
+ BigtableDataSettings.getMetricsCredentials() != null
+ ? BigtableDataSettings.getMetricsCredentials()
+ : defaultCredentials;
+
+ SdkMeterProviderBuilder meterProvider = SdkMeterProvider.builder();
+
+ for (Map.Entry entry :
+ BuiltinMetricsConstants.getAllViews().entrySet()) {
+ meterProvider.registerView(entry.getKey(), entry.getValue());
+ }
+
+ for (Map.Entry e :
+ BuiltinMetricsConstants.getInternalViews().entrySet()) {
+ meterProvider.registerView(e.getKey(), e.getValue());
+ }
+
+ MetricExporter publicExporter =
+ BigtableCloudMonitoringExporter.create(
+ clientInfo, credentials, metricsEndpoint, universeDomain, executor);
+ PeriodicMetricReaderBuilder readerBuilder =
+ PeriodicMetricReader.builder(publicExporter).setExecutor(executor);
+ meterProvider.registerMetricReader(readerBuilder.build());
+
+ return OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
+ }
+
+ private static ApiTracerFactory createOCTracingFactory(ClientInfo clientInfo) {
+ return new OpencensusTracerFactory(
+ ImmutableMap.builder()
+ // Annotate traces with the same tags as metrics
+ .put(
+ RpcMeasureConstants.BIGTABLE_PROJECT_ID.getName(),
+ clientInfo.getInstanceName().getProject())
+ .put(
+ RpcMeasureConstants.BIGTABLE_INSTANCE_ID.getName(),
+ clientInfo.getInstanceName().getInstance())
+ .put(
+ RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID.getName(), clientInfo.getAppProfileId())
+ // Also annotate traces with library versions
+ .put("gax", GaxGrpcProperties.getGaxGrpcVersion())
+ .put("grpc", GaxGrpcProperties.getGrpcVersion())
+ .put("gapic", Version.VERSION)
+ .build());
+ }
+
+ private static ApiTracerFactory createOCMetricsFactory(
+ ClientInfo clientInfo, Tagger tagger, StatsRecorder stats) {
+
+ ImmutableMap attributes =
+ ImmutableMap.builder()
+ .put(
+ RpcMeasureConstants.BIGTABLE_PROJECT_ID,
+ TagValue.create(clientInfo.getInstanceName().getProject()))
+ .put(
+ RpcMeasureConstants.BIGTABLE_INSTANCE_ID,
+ TagValue.create(clientInfo.getInstanceName().getInstance()))
+ .put(
+ RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID,
+ TagValue.create(clientInfo.getAppProfileId()))
+ .build();
+ return MetricsTracerFactory.create(tagger, stats, attributes);
+ }
+
+ private static BuiltinMetricsTracerFactory createOtelMetricsFactory(
+ OpenTelemetry otel, ClientInfo clientInfo) {
+
+ return BuiltinMetricsTracerFactory.create(otel, clientInfo);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index 6f005a0408..46474118b9 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -24,22 +24,17 @@
import com.google.api.gax.core.FixedExecutorProvider;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
import com.google.api.gax.rpc.ClientContext;
-import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.auth.Credentials;
import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials;
import com.google.bigtable.v2.InstanceName;
import com.google.cloud.bigtable.data.v2.internal.JwtCredentialsWithAudience;
+import com.google.cloud.bigtable.data.v2.internal.csm.Metrics;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
-import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
-import com.google.cloud.bigtable.data.v2.stub.metrics.ChannelPoolMetricsTracer;
-import com.google.cloud.bigtable.data.v2.stub.metrics.CompositeTracerFactory;
import com.google.cloud.bigtable.data.v2.stub.metrics.CustomOpenTelemetryMetricsProvider;
-import com.google.cloud.bigtable.data.v2.stub.metrics.Util;
import com.google.cloud.bigtable.gaxx.grpc.BigtableTransportChannelProvider;
import com.google.cloud.bigtable.gaxx.grpc.ChannelPrimer;
-import com.google.common.collect.ImmutableList;
import io.grpc.ManagedChannelBuilder;
-import io.grpc.opentelemetry.GrpcOpenTelemetry;
import io.opencensus.stats.Stats;
import io.opencensus.stats.StatsRecorder;
import io.opencensus.tags.Tagger;
@@ -65,15 +60,11 @@ public class BigtableClientContext {
private final boolean isChild;
private final ClientInfo clientInfo;
- private final ApiTracerFactory userTracerFactory;
- @Nullable private final OpenTelemetrySdk builtinOpenTelemetry;
- @Nullable private final OpenTelemetry userOpenTelemetry;
+ private final Metrics metrics;
private final ClientContext clientContext;
// the background executor shared for OTEL instances and monitoring client and all other
// background tasks
private final ExecutorProvider backgroundExecutorProvider;
- private final Tagger ocTagger;
- private final StatsRecorder ocRecorder;
public static BigtableClientContext create(EnhancedBigtableStubSettings settings)
throws IOException {
@@ -110,8 +101,6 @@ public static BigtableClientContext create(
FixedExecutorProvider.create(backgroundExecutor, shouldAutoClose);
builder.setBackgroundExecutorProvider(executorProvider);
- ApiTracerFactory userTracerFactory = settings.getTracerFactory();
-
// Set up OpenTelemetry
@Nullable OpenTelemetry userOtel = null;
if (settings.getMetricsProvider() instanceof CustomOpenTelemetryMetricsProvider) {
@@ -123,7 +112,7 @@ public static BigtableClientContext create(
try {
if (settings.areInternalMetricsEnabled()) {
builtinOtel =
- Util.createBuiltinOtel(
+ MetricsImpl.createBuiltinOtel(
clientInfo,
credentials,
settings.getMetricsEndpoint(),
@@ -134,26 +123,24 @@ public static BigtableClientContext create(
logger.log(Level.WARNING, "Failed to get OTEL, will skip exporting client side metrics", t);
}
+ Metrics metrics =
+ new MetricsImpl(
+ settings.getTracerFactory(),
+ builtinOtel,
+ userOtel,
+ ocTagger,
+ ocRecorder,
+ backgroundExecutor);
+
// Set up channel
InstantiatingGrpcChannelProvider.Builder transportProvider =
builder.getTransportChannelProvider() instanceof InstantiatingGrpcChannelProvider
? ((InstantiatingGrpcChannelProvider) builder.getTransportChannelProvider()).toBuilder()
: null;
- @Nullable ChannelPoolMetricsTracer channelPoolMetricsTracer = null;
- // Internal metrics are scoped to the connections, so we need a mutable transportProvider,
- // otherwise there is
- // no reason to build the internal OtelProvider
if (transportProvider != null) {
- if (builtinOtel != null) {
- channelPoolMetricsTracer = new ChannelPoolMetricsTracer(builtinOtel);
-
- // Configure grpc metrics
- configureGrpcOtel(transportProvider, builtinOtel);
- }
- }
+ configureGrpcOtel(transportProvider, metrics);
- if (transportProvider != null) {
setupCookieHolder(transportProvider);
ChannelPrimer channelPrimer = NoOpChannelPrimer.create();
@@ -173,42 +160,25 @@ public static BigtableClientContext create(
BigtableTransportChannelProvider.create(
transportProvider.build(),
channelPrimer,
- channelPoolMetricsTracer,
+ metrics.getChannelPoolMetricsTracer(),
backgroundExecutor);
builder.setTransportChannelProvider(btTransportProvider);
}
ClientContext clientContext = ClientContext.create(builder.build());
- if (channelPoolMetricsTracer != null) {
- channelPoolMetricsTracer.start(clientContext.getExecutor());
- }
- return new BigtableClientContext(
- false,
- clientInfo,
- clientContext,
- userTracerFactory,
- builtinOtel,
- userOtel,
- ocTagger,
- ocRecorder,
- executorProvider);
+ metrics.start();
+ try {
+ return new BigtableClientContext(false, clientInfo, clientContext, metrics, executorProvider);
+ } catch (IOException | RuntimeException t) {
+ metrics.close();
+ throw t;
+ }
}
private static void configureGrpcOtel(
- InstantiatingGrpcChannelProvider.Builder transportProvider, OpenTelemetrySdk otel) {
-
- GrpcOpenTelemetry grpcOtel =
- GrpcOpenTelemetry.newBuilder()
- .sdk(otel)
- .addOptionalLabel("grpc.lb.locality")
- // Disable default grpc metrics
- .disableAllMetrics()
- // Enable specific grpc metrics
- .enableMetrics(BuiltinMetricsConstants.GRPC_METRICS.keySet())
- .build();
-
+ InstantiatingGrpcChannelProvider.Builder transportProvider, Metrics metrics) {
@SuppressWarnings("rawtypes")
ApiFunction oldConfigurator =
transportProvider.getChannelConfigurator();
@@ -218,8 +188,7 @@ private static void configureGrpcOtel(
if (oldConfigurator != null) {
b = oldConfigurator.apply(b);
}
- grpcOtel.configureChannelBuilder(b);
- return b;
+ return metrics.configureGrpcChannel(b);
});
}
@@ -227,54 +196,25 @@ private BigtableClientContext(
boolean isChild,
ClientInfo clientInfo,
ClientContext clientContext,
- ApiTracerFactory userTracerFactory,
- @Nullable OpenTelemetrySdk builtinOtel,
- @Nullable OpenTelemetry userOtel,
- Tagger ocTagger,
- StatsRecorder ocRecorder,
+ Metrics metrics,
ExecutorProvider backgroundExecutorProvider)
throws IOException {
this.isChild = isChild;
this.clientInfo = clientInfo;
- this.userTracerFactory = userTracerFactory;
- this.builtinOpenTelemetry = builtinOtel;
- this.userOpenTelemetry = userOtel;
- this.ocTagger = ocTagger;
- this.ocRecorder = ocRecorder;
+ this.metrics = metrics;
this.backgroundExecutorProvider = backgroundExecutorProvider;
- ImmutableList.Builder tracerFactories = ImmutableList.builder();
- tracerFactories
- .add(Util.createOCTracingFactory(clientInfo))
- .add(Util.createOCMetricsFactory(clientInfo, ocTagger, ocRecorder))
- .add(userTracerFactory);
-
- if (builtinOtel != null) {
- tracerFactories.add(Util.createOtelMetricsFactory(builtinOtel, clientInfo));
- }
- if (userOtel != null) {
- tracerFactories.add(Util.createOtelMetricsFactory(userOtel, clientInfo));
- }
-
this.clientContext =
- clientContext.toBuilder()
- .setTracerFactory(new CompositeTracerFactory(tracerFactories.build()))
- .build();
+ clientContext.toBuilder().setTracerFactory(metrics.createTracerFactory(clientInfo)).build();
}
public ClientInfo getClientInfo() {
return clientInfo;
}
- @Nullable
- public OpenTelemetrySdk getBuiltinOpenTelemetry() {
- return builtinOpenTelemetry;
- }
-
- @Nullable
- public OpenTelemetry getUserOpenTelemetry() {
- return this.userOpenTelemetry;
+ public Metrics getMetrics() {
+ return metrics;
}
public ClientContext getClientContext() {
@@ -287,11 +227,7 @@ public BigtableClientContext createChild(InstanceName instanceName, String appPr
true,
clientInfo.toBuilder().setInstanceName(instanceName).setAppProfileId(appProfileId).build(),
clientContext,
- userTracerFactory,
- builtinOpenTelemetry,
- userOpenTelemetry,
- ocTagger,
- ocRecorder,
+ metrics,
backgroundExecutorProvider);
}
@@ -303,12 +239,8 @@ public void close() throws Exception {
for (BackgroundResource resource : clientContext.getBackgroundResources()) {
resource.close();
}
- if (builtinOpenTelemetry != null) {
- builtinOpenTelemetry.close();
- }
- if (builtinOpenTelemetry != null) {
- builtinOpenTelemetry.close();
- }
+ metrics.close();
+
if (backgroundExecutorProvider.shouldAutoClose()) {
backgroundExecutorProvider.getExecutor().shutdown();
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
index 67cc5ba134..2aba290aff 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
@@ -113,7 +113,7 @@ public final class BigtableCloudMonitoringExporter implements MetricExporter {
private final AtomicBoolean exportFailureLogged = new AtomicBoolean(false);
- static BigtableCloudMonitoringExporter create(
+ public static BigtableCloudMonitoringExporter create(
ClientInfo clientInfo,
@Nullable Credentials credentials,
@Nullable String endpoint,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
index 3d83659a28..c59c145f7f 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
@@ -46,7 +46,6 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
-import java.io.IOException;
/**
* {@link ApiTracerFactory} that will generate OpenTelemetry metrics by using the {@link ApiTracer}
@@ -74,7 +73,7 @@ public class BuiltinMetricsTracerFactory extends BaseApiTracerFactory {
private final DoubleHistogram batchWriteFlowControlFactorHistogram;
public static BuiltinMetricsTracerFactory create(
- OpenTelemetry openTelemetry, ClientInfo clientInfo) throws IOException {
+ OpenTelemetry openTelemetry, ClientInfo clientInfo) {
return new BuiltinMetricsTracerFactory(openTelemetry, clientInfo);
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
index dc6155f88a..7381b220e2 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
@@ -16,13 +16,9 @@
package com.google.cloud.bigtable.data.v2.stub.metrics;
import com.google.api.core.InternalApi;
-import com.google.api.gax.grpc.GaxGrpcProperties;
import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ApiException;
-import com.google.api.gax.tracing.ApiTracerFactory;
-import com.google.api.gax.tracing.OpencensusTracerFactory;
-import com.google.auth.Credentials;
import com.google.bigtable.v2.AuthorizedViewName;
import com.google.bigtable.v2.CheckAndMutateRowRequest;
import com.google.bigtable.v2.GenerateInitialChangeStreamPartitionsRequest;
@@ -36,27 +32,10 @@
import com.google.bigtable.v2.ResponseParams;
import com.google.bigtable.v2.SampleRowKeysRequest;
import com.google.bigtable.v2.TableName;
-import com.google.cloud.bigtable.Version;
-import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
-import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.common.collect.ImmutableMap;
import io.grpc.Metadata;
import io.grpc.Status;
-import io.opencensus.stats.StatsRecorder;
-import io.opencensus.tags.TagKey;
-import io.opencensus.tags.TagValue;
-import io.opencensus.tags.Tagger;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.sdk.OpenTelemetrySdk;
-import io.opentelemetry.sdk.metrics.InstrumentSelector;
-import io.opentelemetry.sdk.metrics.SdkMeterProvider;
-import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
-import io.opentelemetry.sdk.metrics.View;
-import io.opentelemetry.sdk.metrics.export.MetricExporter;
-import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
-import io.opentelemetry.sdk.metrics.export.PeriodicMetricReaderBuilder;
-import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
@@ -65,7 +44,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
-import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
/** Utilities to help integrating with OpenCensus. */
@@ -156,41 +134,6 @@ static Map> createStatsHeaders(ApiCallContext apiCallContex
return headers.build();
}
- public static OpenTelemetrySdk createBuiltinOtel(
- ClientInfo clientInfo,
- @Nullable Credentials defaultCredentials,
- @Nullable String metricsEndpoint,
- String universeDomain,
- ScheduledExecutorService executor)
- throws IOException {
-
- Credentials credentials =
- BigtableDataSettings.getMetricsCredentials() != null
- ? BigtableDataSettings.getMetricsCredentials()
- : defaultCredentials;
-
- SdkMeterProviderBuilder meterProvider = SdkMeterProvider.builder();
-
- for (Map.Entry entry :
- BuiltinMetricsConstants.getAllViews().entrySet()) {
- meterProvider.registerView(entry.getKey(), entry.getValue());
- }
-
- for (Map.Entry e :
- BuiltinMetricsConstants.getInternalViews().entrySet()) {
- meterProvider.registerView(e.getKey(), e.getValue());
- }
-
- MetricExporter publicExporter =
- BigtableCloudMonitoringExporter.create(
- clientInfo, credentials, metricsEndpoint, universeDomain, executor);
- PeriodicMetricReaderBuilder readerBuilder =
- PeriodicMetricReader.builder(publicExporter).setExecutor(executor);
- meterProvider.registerMetricReader(readerBuilder.build());
-
- return OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
- }
-
public static String formatTransportTypeMetricLabel(
MetadataExtractorInterceptor.SidebandData sidebandData) {
return Optional.ofNullable(sidebandData)
@@ -219,47 +162,4 @@ public static String formatZoneIdMetricLabel(
.filter(s -> !s.isEmpty())
.orElse("global");
}
-
- public static ApiTracerFactory createOCTracingFactory(ClientInfo clientInfo) {
- return new OpencensusTracerFactory(
- ImmutableMap.builder()
- // Annotate traces with the same tags as metrics
- .put(
- RpcMeasureConstants.BIGTABLE_PROJECT_ID.getName(),
- clientInfo.getInstanceName().getProject())
- .put(
- RpcMeasureConstants.BIGTABLE_INSTANCE_ID.getName(),
- clientInfo.getInstanceName().getInstance())
- .put(
- RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID.getName(), clientInfo.getAppProfileId())
- // Also annotate traces with library versions
- .put("gax", GaxGrpcProperties.getGaxGrpcVersion())
- .put("grpc", GaxGrpcProperties.getGrpcVersion())
- .put("gapic", Version.VERSION)
- .build());
- }
-
- public static ApiTracerFactory createOCMetricsFactory(
- ClientInfo clientInfo, Tagger tagger, StatsRecorder stats) {
-
- ImmutableMap attributes =
- ImmutableMap.builder()
- .put(
- RpcMeasureConstants.BIGTABLE_PROJECT_ID,
- TagValue.create(clientInfo.getInstanceName().getProject()))
- .put(
- RpcMeasureConstants.BIGTABLE_INSTANCE_ID,
- TagValue.create(clientInfo.getInstanceName().getInstance()))
- .put(
- RpcMeasureConstants.BIGTABLE_APP_PROFILE_ID,
- TagValue.create(clientInfo.getAppProfileId()))
- .build();
- return MetricsTracerFactory.create(tagger, stats, attributes);
- }
-
- public static BuiltinMetricsTracerFactory createOtelMetricsFactory(
- OpenTelemetry otel, ClientInfo clientInfo) throws IOException {
-
- return BuiltinMetricsTracerFactory.create(otel, clientInfo);
- }
}
From ed4ad83e9324c87324f8612a2b9b57064b3237fb Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Wed, 25 Feb 2026 21:30:30 -0500
Subject: [PATCH 18/33] chore: wire up the new typesafe metrics (#2808)
---
.../data/v2/internal/csm/MetricsImpl.java | 21 +-
.../data/v2/internal/csm/attributes/Util.java | 43 ++-
.../TableApplicationBlockingLatency.java | 3 +-
.../csm/metrics/TableAttemptLatency.java | 3 +-
.../csm/metrics/TableAttemptLatency2.java | 13 +-
.../metrics/TableClientBlockingLatency.java | 3 +-
.../metrics/TableConnectivityErrorCount.java | 3 +-
.../csm/metrics/TableDebugTagCount.java | 3 +-
.../metrics/TableFirstResponseLatency.java | 3 +-
.../csm/metrics/TableOperationLatency.java | 3 +-
.../internal/csm/metrics/TableRetryCount.java | 3 +-
.../csm/metrics/TableServerLatency.java | 3 +-
.../v2/internal/csm/schema/TableSchema.java | 8 +-
.../data/v2/stub/BigtableClientContext.java | 1 +
.../v2/stub/MetadataExtractorInterceptor.java | 11 +-
.../v2/stub/metrics/BuiltinMetricsTracer.java | 258 +++++++-----------
.../metrics/BuiltinMetricsTracerFactory.java | 178 +-----------
.../metrics/ChannelPoolMetricsTracer.java | 85 ++----
.../data/v2/stub/metrics/MetricsTracer.java | 2 +-
.../bigtable/data/v2/stub/metrics/Util.java | 34 ---
.../BigtableTransportChannelProvider.java | 2 +-
.../metrics/BuiltinMetricsTracerTest.java | 7 +-
.../metrics/ChannelPoolMetricsTracerTest.java | 19 +-
23 files changed, 243 insertions(+), 466 deletions(-)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
index 139ea6727e..3ae54c5313 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
@@ -21,6 +21,7 @@
import com.google.auth.Credentials;
import com.google.cloud.bigtable.Version;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry.RecorderRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableCloudMonitoringExporter;
import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
@@ -57,6 +58,8 @@
import javax.annotation.Nullable;
public class MetricsImpl implements Metrics, Closeable {
+ private final MetricRegistry metricRegistry;
+
private final ApiTracerFactory userTracerFactory;
private final @Nullable OpenTelemetrySdk internalOtel;
private final @Nullable OpenTelemetry userOtel;
@@ -69,12 +72,14 @@ public class MetricsImpl implements Metrics, Closeable {
private final List> tasks = new ArrayList<>();
public MetricsImpl(
+ ClientInfo clientInfo,
ApiTracerFactory userTracerFactory,
@Nullable OpenTelemetrySdk internalOtel,
@Nullable OpenTelemetry userOtel,
Tagger ocTagger,
StatsRecorder ocRecorder,
ScheduledExecutorService executor) {
+ metricRegistry = new MetricRegistry();
this.userTracerFactory = Preconditions.checkNotNull(userTracerFactory);
this.internalOtel = internalOtel;
@@ -100,7 +105,9 @@ public MetricsImpl(
}
if (internalOtel != null) {
- this.channelPoolMetricsTracer = new ChannelPoolMetricsTracer(internalOtel);
+ this.channelPoolMetricsTracer =
+ new ChannelPoolMetricsTracer(
+ metricRegistry.newRecorderRegistry(internalOtel.getMeterProvider()), clientInfo);
} else {
this.channelPoolMetricsTracer = null;
}
@@ -141,10 +148,14 @@ public ApiTracerFactory createTracerFactory(ClientInfo clientInfo) {
.add(userTracerFactory);
if (internalOtel != null) {
- tracerFactories.add(createOtelMetricsFactory(internalOtel, clientInfo));
+ tracerFactories.add(
+ createOtelMetricsFactory(
+ metricRegistry.newRecorderRegistry(internalOtel.getMeterProvider()), clientInfo));
}
if (userOtel != null) {
- tracerFactories.add(createOtelMetricsFactory(userOtel, clientInfo));
+ tracerFactories.add(
+ createOtelMetricsFactory(
+ metricRegistry.newRecorderRegistry(userOtel.getMeterProvider()), clientInfo));
}
return new CompositeTracerFactory(tracerFactories.build());
@@ -229,8 +240,8 @@ private static ApiTracerFactory createOCMetricsFactory(
}
private static BuiltinMetricsTracerFactory createOtelMetricsFactory(
- OpenTelemetry otel, ClientInfo clientInfo) {
+ RecorderRegistry recorder, ClientInfo clientInfo) {
- return BuiltinMetricsTracerFactory.create(otel, clientInfo);
+ return BuiltinMetricsTracerFactory.create(recorder, clientInfo);
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
index cf9c2a114e..9379f4726d 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
@@ -16,26 +16,37 @@
package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+import com.google.bigtable.v2.PeerInfo;
import com.google.bigtable.v2.PeerInfo.TransportType;
-import com.google.common.base.Preconditions;
+import com.google.bigtable.v2.ResponseParams;
import java.util.Locale;
+import java.util.Optional;
+import javax.annotation.Nullable;
public class Util {
static final String TRANSPORT_TYPE_PREFIX = "TRANSPORT_TYPE_";
- public static String transportTypeToString(TransportType transportType) {
+ public static String formatTransportZone(@Nullable PeerInfo peerInfo) {
+ return Optional.ofNullable(peerInfo).map(PeerInfo::getApplicationFrontendZone).orElse("");
+ }
- Preconditions.checkArgument(
- transportType.name().startsWith(TRANSPORT_TYPE_PREFIX)
- || transportType == TransportType.UNRECOGNIZED,
- "TransportType values must start with %s",
- TRANSPORT_TYPE_PREFIX);
+ public static String formatTransportSubzone(@Nullable PeerInfo peerInfo) {
+ return Optional.ofNullable(peerInfo).map(PeerInfo::getApplicationFrontendSubzone).orElse("");
+ }
+
+ public static String formatTransportType(@Nullable PeerInfo peerInfo) {
+ return transportTypeToString(
+ Optional.ofNullable(peerInfo)
+ .map(PeerInfo::getTransportType)
+ .orElse(TransportType.TRANSPORT_TYPE_UNKNOWN));
+ }
+ public static String transportTypeToString(TransportType transportType) {
if (transportType == TransportType.TRANSPORT_TYPE_UNKNOWN) {
- return "session_none";
+ return "none";
}
if (transportType == TransportType.UNRECOGNIZED) {
- return "session_unrecognized";
+ return "unrecognized";
}
return transportType
@@ -43,4 +54,18 @@ public static String transportTypeToString(TransportType transportType) {
.substring(TRANSPORT_TYPE_PREFIX.length())
.toLowerCase(Locale.ENGLISH);
}
+
+ public static String formatClusterIdMetricLabel(@Nullable ResponseParams clusterInfo) {
+ return Optional.ofNullable(clusterInfo)
+ .map(ResponseParams::getClusterId)
+ .filter(s -> !s.isEmpty())
+ .orElse("");
+ }
+
+ public static String formatZoneIdMetricLabel(@Nullable ResponseParams clusterInfo) {
+ return Optional.ofNullable(clusterInfo)
+ .map(ResponseParams::getZoneId)
+ .filter(s -> !s.isEmpty())
+ .orElse("global");
+ }
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java
index 90e390304e..05fdefd0be 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableApplicationBlockingLatency.java
@@ -29,6 +29,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableApplicationBlockingLatency extends MetricWrapper {
private static final String NAME =
@@ -69,7 +70,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Duration duration) {
Attributes attributes =
getSchema()
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java
index 2ba86e89c9..530498fa9a 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency.java
@@ -30,6 +30,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableAttemptLatency extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/attempt_latencies";
@@ -67,7 +68,7 @@ private Recorder(Meter meter) {
public void record(
ClientInfo clientInfo,
String tableId,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
MethodInfo methodInfo,
Status.Code code,
Duration latency) {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java
index 0570559610..63cb2aa929 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableAttemptLatency2.java
@@ -31,6 +31,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableAttemptLatency2 extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/attempt_latencies2";
@@ -68,8 +69,8 @@ private Recorder(Meter meter) {
public void record(
ClientInfo clientInfo,
String tableId,
- PeerInfo peerInfo,
- ResponseParams clusterInfo,
+ @Nullable PeerInfo peerInfo,
+ @Nullable ResponseParams clusterInfo,
MethodInfo methodInfo,
Status.Code code,
Duration latency) {
@@ -77,14 +78,12 @@ public void record(
Attributes attributes =
getSchema()
.createResourceAttrs(clientInfo, tableId, clusterInfo)
- .put(
- MetricLabels.TRANSPORT_TYPE,
- Util.transportTypeToString(peerInfo.getTransportType()))
+ .put(MetricLabels.TRANSPORT_TYPE, Util.formatTransportType(peerInfo))
.put(MetricLabels.STATUS_KEY, code.name())
.put(MetricLabels.TRANSPORT_REGION, "")
// To maintain backwards compat CLIENT_UID is set using sideband data in the exporter
- .put(MetricLabels.TRANSPORT_ZONE, peerInfo.getApplicationFrontendZone())
- .put(MetricLabels.TRANSPORT_SUBZONE, peerInfo.getApplicationFrontendSubzone())
+ .put(MetricLabels.TRANSPORT_ZONE, Util.formatTransportZone(peerInfo))
+ .put(MetricLabels.TRANSPORT_SUBZONE, Util.formatTransportSubzone(peerInfo))
.put(MetricLabels.CLIENT_NAME, clientInfo.getClientName())
.put(MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
.put(MetricLabels.METHOD_KEY, methodInfo.getName())
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java
index 1d8deca639..7f9a584a69 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableClientBlockingLatency.java
@@ -29,6 +29,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableClientBlockingLatency extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/throttling_latencies";
@@ -69,7 +70,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Duration duration) {
Attributes attributes =
getSchema()
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java
index 95d8fca949..0233b8adef 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableConnectivityErrorCount.java
@@ -28,6 +28,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
+import javax.annotation.Nullable;
public class TableConnectivityErrorCount extends MetricWrapper {
private static final String NAME =
@@ -68,7 +69,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Status.Code code,
long count) {
Attributes attributes =
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java
index f8bfc25fb5..5d9dbc8536 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableDebugTagCount.java
@@ -26,6 +26,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
+import javax.annotation.Nullable;
public class TableDebugTagCount extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/debug_tags";
@@ -63,7 +64,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
String tag,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
long amount) {
Attributes attributes =
getSchema()
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java
index af5909c054..bde5009f68 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableFirstResponseLatency.java
@@ -30,6 +30,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableFirstResponseLatency extends MetricWrapper {
private static final String NAME =
@@ -72,7 +73,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Status.Code code,
Duration duration) {
Attributes attributes =
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java
index b6323cce8b..4a30d66f20 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableOperationLatency.java
@@ -30,6 +30,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableOperationLatency extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/operation_latencies";
@@ -70,7 +71,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Status.Code code,
Duration duration) {
Attributes attributes =
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java
index de7b608b4e..a81a4bf903 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableRetryCount.java
@@ -28,6 +28,7 @@
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
+import javax.annotation.Nullable;
public class TableRetryCount extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/retry_count";
@@ -65,7 +66,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Status.Code code,
long amount) {
Attributes attributes =
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java
index b759591113..0d8dc0197b 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/TableServerLatency.java
@@ -30,6 +30,7 @@
import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.Meter;
import java.time.Duration;
+import javax.annotation.Nullable;
public class TableServerLatency extends MetricWrapper {
private static final String NAME = "bigtable.googleapis.com/internal/client/server_latencies";
@@ -70,7 +71,7 @@ public void record(
ClientInfo clientInfo,
String tableId,
MethodInfo methodInfo,
- ResponseParams clusterInfo,
+ @Nullable ResponseParams clusterInfo,
Status.Code code,
Duration duration) {
Attributes attributes =
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java
index f536f73837..618551bb87 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/schema/TableSchema.java
@@ -19,11 +19,13 @@
import com.google.bigtable.v2.ResponseParams;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
import com.google.common.collect.ImmutableList;
import com.google.monitoring.v3.ProjectName;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
+import javax.annotation.Nullable;
public final class TableSchema extends Schema {
// This implements the `bigtable_client_raw` resource defined in
@@ -52,12 +54,12 @@ public ProjectName extractProjectName(Attributes attrs, EnvInfo envInfo, ClientI
}
public AttributesBuilder createResourceAttrs(
- ClientInfo clientInfo, String tableId, ResponseParams clusterInfo) {
+ ClientInfo clientInfo, String tableId, @Nullable ResponseParams clusterInfo) {
return Attributes.builder()
.put(BIGTABLE_PROJECT_ID_KEY, clientInfo.getInstanceName().getProject())
.put(INSTANCE_ID_KEY, clientInfo.getInstanceName().getInstance())
.put(TABLE_ID_KEY, tableId)
- .put(CLUSTER_ID_KEY, clusterInfo.getClusterId())
- .put(ZONE_ID_KEY, clusterInfo.getZoneId());
+ .put(CLUSTER_ID_KEY, Util.formatClusterIdMetricLabel(clusterInfo))
+ .put(ZONE_ID_KEY, Util.formatZoneIdMetricLabel(clusterInfo));
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index 46474118b9..c82b0a8d02 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -125,6 +125,7 @@ public static BigtableClientContext create(
Metrics metrics =
new MetricsImpl(
+ clientInfo,
settings.getTracerFactory(),
builtinOtel,
userOtel,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
index 5b43f57527..14ad73131f 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MetadataExtractorInterceptor.java
@@ -33,6 +33,7 @@
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.grpc.alts.AltsContextUtil;
+import java.time.Duration;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -98,7 +99,7 @@ public static class SidebandData {
@Nullable private volatile ResponseParams responseParams;
@Nullable private volatile PeerInfo peerInfo;
- @Nullable private volatile Long gfeTiming;
+ @Nullable private volatile Duration gfeTiming;
@Nullable
public ResponseParams getResponseParams() {
@@ -111,7 +112,7 @@ public PeerInfo getPeerInfo() {
}
@Nullable
- public Long getGfeTiming() {
+ public Duration getGfeTiming() {
return gfeTiming;
}
@@ -134,7 +135,7 @@ void onClose(Status status, Metadata trailers) {
}
@Nullable
- private static Long extractGfeLatency(Metadata metadata) {
+ private static Duration extractGfeLatency(Metadata metadata) {
String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
if (serverTiming == null) {
return null;
@@ -142,14 +143,14 @@ private static Long extractGfeLatency(Metadata metadata) {
Matcher matcher = SERVER_TIMING_HEADER_PATTERN.matcher(serverTiming);
// this should always be true
if (matcher.find()) {
- return Long.parseLong(matcher.group("dur"));
+ return Duration.ofMillis(Long.parseLong(matcher.group("dur")));
}
return null;
}
@Nullable
private static PeerInfo extractPeerInfo(
- Metadata metadata, Long gfeTiming, Attributes attributes) {
+ Metadata metadata, Duration gfeTiming, Attributes attributes) {
String encodedStr = metadata.get(PEER_INFO_KEY);
if (Strings.isNullOrEmpty(encodedStr)) {
return null;
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
index 74d09f5834..57181faa34 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
@@ -15,38 +15,22 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import static com.google.api.gax.tracing.ApiTracerFactory.OperationType;
import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APPLIED_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_NAME_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLUSTER_ID_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METHOD_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.STATUS_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.STREAMING_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TABLE_ID_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TRANSPORT_REGION;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TRANSPORT_SUBZONE;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TRANSPORT_TYPE;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.TRANSPORT_ZONE;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ZONE_ID_KEY;
import static com.google.cloud.bigtable.data.v2.stub.metrics.Util.extractStatus;
import com.google.api.core.ObsoleteApi;
import com.google.api.gax.retrying.ServerStreamingAttemptException;
-import com.google.api.gax.tracing.SpanName;
-import com.google.bigtable.v2.PeerInfo;
-import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
+import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor.SidebandData;
import com.google.common.base.Stopwatch;
+import com.google.common.collect.Comparators;
import com.google.common.math.IntMath;
import io.grpc.Deadline;
import io.grpc.Status;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.metrics.DoubleGauge;
-import io.opentelemetry.api.metrics.DoubleHistogram;
-import io.opentelemetry.api.metrics.LongCounter;
import java.time.Duration;
-import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -59,9 +43,11 @@
* bigtable.googleapis.com/client namespace
*/
class BuiltinMetricsTracer extends BigtableTracer {
- private static final String NAME = "java-bigtable/" + Version.VERSION;
- private final OperationType operationType;
- private final SpanName spanName;
+ private static final MethodInfo READ_ROWS =
+ MethodInfo.builder().setName("Bigtable.ReadRows").setStreaming(true).build();
+ private final MetricRegistry.RecorderRegistry recorder;
+ private final ClientInfo clientInfo;
+ private final MethodInfo methodInfo;
// Operation level metrics
private final AtomicBoolean operationFinishedEarly = new AtomicBoolean();
@@ -91,67 +77,19 @@ class BuiltinMetricsTracer extends BigtableTracer {
private final AtomicLong totalClientBlockingTime = new AtomicLong(0);
- private final Attributes baseAttributes;
-
private final AtomicLong grpcMessageSentDelay = new AtomicLong(0);
private Deadline operationDeadline = null;
- private volatile long remainingDeadlineAtAttemptStart = 0;
-
- // TODO: ensure that this is never null and remove all of the checks
- // Sideband data wrapper itself should never be null unless a callable chain forgets to
- // add BigtableTracer{Streaming,Unary}Callable. Which would be considered a bug.
- @Nullable private volatile MetadataExtractorInterceptor.SidebandData sidebandData = null;
-
- // OpenCensus (and server) histogram buckets use [start, end), however OpenTelemetry uses (start,
- // end]. To work around this, we measure all the latencies in nanoseconds and convert them
- // to milliseconds and use DoubleHistogram. This should minimize the chance of a data
- // point fall on the bucket boundary that causes off by one errors.
- private final DoubleHistogram operationLatenciesHistogram;
- private final DoubleHistogram attemptLatenciesHistogram;
- private final DoubleHistogram attemptLatencies2Histogram;
- private final DoubleHistogram serverLatenciesHistogram;
- private final DoubleHistogram firstResponseLatenciesHistogram;
- private final DoubleHistogram clientBlockingLatenciesHistogram;
- private final DoubleHistogram applicationBlockingLatenciesHistogram;
- private final DoubleHistogram remainingDeadlineHistogram;
- private final LongCounter connectivityErrorCounter;
- private final LongCounter retryCounter;
- private final DoubleGauge batchWriteFlowControlTargetQps;
- private final DoubleHistogram batchWriteFlowControlFactorHistogram;
+ private volatile Duration remainingDeadlineAtAttemptStart = Duration.ZERO;
+
+ private volatile MetadataExtractorInterceptor.SidebandData sidebandData = new SidebandData();
BuiltinMetricsTracer(
- OperationType operationType,
- SpanName spanName,
- Attributes attributes,
- DoubleHistogram operationLatenciesHistogram,
- DoubleHistogram attemptLatenciesHistogram,
- DoubleHistogram attemptLatencies2Histogram,
- DoubleHistogram serverLatenciesHistogram,
- DoubleHistogram firstResponseLatenciesHistogram,
- DoubleHistogram clientBlockingLatenciesHistogram,
- DoubleHistogram applicationBlockingLatenciesHistogram,
- DoubleHistogram deadlineHistogram,
- LongCounter connectivityErrorCounter,
- LongCounter retryCounter,
- DoubleGauge batchWriteFlowControlTargetQps,
- DoubleHistogram batchWriteFlowControlFactorHistogram) {
- this.operationType = operationType;
- this.spanName = spanName;
- this.baseAttributes = attributes;
-
- this.operationLatenciesHistogram = operationLatenciesHistogram;
- this.attemptLatenciesHistogram = attemptLatenciesHistogram;
- this.attemptLatencies2Histogram = attemptLatencies2Histogram;
- this.serverLatenciesHistogram = serverLatenciesHistogram;
- this.firstResponseLatenciesHistogram = firstResponseLatenciesHistogram;
- this.clientBlockingLatenciesHistogram = clientBlockingLatenciesHistogram;
- this.applicationBlockingLatenciesHistogram = applicationBlockingLatenciesHistogram;
- this.remainingDeadlineHistogram = deadlineHistogram;
- this.connectivityErrorCounter = connectivityErrorCounter;
- this.retryCounter = retryCounter;
- this.batchWriteFlowControlTargetQps = batchWriteFlowControlTargetQps;
- this.batchWriteFlowControlFactorHistogram = batchWriteFlowControlFactorHistogram;
+ MetricRegistry.RecorderRegistry recorder, ClientInfo clientInfo, MethodInfo methodInfo) {
+
+ this.recorder = recorder;
+ this.clientInfo = clientInfo;
+ this.methodInfo = methodInfo;
}
@Override
@@ -195,7 +133,8 @@ public void attemptStarted(Object request, int attemptNumber) {
attemptCount++;
attemptTimer = Stopwatch.createStarted();
if (operationDeadline != null) {
- remainingDeadlineAtAttemptStart = operationDeadline.timeRemaining(TimeUnit.MILLISECONDS);
+ remainingDeadlineAtAttemptStart =
+ Duration.ofMillis(operationDeadline.timeRemaining(TimeUnit.MILLISECONDS));
}
if (request != null) {
this.tableId = Util.extractTableId(request);
@@ -328,7 +267,7 @@ public void setTotalTimeoutDuration(java.time.Duration totalTimeoutDuration) {
if (operationDeadline == null && !totalTimeoutDuration.isZero()) {
this.operationDeadline =
Deadline.after(totalTimeoutDuration.toMillis(), TimeUnit.MILLISECONDS);
- this.remainingDeadlineAtAttemptStart = totalTimeoutDuration.toMillis();
+ this.remainingDeadlineAtAttemptStart = totalTimeoutDuration;
}
}
@@ -347,38 +286,45 @@ private void recordOperationCompletion(@Nullable Throwable throwable) {
}
long operationLatencyNano = operationTimer.elapsed(TimeUnit.NANOSECONDS);
- boolean isStreaming = operationType == OperationType.ServerStreaming;
Status.Code code = extractStatus(throwable);
- // Publish metric data with all the attributes. The attributes get filtered in
- // BuiltinMetricsConstants when we construct the views.
- Attributes attributes =
- baseAttributes.toBuilder()
- .put(TABLE_ID_KEY, tableId)
- .put(CLUSTER_ID_KEY, Util.formatClusterIdMetricLabel(sidebandData))
- .put(ZONE_ID_KEY, Util.formatZoneIdMetricLabel(sidebandData))
- .put(METHOD_KEY, spanName.toString())
- .put(CLIENT_NAME_KEY, NAME)
- .put(STREAMING_KEY, isStreaming)
- .put(STATUS_KEY, code.name())
- .build();
-
// Only record when retry count is greater than 0 so the retry
// graph will be less confusing
if (attemptCount > 1) {
- retryCounter.add(attemptCount - 1, attributes);
+ recorder.retryCount.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ attemptCount - 1);
}
- operationLatenciesHistogram.record(convertToMs(operationLatencyNano), attributes);
+ recorder.operationLatency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ Duration.ofNanos(operationLatencyNano));
// serverLatencyTimer should already be stopped in recordAttemptCompletion
long applicationLatencyNano = operationLatencyNano - totalServerLatencyNano.get();
- applicationBlockingLatenciesHistogram.record(convertToMs(applicationLatencyNano), attributes);
-
- if (operationType == OperationType.ServerStreaming
- && spanName.getMethodName().equals("ReadRows")) {
- firstResponseLatenciesHistogram.record(
- convertToMs(firstResponsePerOpTimer.elapsed(TimeUnit.NANOSECONDS)), attributes);
+ recorder.applicationBlockingLatency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ Duration.ofNanos(applicationLatencyNano));
+
+ if (methodInfo.equals(READ_ROWS)) {
+ recorder.firstResponseLantency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ firstResponsePerOpTimer.elapsed());
}
}
@@ -396,8 +342,6 @@ private void recordAttemptCompletion(@Nullable Throwable throwable) {
}
}
- boolean isStreaming = operationType == OperationType.ServerStreaming;
-
// Patch the throwable until it's fixed in gax. When an attempt failed,
// it'll throw a ServerStreamingAttemptException. Unwrap the exception
// so it could get processed by extractStatus
@@ -407,60 +351,56 @@ private void recordAttemptCompletion(@Nullable Throwable throwable) {
Status.Code code = extractStatus(throwable);
- Attributes attributes =
- baseAttributes.toBuilder()
- .put(TABLE_ID_KEY, tableId)
- .put(CLUSTER_ID_KEY, Util.formatClusterIdMetricLabel(sidebandData))
- .put(ZONE_ID_KEY, Util.formatZoneIdMetricLabel(sidebandData))
- .put(METHOD_KEY, spanName.toString())
- .put(CLIENT_NAME_KEY, NAME)
- .put(STREAMING_KEY, isStreaming)
- .put(STATUS_KEY, code.name())
- .build();
-
totalClientBlockingTime.addAndGet(grpcMessageSentDelay.get());
- clientBlockingLatenciesHistogram.record(convertToMs(totalClientBlockingTime.get()), attributes);
-
- attemptLatenciesHistogram.record(
- convertToMs(attemptTimer.elapsed(TimeUnit.NANOSECONDS)), attributes);
-
- String transportTypeStr = "cloudpath";
- String transportRegion = "";
- String transportZone = "";
- String transportSubzone = "";
-
- if (sidebandData != null) {
- transportTypeStr = Util.formatTransportTypeMetricLabel(sidebandData);
- transportZone =
- Optional.ofNullable(sidebandData.getPeerInfo())
- .map(PeerInfo::getApplicationFrontendZone)
- .orElse("");
- transportSubzone =
- Optional.ofNullable(sidebandData.getPeerInfo())
- .map(PeerInfo::getApplicationFrontendSubzone)
- .orElse("");
- }
-
- attemptLatencies2Histogram.record(
- convertToMs(attemptTimer.elapsed(TimeUnit.NANOSECONDS)),
- attributes.toBuilder()
- .put(TRANSPORT_TYPE, transportTypeStr)
- .put(TRANSPORT_REGION, transportRegion)
- .put(TRANSPORT_ZONE, transportZone)
- .put(TRANSPORT_SUBZONE, transportSubzone)
- .build());
+ recorder.clientBlockingLatency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ Duration.ofNanos(totalClientBlockingTime.get()));
+
+ recorder.attemptLatency.record(
+ clientInfo,
+ tableId,
+ sidebandData.getResponseParams(),
+ methodInfo,
+ code,
+ attemptTimer.elapsed());
+
+ recorder.attemptLatency2.record(
+ clientInfo,
+ tableId,
+ sidebandData.getPeerInfo(),
+ sidebandData.getResponseParams(),
+ methodInfo,
+ code,
+ attemptTimer.elapsed());
// When operationDeadline is set, it's possible that the deadline is passed by the time we send
// a new attempt. In this case we'll record 0.
if (operationDeadline != null) {
- remainingDeadlineHistogram.record(Math.max(0, remainingDeadlineAtAttemptStart), attributes);
+ recorder.remainingDeadline.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ Comparators.max(remainingDeadlineAtAttemptStart, Duration.ZERO));
}
- if (sidebandData != null && sidebandData.getGfeTiming() != null) {
- serverLatenciesHistogram.record(sidebandData.getGfeTiming(), attributes);
- connectivityErrorCounter.add(0, attributes);
+ if (sidebandData.getGfeTiming() != null) {
+ recorder.serverLatency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ sidebandData.getGfeTiming());
+ recorder.connectivityErrorCount.record(
+ clientInfo, tableId, methodInfo, sidebandData.getResponseParams(), code, 0);
} else {
- connectivityErrorCounter.add(1, attributes);
+ recorder.connectivityErrorCount.record(
+ clientInfo, tableId, methodInfo, sidebandData.getResponseParams(), code, 1);
}
}
@@ -471,21 +411,13 @@ private static double convertToMs(long nanoSeconds) {
@Override
public void setBatchWriteFlowControlTargetQps(double targetQps) {
- Attributes attributes = baseAttributes.toBuilder().put(METHOD_KEY, spanName.toString()).build();
-
- batchWriteFlowControlTargetQps.set(targetQps, attributes);
+ recorder.batchWriteFlowControlTargetQps.record(clientInfo, methodInfo, targetQps);
}
@Override
public void addBatchWriteFlowControlFactor(
double factor, @Nullable Throwable throwable, boolean applied) {
- Attributes attributes =
- baseAttributes.toBuilder()
- .put(METHOD_KEY, spanName.toString())
- .put(STATUS_KEY, extractStatus(throwable).name())
- .put(APPLIED_KEY, applied)
- .build();
-
- batchWriteFlowControlFactorHistogram.record(factor, attributes);
+ recorder.batchWriteFlowControlFactor.record(
+ clientInfo, extractStatus(throwable), applied, methodInfo, factor);
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
index c59c145f7f..0355160b67 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java
@@ -15,37 +15,14 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APPLICATION_BLOCKING_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APP_PROFILE_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES2_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_BLOCKING_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_NAME_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CONNECTIVITY_ERROR_COUNT_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.FIRST_RESPONSE_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.INSTANCE_ID_KEY;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.RETRY_COUNT_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.SERVER_LATENCIES_NAME;
-
import com.google.api.core.InternalApi;
import com.google.api.gax.tracing.ApiTracer;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.BaseApiTracerFactory;
import com.google.api.gax.tracing.SpanName;
-import com.google.cloud.bigtable.Version;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.metrics.DoubleGauge;
-import io.opentelemetry.api.metrics.DoubleHistogram;
-import io.opentelemetry.api.metrics.LongCounter;
-import io.opentelemetry.api.metrics.Meter;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
/**
* {@link ApiTracerFactory} that will generate OpenTelemetry metrics by using the {@link ApiTracer}
@@ -54,151 +31,26 @@
@InternalApi("For internal use only")
public class BuiltinMetricsTracerFactory extends BaseApiTracerFactory {
- private final Attributes attributes;
-
- private static final String MILLISECOND = "ms";
- private static final String COUNT = "1";
-
- private final DoubleHistogram operationLatenciesHistogram;
- private final DoubleHistogram attemptLatenciesHistogram;
- private final DoubleHistogram attemptLatencies2Histogram;
- private final DoubleHistogram serverLatenciesHistogram;
- private final DoubleHistogram firstResponseLatenciesHistogram;
- private final DoubleHistogram clientBlockingLatenciesHistogram;
- private final DoubleHistogram applicationBlockingLatenciesHistogram;
- private final DoubleHistogram remainingDeadlineHistogram;
- private final LongCounter connectivityErrorCounter;
- private final LongCounter retryCounter;
- private final DoubleGauge batchWriteFlowControlTargetQps;
- private final DoubleHistogram batchWriteFlowControlFactorHistogram;
+ private final MetricRegistry.RecorderRegistry recorder;
+ private final ClientInfo clientInfo;
public static BuiltinMetricsTracerFactory create(
- OpenTelemetry openTelemetry, ClientInfo clientInfo) {
- return new BuiltinMetricsTracerFactory(openTelemetry, clientInfo);
+ MetricRegistry.RecorderRegistry recorder, ClientInfo clientInfo) {
+ return new BuiltinMetricsTracerFactory(recorder, clientInfo);
}
- BuiltinMetricsTracerFactory(OpenTelemetry openTelemetry, ClientInfo clientInfo) {
- Meter meter = openTelemetry.getMeter(METER_NAME);
-
- this.attributes =
- Attributes.of(
- BIGTABLE_PROJECT_ID_KEY,
- clientInfo.getInstanceName().getProject(),
- INSTANCE_ID_KEY,
- clientInfo.getInstanceName().getInstance(),
- APP_PROFILE_KEY,
- clientInfo.getAppProfileId(),
- CLIENT_NAME_KEY,
- "bigtable-java/" + Version.VERSION);
-
- operationLatenciesHistogram =
- meter
- .histogramBuilder(OPERATION_LATENCIES_NAME)
- .setDescription(
- "Total time until final operation success or failure, including retries and"
- + " backoff.")
- .setUnit(MILLISECOND)
- .build();
- attemptLatenciesHistogram =
- meter
- .histogramBuilder(ATTEMPT_LATENCIES_NAME)
- .setDescription("Client observed latency per RPC attempt.")
- .setUnit(MILLISECOND)
- .build();
- attemptLatencies2Histogram =
- meter
- .histogramBuilder(ATTEMPT_LATENCIES2_NAME)
- .setDescription("Client observed latency per RPC attempt with transport labels.")
- .setUnit(MILLISECOND)
- .build();
- serverLatenciesHistogram =
- meter
- .histogramBuilder(SERVER_LATENCIES_NAME)
- .setDescription(
- "The latency measured from the moment that the RPC entered the Google data center"
- + " until the RPC was completed.")
- .setUnit(MILLISECOND)
- .build();
- firstResponseLatenciesHistogram =
- meter
- .histogramBuilder(FIRST_RESPONSE_LATENCIES_NAME)
- .setDescription(
- "Latency from operation start until the response headers were received. The"
- + " publishing of the measurement will be delayed until the attempt response"
- + " has been received.")
- .setUnit(MILLISECOND)
- .build();
- clientBlockingLatenciesHistogram =
- meter
- .histogramBuilder(CLIENT_BLOCKING_LATENCIES_NAME)
- .setDescription(
- "The artificial latency introduced by the client to limit the number of outstanding"
- + " requests. The publishing of the measurement will be delayed until the"
- + " attempt trailers have been received.")
- .setUnit(MILLISECOND)
- .build();
- applicationBlockingLatenciesHistogram =
- meter
- .histogramBuilder(APPLICATION_BLOCKING_LATENCIES_NAME)
- .setDescription(
- "The latency of the client application consuming available response data.")
- .setUnit(MILLISECOND)
- .build();
- remainingDeadlineHistogram =
- meter
- .histogramBuilder(REMAINING_DEADLINE_NAME)
- .setDescription(
- "The remaining deadline when the request is sent to grpc. This will either be the"
- + " operation timeout, or the remaining deadline from operation timeout after"
- + " retries and back offs.")
- .setUnit(MILLISECOND)
- .build();
- connectivityErrorCounter =
- meter
- .counterBuilder(CONNECTIVITY_ERROR_COUNT_NAME)
- .setDescription(
- "Number of requests that failed to reach the Google datacenter. (Requests without"
- + " google response headers")
- .setUnit(COUNT)
- .build();
- retryCounter =
- meter
- .counterBuilder(RETRY_COUNT_NAME)
- .setDescription("The number of additional RPCs sent after the initial attempt.")
- .setUnit(COUNT)
- .build();
- batchWriteFlowControlTargetQps =
- meter
- .gaugeBuilder(BATCH_WRITE_FLOW_CONTROL_TARGET_QPS_NAME)
- .setDescription("The current target QPS of the client under batch write flow control.")
- .setUnit("1")
- .build();
- batchWriteFlowControlFactorHistogram =
- meter
- .histogramBuilder(BATCH_WRITE_FLOW_CONTROL_FACTOR_NAME)
- .setDescription(
- "The distribution of batch write flow control factors received from the server.")
- .setUnit("1")
- .build();
+ BuiltinMetricsTracerFactory(MetricRegistry.RecorderRegistry recorder, ClientInfo clientInfo) {
+ this.recorder = recorder;
+ this.clientInfo = clientInfo;
}
@Override
public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) {
- return new BuiltinMetricsTracer(
- operationType,
- spanName,
- attributes,
- operationLatenciesHistogram,
- attemptLatenciesHistogram,
- attemptLatencies2Histogram,
- serverLatenciesHistogram,
- firstResponseLatenciesHistogram,
- clientBlockingLatenciesHistogram,
- applicationBlockingLatenciesHistogram,
- remainingDeadlineHistogram,
- connectivityErrorCounter,
- retryCounter,
- batchWriteFlowControlTargetQps,
- batchWriteFlowControlFactorHistogram);
+ MethodInfo methodInfo =
+ MethodInfo.builder()
+ .setName(spanName.toString())
+ .setStreaming(operationType == OperationType.ServerStreaming)
+ .build();
+ return new BuiltinMetricsTracer(recorder, clientInfo, methodInfo);
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracer.java
index ea849cf8ce..67adfe78d3 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracer.java
@@ -15,17 +15,13 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OUTSTANDING_RPCS_PER_CHANNEL_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.PER_CONNECTION_ERROR_COUNT_NAME;
-
import com.google.api.core.InternalApi;
+import com.google.bigtable.v2.PeerInfo.TransportType;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelObserver;
import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolObserver;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.metrics.LongHistogram;
-import io.opentelemetry.api.metrics.Meter;
+import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings.LoadBalancingStrategy;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@@ -38,34 +34,19 @@ public class ChannelPoolMetricsTracer implements Runnable {
private static final Logger logger = Logger.getLogger(ChannelPoolMetricsTracer.class.getName());
private static final int SAMPLING_PERIOD_SECONDS = 60;
- private final LongHistogram outstandingRpcsHistogram;
- private final LongHistogram perConnectionErrorCountHistogram;
+ private final MetricRegistry.RecorderRegistry recorder;
+ private final ClientInfo clientInfo;
private final AtomicReference bigtableChannelInsightsProviderRef =
new AtomicReference<>();
- private final AtomicReference lbPolicyRef = new AtomicReference<>("ROUND_ROBIN");
+ private final AtomicReference lbPolicyRef =
+ new AtomicReference<>(LoadBalancingStrategy.ROUND_ROBIN);
// Attributes for unary and streaming RPCs, built on demand in run()
- public ChannelPoolMetricsTracer(OpenTelemetry openTelemetry) {
- Meter meter = openTelemetry.getMeter(METER_NAME);
- this.outstandingRpcsHistogram =
- meter
- .histogramBuilder(OUTSTANDING_RPCS_PER_CHANNEL_NAME)
- .ofLongs()
- .setDescription(
- "A distribution of the number of outstanding RPCs per connection in the client"
- + " pool, sampled periodically.")
- .setUnit("1")
- .build();
-
- this.perConnectionErrorCountHistogram =
- meter
- .histogramBuilder(PER_CONNECTION_ERROR_COUNT_NAME)
- .ofLongs()
- .setDescription("Distribution of counts of channels per 'error count per minute'.")
- .setUnit("1")
- .build();
+ public ChannelPoolMetricsTracer(MetricRegistry.RecorderRegistry recorder, ClientInfo clientInfo) {
+ this.recorder = recorder;
+ this.clientInfo = clientInfo;
}
/**
@@ -77,7 +58,7 @@ public void registerChannelInsightsProvider(BigtableChannelPoolObserver channelI
}
/** Register the current lb policy * */
- public void registerLoadBalancingStrategy(String lbPolicy) {
+ public void registerLoadBalancingStrategy(LoadBalancingStrategy lbPolicy) {
this.lbPolicyRef.set(lbPolicy);
}
@@ -100,45 +81,25 @@ public void run() {
return;
}
- String lbPolicy = lbPolicyRef.get();
-
- Attributes dpUnaryAttrs =
- Attributes.builder()
- .put("transport_type", "directpath")
- .put("streaming", false)
- .put("lb_policy", lbPolicy)
- .build();
- Attributes dpStreamingAttrs =
- Attributes.builder()
- .put("transport_type", "directpath")
- .put("streaming", true)
- .put("lb_policy", lbPolicy)
- .build();
- Attributes cpUnaryAttrs =
- Attributes.builder()
- .put("transport_type", "cloudpath")
- .put("streaming", false)
- .put("lb_policy", lbPolicy)
- .build();
- Attributes cpStreamingAttrs =
- Attributes.builder()
- .put("transport_type", "cloudpath")
- .put("streaming", true)
- .put("lb_policy", lbPolicy)
- .build();
+ LoadBalancingStrategy lbPolicy = lbPolicyRef.get();
for (BigtableChannelObserver info : channelInsights) {
- Attributes unaryAttrs = info.isAltsChannel() ? dpUnaryAttrs : cpUnaryAttrs;
- Attributes streamingAttrs = info.isAltsChannel() ? dpStreamingAttrs : cpStreamingAttrs;
+ TransportType transportType =
+ info.isAltsChannel()
+ ? TransportType.TRANSPORT_TYPE_DIRECT_ACCESS
+ : TransportType.TRANSPORT_TYPE_CLOUD_PATH;
long currentOutstandingUnaryRpcs = info.getOutstandingUnaryRpcs();
long currentOutstandingStreamingRpcs = info.getOutstandingStreamingRpcs();
- outstandingRpcsHistogram.record(currentOutstandingUnaryRpcs, unaryAttrs);
- outstandingRpcsHistogram.record(currentOutstandingStreamingRpcs, streamingAttrs);
+
+ recorder.channelPoolOutstandingRpcs.record(
+ clientInfo, transportType, lbPolicy, false, currentOutstandingUnaryRpcs);
+ recorder.channelPoolOutstandingRpcs.record(
+ clientInfo, transportType, lbPolicy, true, currentOutstandingStreamingRpcs);
long errors = info.getAndResetErrorCount();
// Record errors with empty attributes.
- perConnectionErrorCountHistogram.record(errors, Attributes.empty());
+ recorder.perConnectionErrorCount.record(clientInfo, errors);
}
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
index 73f54ad810..448d8b442b 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
@@ -191,7 +191,7 @@ private void recordAttemptCompletion(@Nullable Throwable throwable) {
if (sidebandData != null && sidebandData.getGfeTiming() != null) {
measures
- .put(RpcMeasureConstants.BIGTABLE_GFE_LATENCY, sidebandData.getGfeTiming())
+ .put(RpcMeasureConstants.BIGTABLE_GFE_LATENCY, sidebandData.getGfeTiming().toMillis())
.put(RpcMeasureConstants.BIGTABLE_GFE_HEADER_MISSING_COUNT, 0L);
} else {
measures.put(RpcMeasureConstants.BIGTABLE_GFE_HEADER_MISSING_COUNT, 1L);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
index 7381b220e2..4af8abb869 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java
@@ -25,14 +25,11 @@
import com.google.bigtable.v2.MaterializedViewName;
import com.google.bigtable.v2.MutateRowRequest;
import com.google.bigtable.v2.MutateRowsRequest;
-import com.google.bigtable.v2.PeerInfo;
import com.google.bigtable.v2.ReadChangeStreamRequest;
import com.google.bigtable.v2.ReadModifyWriteRowRequest;
import com.google.bigtable.v2.ReadRowsRequest;
-import com.google.bigtable.v2.ResponseParams;
import com.google.bigtable.v2.SampleRowKeysRequest;
import com.google.bigtable.v2.TableName;
-import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
import com.google.common.collect.ImmutableMap;
import io.grpc.Metadata;
import io.grpc.Status;
@@ -40,9 +37,7 @@
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.Optional;
import java.util.concurrent.CancellationException;
import javax.annotation.Nullable;
@@ -133,33 +128,4 @@ static Map> createStatsHeaders(ApiCallContext apiCallContex
}
return headers.build();
}
-
- public static String formatTransportTypeMetricLabel(
- MetadataExtractorInterceptor.SidebandData sidebandData) {
- return Optional.ofNullable(sidebandData)
- .flatMap(s -> Optional.ofNullable(s.getPeerInfo()))
- .map(PeerInfo::getTransportType)
- .orElse(PeerInfo.TransportType.TRANSPORT_TYPE_UNKNOWN)
- .name()
- .replace("TRANSPORT_TYPE_", "")
- .toLowerCase(Locale.ENGLISH);
- }
-
- public static String formatClusterIdMetricLabel(
- @Nullable MetadataExtractorInterceptor.SidebandData sidebandData) {
- return Optional.ofNullable(sidebandData)
- .flatMap(d -> Optional.ofNullable(d.getResponseParams()))
- .map(ResponseParams::getClusterId)
- .filter(s -> !s.isEmpty())
- .orElse("");
- }
-
- public static String formatZoneIdMetricLabel(
- @Nullable MetadataExtractorInterceptor.SidebandData sidebandData) {
- return Optional.ofNullable(sidebandData)
- .flatMap(d -> Optional.ofNullable(d.getResponseParams()))
- .map(ResponseParams::getZoneId)
- .filter(s -> !s.isEmpty())
- .orElse("global");
- }
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java
index e21c100c9c..3c71da79c6 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableTransportChannelProvider.java
@@ -166,7 +166,7 @@ public TransportChannel getTransportChannel() throws IOException {
if (channelPoolMetricsTracer != null) {
channelPoolMetricsTracer.registerChannelInsightsProvider(btChannelPool::getChannelInfos);
channelPoolMetricsTracer.registerLoadBalancingStrategy(
- btPoolSettings.getLoadBalancingStrategy().name());
+ btPoolSettings.getLoadBalancingStrategy());
}
return GrpcTransportChannel.create(btChannelPool);
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
index 2aaea4a5e5..47d1078b9d 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java
@@ -65,6 +65,7 @@
import com.google.cloud.bigtable.Version;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
import com.google.cloud.bigtable.data.v2.FakeServiceBuilder;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId;
import com.google.cloud.bigtable.data.v2.models.Query;
@@ -197,7 +198,11 @@ public void setUp() throws Exception {
OpenTelemetrySdk otel =
OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
- BuiltinMetricsTracerFactory facotry = new BuiltinMetricsTracerFactory(otel, clientInfo);
+ MetricRegistry mr = new MetricRegistry();
+
+ BuiltinMetricsTracerFactory facotry =
+ new BuiltinMetricsTracerFactory(
+ mr.newRecorderRegistry(otel.getMeterProvider()), clientInfo);
// Add an interceptor to add server-timing in headers
ServerInterceptor trailersInterceptor =
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracerTest.java
index 855709503e..e33ffe37e3 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracerTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/ChannelPoolMetricsTracerTest.java
@@ -22,8 +22,12 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
+import com.google.bigtable.v2.InstanceName;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelObserver;
import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolObserver;
+import com.google.cloud.bigtable.gaxx.grpc.BigtableChannelPoolSettings.LoadBalancingStrategy;
import com.google.common.collect.ImmutableList;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
@@ -68,12 +72,21 @@ public class ChannelPoolMetricsTracerTest {
@Before
public void setUp() {
metricReader = InMemoryMetricReader.create();
+ ClientInfo clientInfo =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of("fake-project", "fake-instance"))
+ .setAppProfileId("fake-profile")
+ .build();
SdkMeterProvider meterProvider =
SdkMeterProvider.builder().registerMetricReader(metricReader).build();
OpenTelemetry openTelemetry =
OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build();
- tracker = new ChannelPoolMetricsTracer(openTelemetry);
+ MetricRegistry mr = new MetricRegistry();
+
+ tracker =
+ new ChannelPoolMetricsTracer(
+ mr.newRecorderRegistry(openTelemetry.getMeterProvider()), clientInfo);
runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
// Configure mockScheduler to capture the runnable when tracker.start() is called
@@ -147,7 +160,7 @@ private static Attributes getExpectedAttributes(String lbPolicy, boolean streami
public void testSingleRun() {
// Arrange
tracker.registerChannelInsightsProvider(mockInsightsProvider);
- tracker.registerLoadBalancingStrategy("LEAST_IN_FLIGHT");
+ tracker.registerLoadBalancingStrategy(LoadBalancingStrategy.LEAST_IN_FLIGHT);
tracker.start(mockScheduler);
// Outstanding RPCs
@@ -205,7 +218,7 @@ public void testSingleRun() {
public void testMultipleRuns() {
// Arrange
tracker.registerChannelInsightsProvider(mockInsightsProvider);
- tracker.registerLoadBalancingStrategy("ROUND_ROBIN");
+ tracker.registerLoadBalancingStrategy(LoadBalancingStrategy.ROUND_ROBIN);
tracker.start(mockScheduler);
// First run
From a0a042bdef1dbab462753584023d59aac60c684f Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Thu, 26 Feb 2026 13:51:22 -0500
Subject: [PATCH 19/33] chore: align transport type with previous labels
(#2809)
Change-Id: Ia98cb0f3987472d5a1751591ecb2df848348b63b
---
.../data/v2/internal/csm/attributes/Util.java | 48 +++++++++++++++----
.../v2/internal/csm/attributes/UtilTest.java | 7 ++-
2 files changed, 45 insertions(+), 10 deletions(-)
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
index 9379f4726d..818e0b8859 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java
@@ -19,6 +19,7 @@
import com.google.bigtable.v2.PeerInfo;
import com.google.bigtable.v2.PeerInfo.TransportType;
import com.google.bigtable.v2.ResponseParams;
+import com.google.common.annotations.VisibleForTesting;
import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -42,17 +43,48 @@ public static String formatTransportType(@Nullable PeerInfo peerInfo) {
}
public static String transportTypeToString(TransportType transportType) {
- if (transportType == TransportType.TRANSPORT_TYPE_UNKNOWN) {
- return "none";
+ String label = transportTypeToStringWithoutFallback(transportType);
+ if (label != null) {
+ return label;
}
- if (transportType == TransportType.UNRECOGNIZED) {
- return "unrecognized";
+ // In case the client is running with a newer version of protos
+ if (transportType.name().startsWith(TRANSPORT_TYPE_PREFIX)) {
+ return transportType
+ .name()
+ .substring(TRANSPORT_TYPE_PREFIX.length())
+ .toLowerCase(Locale.ENGLISH);
+ } else {
+ return transportType.name();
}
+ }
- return transportType
- .name()
- .substring(TRANSPORT_TYPE_PREFIX.length())
- .toLowerCase(Locale.ENGLISH);
+ @VisibleForTesting
+ static String transportTypeToStringWithoutFallback(TransportType transportType) {
+ if (transportType == null) {
+ return "null";
+ }
+ switch (transportType) {
+ case TRANSPORT_TYPE_UNKNOWN:
+ return "unknown";
+ case TRANSPORT_TYPE_EXTERNAL:
+ return "external";
+ case TRANSPORT_TYPE_CLOUD_PATH:
+ return "cloudpath";
+ case TRANSPORT_TYPE_DIRECT_ACCESS:
+ return "directpath";
+ case TRANSPORT_TYPE_SESSION_UNKNOWN:
+ return "session_unknown";
+ case TRANSPORT_TYPE_SESSION_EXTERNAL:
+ return "session_external";
+ case TRANSPORT_TYPE_SESSION_CLOUD_PATH:
+ return "session_cloudpath";
+ case TRANSPORT_TYPE_SESSION_DIRECT_ACCESS:
+ return "session_directpath";
+ case UNRECOGNIZED:
+ return "unrecognized";
+ default:
+ return null;
+ }
}
public static String formatClusterIdMetricLabel(@Nullable ResponseParams clusterInfo) {
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java
index f75bb81727..78b6c18b8b 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/UtilTest.java
@@ -16,6 +16,8 @@
package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import com.google.bigtable.v2.PeerInfo.TransportType;
import org.junit.jupiter.api.Test;
@@ -23,8 +25,9 @@ class UtilTest {
@Test
void ensureAllTransportTypeHaveExpectedPrefix() {
for (TransportType type : TransportType.values()) {
- // Ensure that no variant throws an error
- Util.transportTypeToString(type);
+ assertWithMessage("%s should have a mapping", type)
+ .that(Util.transportTypeToStringWithoutFallback(type))
+ .isNotNull();
}
}
}
From 335414dd0803e7edb5e7c01b8c462513efd652df Mon Sep 17 00:00:00 2001
From: Igor Bernstein
Date: Thu, 26 Feb 2026 15:52:31 -0500
Subject: [PATCH 20/33] chore: update the exporter to use MetricRegistry
(#2810)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)
- [ ] Rollback plan is reviewed and LGTMed
- [ ] All new data plane features have a completed end to end testing plan
Fixes # ☕️
If you write sample code, please follow the [samples format](
https://togithub.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
---
.../data/v2/internal/csm/MetricRegistry.java | 2 +-
.../data/v2/internal/csm/MetricsImpl.java | 12 +-
.../internal/csm/attributes/ClientInfo.java | 2 +-
.../v2/internal/csm/attributes/EnvInfo.java | 2 +-
.../data/v2/stub/BigtableClientContext.java | 4 +
.../BigtableCloudMonitoringExporter.java | 328 ++++++------------
.../data/v2/stub/metrics/Converter.java | 218 ++++++++++++
.../BigtableCloudMonitoringExporterTest.java | 62 ++--
8 files changed, 372 insertions(+), 258 deletions(-)
create mode 100644 google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Converter.java
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java
index f485e79e4f..266ac7bc13 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java
@@ -165,7 +165,7 @@ List getGrpcMetricNames() {
return ImmutableList.copyOf(grpcMetricNames);
}
- MetricWrapper> getMetric(String name) {
+ public MetricWrapper> getMetric(String name) {
return metrics.get(name);
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
index 3ae54c5313..c7bf859431 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java
@@ -23,6 +23,7 @@
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry.RecorderRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableCloudMonitoringExporter;
import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants;
import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsTracerFactory;
@@ -72,6 +73,7 @@ public class MetricsImpl implements Metrics, Closeable {
private final List> tasks = new ArrayList<>();
public MetricsImpl(
+ MetricRegistry metricRegistry,
ClientInfo clientInfo,
ApiTracerFactory userTracerFactory,
@Nullable OpenTelemetrySdk internalOtel,
@@ -79,7 +81,7 @@ public MetricsImpl(
Tagger ocTagger,
StatsRecorder ocRecorder,
ScheduledExecutorService executor) {
- metricRegistry = new MetricRegistry();
+ this.metricRegistry = metricRegistry;
this.userTracerFactory = Preconditions.checkNotNull(userTracerFactory);
this.internalOtel = internalOtel;
@@ -168,6 +170,7 @@ public ChannelPoolMetricsTracer getChannelPoolMetricsTracer() {
}
public static OpenTelemetrySdk createBuiltinOtel(
+ MetricRegistry metricRegistry,
ClientInfo clientInfo,
@Nullable Credentials defaultCredentials,
@Nullable String metricsEndpoint,
@@ -194,7 +197,12 @@ public static OpenTelemetrySdk createBuiltinOtel(
MetricExporter publicExporter =
BigtableCloudMonitoringExporter.create(
- clientInfo, credentials, metricsEndpoint, universeDomain, executor);
+ metricRegistry,
+ EnvInfo::detect,
+ clientInfo,
+ credentials,
+ metricsEndpoint,
+ universeDomain);
PeriodicMetricReaderBuilder readerBuilder =
PeriodicMetricReader.builder(publicExporter).setExecutor(executor);
meterProvider.registerMetricReader(readerBuilder.build());
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java
index 0d4717dfe9..64c4b211b2 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.java
@@ -42,7 +42,7 @@ public static Builder builder() {
@AutoValue.Builder
public abstract static class Builder {
- protected abstract Builder setClientName(String name);
+ public abstract Builder setClientName(String name);
public abstract Builder setInstanceName(InstanceName name);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
index cfc9182881..b7afb73ee9 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
@@ -77,7 +77,7 @@ public static Builder builder() {
@AutoValue.Builder
public abstract static class Builder {
- protected abstract Builder setUid(String uid);
+ public abstract Builder setUid(String uid);
public abstract Builder setPlatform(String platform);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
index c82b0a8d02..c4bef24798 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableClientContext.java
@@ -28,6 +28,7 @@
import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials;
import com.google.bigtable.v2.InstanceName;
import com.google.cloud.bigtable.data.v2.internal.JwtCredentialsWithAudience;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.Metrics;
import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
@@ -101,6 +102,7 @@ public static BigtableClientContext create(
FixedExecutorProvider.create(backgroundExecutor, shouldAutoClose);
builder.setBackgroundExecutorProvider(executorProvider);
+ MetricRegistry metricRegistry = new MetricRegistry();
// Set up OpenTelemetry
@Nullable OpenTelemetry userOtel = null;
if (settings.getMetricsProvider() instanceof CustomOpenTelemetryMetricsProvider) {
@@ -113,6 +115,7 @@ public static BigtableClientContext create(
if (settings.areInternalMetricsEnabled()) {
builtinOtel =
MetricsImpl.createBuiltinOtel(
+ metricRegistry,
clientInfo,
credentials,
settings.getMetricsEndpoint(),
@@ -125,6 +128,7 @@ public static BigtableClientContext create(
Metrics metrics =
new MetricsImpl(
+ metricRegistry,
clientInfo,
settings.getTracerFactory(),
builtinOtel,
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
index 2aba290aff..3bec1fc1e7 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java
@@ -15,40 +15,20 @@
*/
package com.google.cloud.bigtable.data.v2.stub.metrics;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.APPLICATION_BLOCKING_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES2_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.ATTEMPT_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CLIENT_BLOCKING_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.CONNECTIVITY_ERROR_COUNT_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.FIRST_RESPONSE_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.PER_CONNECTION_ERROR_COUNT_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.RETRY_COUNT_NAME;
-import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.SERVER_LATENCIES_NAME;
-
-import com.google.api.MonitoredResource;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
-import com.google.api.core.InternalApi;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.FixedCredentialsProvider;
-import com.google.api.gax.core.FixedExecutorProvider;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.api.gax.rpc.PermissionDeniedException;
import com.google.auth.Credentials;
+import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
+import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
import com.google.cloud.monitoring.v3.MetricServiceClient;
import com.google.cloud.monitoring.v3.MetricServiceSettings;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
-import com.google.common.base.Supplier;
-import com.google.common.base.Suppliers;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.monitoring.v3.CreateTimeSeriesRequest;
@@ -65,28 +45,19 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Optional;
-import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.stream.Collectors;
import javax.annotation.Nullable;
-/**
- * Bigtable Cloud Monitoring OpenTelemetry Exporter.
- *
- *