Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions google-cloud-bigtable/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@
<artifactId>proto-google-cloud-monitoring-v3</artifactId>
</dependency>

<!-- export custom metrics to cloud console -->
<dependency>
<groupId>com.google.cloud.opentelemetry</groupId>
<artifactId>exporter-metrics</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>com.google.api.grpc</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientSessionOpenLatency;
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientSessionUptime;
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientTransportLatency;
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.CustomAttemptLatency;
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;
Expand Down Expand Up @@ -87,6 +88,8 @@ public class MetricRegistry {

final ClientChannelPoolFallbackCount channelFallbackCountMetric;

final CustomAttemptLatency customAttemptLatencyMetric;

private final Map<String, MetricWrapper<?>> metrics = new HashMap<>();
private final List<String> grpcMetricNames = new ArrayList<>();

Expand Down Expand Up @@ -117,6 +120,8 @@ public MetricRegistry() {

channelFallbackCountMetric = register(new ClientChannelPoolFallbackCount());

customAttemptLatencyMetric = register(new CustomAttemptLatency());

// From
// https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java#L138-L165
registerGrpcMetric(
Expand Down Expand Up @@ -226,6 +231,8 @@ public class RecorderRegistry {

public final ClientChannelPoolFallbackCount.Recorder channelFallbackCount;

public final CustomAttemptLatency.Recorder customAttemptLatency;

private RecorderRegistry(Meter meter, boolean disableInternalMetrics) {
// Public metrics
operationLatency = operationLatencyMetric.newRecorder(meter);
Expand Down Expand Up @@ -260,6 +267,8 @@ private RecorderRegistry(Meter meter, boolean disableInternalMetrics) {
pacemakerDelay = pacemakerDelayMetric.newRecorder(meter);

channelFallbackCount = channelFallbackCountMetric.newRecorder(meter);

customAttemptLatency = customAttemptLatencyMetric.newRecorder(meter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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.exporter.BigtableCloudMonitoringExporter;
import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtableFilteringExporter;
import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtablePeriodicReader;
import com.google.cloud.bigtable.data.v2.internal.csm.opencensus.MetricsTracerFactory;
import com.google.cloud.bigtable.data.v2.internal.csm.opencensus.RpcMeasureConstants;
Expand All @@ -52,6 +53,8 @@
import com.google.cloud.bigtable.data.v2.internal.session.SessionPoolInfo;
import com.google.cloud.bigtable.data.v2.internal.session.VRpcDescriptor;
import com.google.cloud.bigtable.data.v2.stub.metrics.NoopMetricsProvider;
import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
import com.google.cloud.opentelemetry.metric.MetricConfiguration;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Suppliers;
Expand All @@ -68,17 +71,23 @@
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
import java.io.Closeable;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

public class MetricsImpl implements Metrics, Closeable {

public static final String CUSTOM_METRIC = "bigtable.internal.enable-custom-metric";

private final ApiTracerFactory userTracerFactory;
private final @Nullable OpenTelemetrySdk internalOtel;
private final @Nullable MetricRegistry.RecorderRegistry internalRecorder;
Expand Down Expand Up @@ -300,8 +309,30 @@ public static OpenTelemetrySdk createBuiltinOtel(
metricsEndpoint,
universeDomain);

meterProvider.registerMetricReader(new BigtablePeriodicReader(exporter, executor));

meterProvider.registerMetricReader(
new BigtablePeriodicReader(
new BigtableFilteringExporter(
exporter,
input -> input.getName().startsWith("bigtable.googleapis.com/internal/client")),
executor));

Optional<Boolean> enableCustomMetric =
Optional.ofNullable(System.getProperty(CUSTOM_METRIC)).map(Boolean::parseBoolean);
if (enableCustomMetric.isPresent() && enableCustomMetric.get()) {
// Monitored resource and project id are detected at export time
MetricConfiguration metricConfig =
MetricConfiguration.builder()
.setCredentials(credentials)
.setInstrumentationLibraryLabelsEnabled(false)
.build();
meterProvider.registerMetricReader(
PeriodicMetricReader.builder(
new BigtableFilteringExporter(
GoogleCloudMetricExporter.createWithConfiguration(metricConfig),
input -> input.getName().startsWith("bigtable.custom")))
.setInterval(Duration.ofMinutes(1))
.build());
}
return OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ public static String formatTransportType(@Nullable PeerInfo peerInfo) {
.orElse(TransportType.TRANSPORT_TYPE_UNKNOWN));
}

public static long formatAfeId(@Nullable PeerInfo peerInfo) {
return Optional.ofNullable(peerInfo).map(PeerInfo::getApplicationFrontendId).orElse(0L);
}

public static String transportTypeToString(TransportType transportType) {
String label = transportTypeToStringWithoutFallback(transportType);
if (label != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.exporter;

import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.metrics.InstrumentType;
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class BigtableFilteringExporter implements MetricExporter {

private MetricExporter delegate;
private Predicate<MetricData> filter;

public BigtableFilteringExporter(MetricExporter exporter, Predicate<MetricData> filter) {
this.delegate = exporter;
this.filter = filter;
}

@Override
public CompletableResultCode export(Collection<MetricData> metrics) {
List<MetricData> filtered = metrics.stream().filter(filter).collect(Collectors.toList());
return delegate.export(filtered);
}

@Override
public CompletableResultCode flush() {
return delegate.flush();
}

@Override
public CompletableResultCode shutdown() {
return delegate.shutdown();
}

public void prepareForShutdown() {
if (delegate instanceof BigtableCloudMonitoringExporter) {
((BigtableCloudMonitoringExporter) delegate).prepareForShutdown();
}
}

@Override
public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
return delegate.getAggregationTemporality(instrumentType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
*/
public class BigtablePeriodicReader implements MetricReader {
private final MetricReader delegate;
private final BigtableCloudMonitoringExporter exporter;
private final BigtableFilteringExporter exporter;

public BigtablePeriodicReader(
BigtableCloudMonitoringExporter exporter, ScheduledExecutorService executor) {
BigtableFilteringExporter exporter, ScheduledExecutorService executor) {
delegate = PeriodicMetricReader.builder(exporter).setExecutor(executor).build();
this.exporter = exporter;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.bigtable.data.v2.internal.csm.metrics;

import com.google.bigtable.v2.ClusterInformation;
import com.google.bigtable.v2.PeerInfo;
import com.google.cloud.bigtable.data.v2.internal.csm.MetricsImpl;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
import com.google.cloud.bigtable.data.v2.internal.csm.schema.CustomSchema;
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;
import java.util.Optional;
import javax.annotation.Nullable;

/**
* Custom attempt latencies with afe id metric label. This metric is high cardinality and is
* exported as a custom metric.
*/
public class CustomAttemptLatency extends MetricWrapper<CustomSchema> {
private static final String NAME = "bigtable.custom.attempt_latencies";

public CustomAttemptLatency() {
super(CustomSchema.INSTANCE, NAME);
}

public Recorder newRecorder(Meter meter) {
Optional<Boolean> enableCustomMetric =
Optional.ofNullable(System.getProperty(MetricsImpl.CUSTOM_METRIC))
.map(Boolean::parseBoolean);
if (enableCustomMetric.isPresent() && enableCustomMetric.get()) {
return new RecorderImpl(meter);
} else {
// Skip recording high cardinality metric when it's disabled
return new NoopRecorder();
}
}

public abstract static class Recorder {
public abstract void record(
ClientInfo clientInfo,
String tableId,
@Nullable PeerInfo peerInfo,
@Nullable ClusterInformation clusterInfo,
MethodInfo methodInfo,
Status.Code code,
Duration latency);
}

public static class RecorderImpl extends Recorder {
private final DoubleHistogram instrument;

private RecorderImpl(Meter meter) {
instrument =
meter
.histogramBuilder(NAME)
.setDescription("Client observed latency per RPC attempt.")
.setUnit(Constants.Units.MILLISECOND)
.setExplicitBucketBoundariesAdvice(
Constants.Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
.build();
}

public void record(
ClientInfo clientInfo,
String tableId,
@Nullable PeerInfo peerInfo,
@Nullable ClusterInformation clusterInfo,
MethodInfo methodInfo,
Status.Code code,
Duration latency) {

Attributes attributes =
Attributes.builder()
.put(
Constants.MetricLabels.BIGTABLE_PROJECT_ID_KEY,
clientInfo.getInstanceName().getProjectId())
.put(
Constants.MetricLabels.INSTANCE_ID_KEY,
clientInfo.getInstanceName().getInstanceId())
.put("table", tableId)
.put(Constants.MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
.put("cluster", Util.formatClusterIdMetricLabel(clusterInfo))
.put("zone", Util.formatZoneIdMetricLabel(clusterInfo))
.put(Constants.MetricLabels.STATUS_KEY, code.name())
.put(Constants.MetricLabels.METHOD_KEY, methodInfo.getName())
.put("afe_id", Util.formatAfeId(peerInfo))
.build();

instrument.record(toMillis(latency), attributes);
}
}

public static class NoopRecorder extends Recorder {
@Override
public void record(
ClientInfo clientInfo,
String tableId,
@Nullable PeerInfo peerInfo,
@Nullable ClusterInformation clusterInfo,
MethodInfo methodInfo,
Status.Code code,
Duration latency) {
// do nothing
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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.Attributes;

/** Placeholder schema for exporting custom metrics */
public class CustomSchema extends Schema {
private CustomSchema() {
super("custom_schema", ImmutableList.of());
}

public static final CustomSchema INSTANCE = new CustomSchema();

@Override
public ProjectName extractProjectName(Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo) {
return ProjectName.of(clientInfo.getInstanceName().getProjectId());
}
}
Loading
Loading