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;
+
+ 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);
+
+ 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 000000000000..cb2a0c9f19de
--- /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 000000000000..e141533e368b
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfo.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.attributes;
+
+import com.google.auto.value.AutoValue;
+import com.google.bigtable.v2.InstanceName;
+import com.google.cloud.bigtable.Version;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * 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 {
+ private static final Logger logger = Logger.getLogger(ClientInfo.class.getName());
+
+ private static final AtomicLong uidSuffix = new AtomicLong(0);
+
+ /** 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 String getUid();
+
+ 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)
+ .setUid(computeUid() + "-" + uidSuffix.getAndIncrement());
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ protected abstract Builder setClientName(String name);
+
+ protected abstract Builder setUid(String uid);
+
+ public abstract Builder setInstanceName(InstanceName name);
+
+ public abstract Builder setAppProfileId(String appProfileId);
+
+ public abstract ClientInfo 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;
+ }
+}
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 000000000000..1c8a73b2f843
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfo.java
@@ -0,0 +1,164 @@
+/*
+ * 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.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+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");
+
+ /** 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();
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ 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();
+ }
+
+ 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 000000000000..122e5fe5ba1e
--- /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 000000000000..cf9c2a114ea2
--- /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 000000000000..c369f5a556d9
--- /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_target_qps";
+
+ 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 current target QPS of the client under batch write flow control.")
+ .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 000000000000..fb6f55894f88
--- /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 000000000000..597d34fda58f
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientChannelPoolOutstandingRpcs.java
@@ -0,0 +1,55 @@
+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.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;
+
+public class ClientChannelPoolOutstandingRpcs extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/connection_pool/outstanding_rpcs";
+
+ 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()
+ .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 000000000000..caef353e4708
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientDpCompatGuage.java
@@ -0,0 +1,58 @@
+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 000000000000..8090d645a270
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/ClientPerConnectionErrorCount.java
@@ -0,0 +1,102 @@
+/*
+ * 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.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.Set;
+
+public class ClientPerConnectionErrorCount extends MetricWrapper {
+ private static final String NAME =
+ "bigtable.googleapis.com/internal/client/per_connection_error_count";
+ // 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());
+ }
+ });
+ 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.AGGREGATION_PER_CONNECTION_ERROR_COUNT_HISTOGRAM)
+ .build();
+ }
+
+ public void record(ClientInfo clientInfo, long value) {
+ Attributes attributes =
+ getSchema()
+ .createResourceAttrs(clientInfo)
+ .put(MetricLabels.BIGTABLE_PROJECT_ID_KEY, clientInfo.getInstanceName().getProject())
+ .put(MetricLabels.CLIENT_UID, clientInfo.getUid())
+ .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 000000000000..d1284bbae630
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/Constants.java
@@ -0,0 +1,152 @@
+/*
+ * 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 com.google.common.collect.ImmutableSortedSet;
+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 =
+ ImmutableSortedSet.naturalOrder()
+ // Match `bigtable.googleapis.com/frontend_server/handler_latencies` buckets
+ .add(
+ new Double[] {
+ 0d, 1d, 2d, 3d, 4d, 5d, 6d, 8d, 10d,
+ 13d, 16d, 20d, 25d, 30d, 40d, 50d, 65d, 80d,
+ 100d, 130d, 160d, 200d, 250d, 300d, 400d, 500d, 650d,
+ 800d, 900d, 1_000d, 2_000d, 3_000d, 4_000d, 5_000d, 6_000d, 10_000d,
+ 20_000d, 50_000d, 100_000d, 200_000d, 500_000d, 1_000_000d, 2_000_000d, 5_000_000d
+ })
+ // TODO: figure out what actual additional buckets we want to jetstream
+ // add 100us buckets for the first 3ms
+ .addAll(generateLinearSeq(0, 3, 0.1))
+ .build()
+ .asList();
+ static final List HIGH_RES_AGGREGATION_WITH_MILLIS_HISTOGRAM =
+ ImmutableSortedSet.naturalOrder()
+ .add(
+ new Double[] {
+ 0d, 1d, 2d, 3d, 4d, 5d, 6d, 8d, 10d,
+ 13d, 16d, 20d, 25d, 30d, 40d, 50d, 65d, 80d,
+ 100d, 130d, 160d, 200d, 250d, 300d, 400d, 500d, 650d,
+ 800d, 900d, 1_000d, 2_000d, 3_000d, 4_000d, 5_000d, 6_000d, 10_000d,
+ 20_000d, 50_000d, 100_000d, 200_000d
+ })
+ // add 50us buckets for the first 3ms
+ .addAll(generateLinearSeq(0, 3, 0.05))
+ .build()
+ .asList();
+ static final List AGGREGATION_PER_CONNECTION_ERROR_COUNT_HISTOGRAM =
+ ImmutableList.builder()
+ .add(0L)
+ .addAll(generateGeometricSeq(1, 64))
+ .addAll(generateGeometricSeq(125, 1_000_000L))
+ .build();
+ static final List PACEMAKER_BUCKET =
+ ImmutableList.builder()
+ // Up to 67,108,864, ~1 minute in microseconds
+ .addAll(generateExponentialSeq(1.0, 13, 4))
+ .build();
+
+ 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();
+ }
+
+ 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 000000000000..e4ddc1216536
--- /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 000000000000..a6c882d820e7
--- /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 000000000000..87aef7bae809
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/PacemakerDelay.java
@@ -0,0 +1,68 @@
+/*
+ * 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 io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
+import io.opentelemetry.api.metrics.Meter;
+import java.time.Duration;
+
+/**
+ * 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";
+
+ 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.PACEMAKER_BUCKET)
+ .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 000000000000..04d93ae6498e
--- /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(), clientInfo.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 000000000000..7d3378f34d69
--- /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(), clientInfo.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 000000000000..0a76d08c2452
--- /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(), clientInfo.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 000000000000..9c7bf7fa3da7
--- /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(), clientInfo.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 000000000000..dbc390388ff2
--- /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(), clientInfo.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 000000000000..dc75bfa82e14
--- /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(), clientInfo.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 000000000000..c8f67cef59a5
--- /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(), clientInfo.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 000000000000..4187e01a8c70
--- /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(), clientInfo.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 000000000000..226f44d0f325
--- /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(), clientInfo.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 000000000000..1302488af3d3
--- /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(), clientInfo.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 000000000000..91553185f4bb
--- /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(), clientInfo.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 000000000000..f0489b1102ff
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/metrics/package-info.java
@@ -0,0 +1,4 @@
+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 000000000000..99d8a5cdf97d
--- /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.fromClientInfo("uuid", ClientInfo::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 000000000000..efc7787d717c
--- /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.fromClientInfo("uuid", ClientInfo::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 000000000000..a5d621acbc2b
--- /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 000000000000..f536f7383724
--- /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/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 c89f36819064..21f3af0bc22f 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,12 @@ 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 +188,7 @@ public static BigtableClientContext create(
return new BigtableClientContext(
false,
- instanceName,
- appProfileId,
+ clientInfo,
clientContext,
userTracerFactory,
builtinOtel,
@@ -224,8 +227,7 @@ private static void configureGrpcOtel(
private BigtableClientContext(
boolean isChild,
- InstanceName instanceName,
- String appProfileId,
+ ClientInfo clientInfo,
ClientContext clientContext,
ApiTracerFactory userTracerFactory,
@Nullable OpenTelemetrySdk builtinOtel,
@@ -235,8 +237,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 +248,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 +265,8 @@ private BigtableClientContext(
.build();
}
- public InstanceName getInstanceName() {
- return instanceName;
- }
-
- public String getAppProfileId() {
- return appProfileId;
+ public ClientInfo getClientInfo() {
+ return clientInfo;
}
@Nullable
@@ -290,8 +287,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 6f0ffdc60f8f..abf3eb49dbc9 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
@@ -184,9 +184,9 @@ public EnhancedBigtableStub(
this.bigtableClientContext = clientContext;
this.requestContext =
RequestContext.create(
- clientContext.getInstanceName().getProject(),
- clientContext.getInstanceName().getInstance(),
- clientContext.getAppProfileId());
+ clientContext.getClientInfo().getInstanceName().getProject(),
+ clientContext.getClientInfo().getInstanceName().getInstance(),
+ clientContext.getClientInfo().getAppProfileId());
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/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java
index 546ea41c9fc7..a46c8a2e197a 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,37 +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.MetricRegistry.RecorderRegistry;
+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.math.IntMath;
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 io.grpc.Status;
import java.time.Duration;
-import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -58,9 +43,8 @@
* 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 final ClientInfo clientInfo;
+ private final MethodInfo methodInfo;
// Operation level metrics
private final AtomicBoolean operationFinishedEarly = new AtomicBoolean();
@@ -90,67 +74,20 @@ 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;
-
- 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;
+ private volatile MetadataExtractorInterceptor.SidebandData sidebandData = new SidebandData();
+
+ MetricRegistry.RecorderRegistry recorder;
+
+ BuiltinMetricsTracer(RecorderRegistry recorder, ClientInfo clientInfo, MethodInfo methodInfo) {
+ this.clientInfo = clientInfo;
+ this.methodInfo = methodInfo;
+
+ this.recorder = recorder;
}
@Override
@@ -344,40 +281,42 @@ private void recordOperationCompletion(@Nullable Throwable status) {
if (!opFinished.compareAndSet(false, true)) {
return;
}
- long operationLatencyNano = operationTimer.elapsed(TimeUnit.NANOSECONDS);
-
- boolean isStreaming = operationType == OperationType.ServerStreaming;
- String statusStr = extractStatus(status);
-
- // 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, statusStr)
- .build();
+ Duration operationLatency = operationTimer.elapsed();
+
+ Status.Code code = extractStatus(status);
// 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, operationLatency);
// 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);
+ long applicationLatencyNano = operationLatency.toNanos() - totalServerLatencyNano.get();
+ recorder.applicationBlockingLatency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ Duration.ofNanos(applicationLatencyNano));
+
+ if (methodInfo.getStreaming() && "ReadRows".equals(methodInfo.getName())) {
+ recorder.firstResponseLantency.record(
+ clientInfo,
+ tableId,
+ methodInfo,
+ sidebandData.getResponseParams(),
+ code,
+ firstResponsePerOpTimer.elapsed());
}
}
@@ -395,8 +334,6 @@ private void recordAttemptCompletion(@Nullable Throwable status) {
}
}
- boolean isStreaming = operationType == OperationType.ServerStreaming;
-
// Patch the status 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
@@ -404,62 +341,58 @@ private void recordAttemptCompletion(@Nullable Throwable status) {
status = status.getCause();
}
- String statusStr = extractStatus(status);
-
- 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, statusStr)
- .build();
+ Status.Code code = extractStatus(status);
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,
+ Duration.ofMillis(Math.max(0, remainingDeadlineAtAttemptStart)));
}
- 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,
+ Duration.ofMillis(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);
}
}
@@ -470,21 +403,16 @@ 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, tableId, methodInfo, sidebandData.getResponseParams(), targetQps);
}
@Override
public void addBatchWriteFlowControlFactor(
double factor, @Nullable Throwable status, boolean applied) {
- Attributes attributes =
- baseAttributes.toBuilder()
- .put(METHOD_KEY, spanName.toString())
- .put(STATUS_KEY, extractStatus(status))
- .put(APPLIED_KEY, applied)
- .build();
-
- batchWriteFlowControlFactorHistogram.record(factor, attributes);
+ Status.Code code = Util.extractStatus(status);
+
+ recorder.batchWriteFlowControlFactor.record(
+ clientInfo, tableId, methodInfo, sidebandData.getResponseParams(), code, applied, 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 eb8089b1c6b4..ef564885700b 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,31 +15,15 @@
*/
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.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.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.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 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.MetricRegistry;
+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.MethodInfo;
import java.io.IOException;
/**
@@ -48,142 +32,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, Attributes attributes) throws IOException {
- return new BuiltinMetricsTracerFactory(openTelemetry, attributes);
+ MetricRegistry.RecorderRegistry recorder, ClientInfo clientInfo) throws IOException {
+ return new BuiltinMetricsTracerFactory(recorder, clientInfo);
}
- BuiltinMetricsTracerFactory(OpenTelemetry openTelemetry, Attributes attributes) {
- this.attributes = attributes;
- Meter meter = openTelemetry.getMeter(METER_NAME);
-
- 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(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.getMethodName())
+ .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/MetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java
index 53b4ca87a807..73f54ad810a1 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 cc341c994e21..7ea809b4c013 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,17 +15,11 @@
*/
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.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;
@@ -45,20 +39,20 @@
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.MetricRegistry;
+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.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;
-import io.grpc.StatusException;
-import io.grpc.StatusRuntimeException;
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.api.common.Attributes;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.InstrumentSelector;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
@@ -75,7 +69,6 @@
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
-import java.util.concurrent.CancellationException;
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
@@ -87,25 +80,22 @@ 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;
+ }
+ 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) {
@@ -241,14 +231,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 +251,28 @@ 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) throws IOException {
+
+ MetricRegistry mr = new MetricRegistry();
+ RecorderRegistry recorder = mr.newRecorderRegistry(otel.getMeterProvider());
+ return BuiltinMetricsTracerFactory.create(recorder, clientInfo);
}
}
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 000000000000..8abb87541770
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/ClientInfoTest.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 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+.*");
+ }
+
+ @Test
+ void testUid() {
+ ClientInfo info1 =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of("fake-project", "fake-instance"))
+ .setAppProfileId("fake-app-profile")
+ .build();
+
+ ClientInfo info2 =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of("fake-project", "fake-instance"))
+ .setAppProfileId("fake-app-profile")
+ .build();
+
+ assertThat(info1.getUid()).isNotEmpty();
+ assertThat(info2.getUid()).isNotEmpty();
+ assertThat(info1.getUid()).isNotEqualTo(info2.getUid());
+ }
+}
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 000000000000..152369133f1c
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/EnvInfoTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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 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 000000000000..f75bb8172741
--- /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);
+ }
+ }
+}
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 1ffccab7dd6a..dc8c17fa1eb9 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,9 @@
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.MetricRegistry.RecorderRegistry;
+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 +167,8 @@ public class BuiltinMetricsTracerTest {
private int batchElementCount = 2;
- private Attributes baseAttributes;
+ private ClientInfo clientInfo;
+ private Attributes expectedBaseAttributes;
private InMemoryMetricReader metricReader;
@@ -175,7 +180,12 @@ public class BuiltinMetricsTracerTest {
public void setUp() throws Exception {
metricReader = InMemoryMetricReader.create();
- baseAttributes =
+ clientInfo =
+ ClientInfo.builder()
+ .setInstanceName(InstanceName.of(PROJECT_ID, INSTANCE_ID))
+ .setAppProfileId(APP_PROFILE_ID)
+ .build();
+ expectedBaseAttributes =
Attributes.builder()
.put(BuiltinMetricsConstants.BIGTABLE_PROJECT_ID_KEY, PROJECT_ID)
.put(BuiltinMetricsConstants.INSTANCE_ID_KEY, INSTANCE_ID)
@@ -192,7 +202,9 @@ public void setUp() throws Exception {
OpenTelemetrySdk otel =
OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
- BuiltinMetricsTracerFactory facotry = BuiltinMetricsTracerFactory.create(otel, baseAttributes);
+ MetricRegistry mr = new MetricRegistry();
+ RecorderRegistry recorder = mr.newRecorderRegistry(otel.getSdkMeterProvider());
+ BuiltinMetricsTracerFactory facotry = BuiltinMetricsTracerFactory.create(recorder, clientInfo);
// Add an interceptor to add server-timing in headers
ServerInterceptor trailersInterceptor =
@@ -302,7 +314,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 +339,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 +384,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 +404,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 +421,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 +430,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 +485,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 +520,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 +549,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 +571,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 +582,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 +610,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 +638,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 +658,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 +669,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 +698,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 +724,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 +751,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 +777,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 +799,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 +819,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 +852,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 +882,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 +912,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 +920,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 +944,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 +952,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 +977,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 +985,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")
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 824d8be307bc..b9e72855058a 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((Throwable) 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");
}
}