diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index 102de757844c..76285f5dfa76 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -338,11 +338,26 @@ junit test + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter + test + org.mockito mockito-core test + + org.mockito + mockito-junit-jupiter + test + com.google.guava guava-testlib diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java index d73fbe2a12db..d529f02eb24b 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactory.java @@ -109,7 +109,8 @@ public BigtableDataClient createDefault() { try { BigtableClientContext ctx = sharedClientContext.createChild( - sharedClientContext.getInstanceName(), sharedClientContext.getAppProfileId()); + sharedClientContext.getClientInfo().getInstanceName(), + sharedClientContext.getClientInfo().getAppProfileId()); return new BigtableDataClient(new EnhancedBigtableStub(perOpSettings, ctx)); } catch (IOException e) { @@ -130,7 +131,8 @@ public BigtableDataClient createDefault() { */ public BigtableDataClient createForAppProfile(@Nonnull String appProfileId) throws IOException { BigtableClientContext ctx = - sharedClientContext.createChild(sharedClientContext.getInstanceName(), appProfileId); + sharedClientContext.createChild( + sharedClientContext.getClientInfo().getInstanceName(), appProfileId); return new BigtableDataClient(new EnhancedBigtableStub(perOpSettings, ctx)); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java new file mode 100644 index 000000000000..7ca29783bb27 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java @@ -0,0 +1,212 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigtable.data.v2.internal.csm; + +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlFactor; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlTargetQps; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientChannelPoolOutstandingRpcs; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientDpCompatGuage; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientPerConnectionErrorCount; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.GrpcMetric; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.MetricWrapper; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.PacemakerDelay; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableApplicationBlockingLatency; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency2; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableClientBlockingLatency; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableConnectivityErrorCount; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableFirstResponseLatency; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableOperationLatency; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRemainingDeadline; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRetryCount; +import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableServerLatency; +import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Repository for all client metrics. This class has 2 audiences: + * + * + */ +public class MetricRegistry { + static final String METER_NAME = "bigtable.googleapis.com/internal/client/"; + + final TableOperationLatency operationLatencyMetric; + final TableAttemptLatency attemptLatencyMetric; + final TableAttemptLatency2 attemptLatency2Metric; + final TableRetryCount retryCountMetric; + final TableFirstResponseLatency firstResponseLantencyMetric; + final TableServerLatency serverLatencyMetric; + final ClientChannelPoolOutstandingRpcs channelPoolOutstandingRpcsMetric; + final TableConnectivityErrorCount connectivityErrorCountMetric; + final ClientDpCompatGuage dpCompatGuageMetric; + final TableApplicationBlockingLatency applicationBlockingLatencyMetric; + final TableClientBlockingLatency clientBlockingLatencyMetric; + final ClientPerConnectionErrorCount perConnectionErrorCountMetric; + final TableRemainingDeadline remainingDeadlineMetric; + final ClientBatchWriteFlowControlFactor batchWriteFlowControlFactorMetric; + final ClientBatchWriteFlowControlTargetQps batchWriteFlowControlTargetQpsMetric; + + final PacemakerDelay pacemakerDelayMetric; + + private final Map> metrics = new HashMap<>(); + private final List grpcMetricNames = new ArrayList<>(); + + public MetricRegistry() { + operationLatencyMetric = register(new TableOperationLatency()); + attemptLatencyMetric = register(new TableAttemptLatency()); + attemptLatency2Metric = register(new TableAttemptLatency2()); + retryCountMetric = register(new TableRetryCount()); + firstResponseLantencyMetric = register(new TableFirstResponseLatency()); + serverLatencyMetric = register(new TableServerLatency()); + channelPoolOutstandingRpcsMetric = register(new ClientChannelPoolOutstandingRpcs()); + connectivityErrorCountMetric = register(new TableConnectivityErrorCount()); + applicationBlockingLatencyMetric = register(new TableApplicationBlockingLatency()); + clientBlockingLatencyMetric = register(new TableClientBlockingLatency()); + perConnectionErrorCountMetric = register(new ClientPerConnectionErrorCount()); + dpCompatGuageMetric = register(new ClientDpCompatGuage()); + remainingDeadlineMetric = register(new TableRemainingDeadline()); + batchWriteFlowControlFactorMetric = register(new ClientBatchWriteFlowControlFactor()); + batchWriteFlowControlTargetQpsMetric = register(new ClientBatchWriteFlowControlTargetQps()); + + pacemakerDelayMetric = register(new PacemakerDelay()); + + // From + // https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java#L138-L165 + registerGrpcMetric( + "grpc.client.attempt.duration", + ImmutableList.of("grpc.lb.locality", "grpc.status", "grpc.method", "grpc.target")); + registerGrpcMetric( + "grpc.lb.rls.default_target_picks", + ImmutableList.of( + "grpc.target", + "grpc.lb.rls.server_target", + "grpc.lb.rls.data_plane_target", + "grpc.lb.pick_result")); + registerGrpcMetric( + "grpc.lb.rls.target_picks", + ImmutableList.of( + "grpc.target", + "grpc.lb.rls.server_target", + "grpc.lb.rls.data_plane_target", + "grpc.lb.pick_result")); + registerGrpcMetric( + "grpc.lb.rls.failed_picks", ImmutableList.of("grpc.target", "grpc.lb.rls.server_target")); + + // From + // https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java#L67-L94 + // TODO: "grpc.xds_client.connected" + registerGrpcMetric( + "grpc.xds_client.server_failure", ImmutableList.of("grpc.target", "grpc.xds.server")); + // TODO: "grpc.xds_client.resource_updates_valid", + registerGrpcMetric( + "grpc.xds_client.resource_updates_invalid", + ImmutableList.of("grpc.target", "grpc.xds.server", "grpc.xds.resource_type")); + // TODO: "grpc.xds_client.resources" + + // From + // https://github.com/grpc/proposal/blob/86990145a7cef9e5473a132709b2556fec00c4c6/A94-subchannel-otel-metrics.md + registerGrpcMetric( + "grpc.subchannel.disconnections", + ImmutableList.of( + "grpc.target", "grpc.lb.backend_service", "grpc.lb.locality", "grpc.disconnect_error")); + + registerGrpcMetric( + "grpc.subchannel.connection_attempts_succeeded", + ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality")); + + registerGrpcMetric( + "grpc.subchannel.connection_attempts_failed", + ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality")); + + registerGrpcMetric( + "grpc.subchannel.open_connections", + ImmutableList.of( + "grpc.target", "grpc.security_level", "grpc.lb.backend_service", "grpc.lb.locality")); + } + + private void registerGrpcMetric(String name, List labels) { + grpcMetricNames.add(name); + register(new GrpcMetric(name, labels)); + } + + private > T register(T instrument) { + metrics.put(instrument.getName(), instrument); + return instrument; + } + + List getGrpcMetricNames() { + return ImmutableList.copyOf(grpcMetricNames); + } + + MetricWrapper getMetric(String name) { + return metrics.get(name); + } + + public RecorderRegistry newRecorderRegistry(MeterProvider meterProvider) { + return new RecorderRegistry(meterProvider.get(METER_NAME)); + } + + public class RecorderRegistry { + public final TableOperationLatency.Recorder operationLatency; + public final TableAttemptLatency.Recorder attemptLatency; + public final TableAttemptLatency2.Recorder attemptLatency2; + public final TableRetryCount.Recorder retryCount; + public final TableFirstResponseLatency.Recorder firstResponseLantency; + public final TableServerLatency.Recorder serverLatency; + public final ClientChannelPoolOutstandingRpcs.Recorder channelPoolOutstandingRpcs; + public final TableConnectivityErrorCount.Recorder connectivityErrorCount; + public final ClientDpCompatGuage.Recorder dpCompatGuage; + public final TableApplicationBlockingLatency.Recorder applicationBlockingLatency; + public final TableClientBlockingLatency.Recorder clientBlockingLatency; + public final ClientPerConnectionErrorCount.Recorder perConnectionErrorCount; + public final TableRemainingDeadline.Recorder remainingDeadline; + public final ClientBatchWriteFlowControlTargetQps.Recorder batchWriteFlowControlTargetQps; + public final ClientBatchWriteFlowControlFactor.Recorder batchWriteFlowControlFactor; + + 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"); } }