Skip to content

Commit 820acbe

Browse files
authored
feat: add custom attempt latency metric (#2882)
* feat: add custom attempt latency metric * license header Change-Id: Ia2df0e6bf15fe7bbd4327baa080c73009412924f * address comments Change-Id: I21f8d0f49749722d9b22b0db949da35df7672d22 * update Change-Id: I391f6118d55885de7f1a2a695f27aff7a02eb426 * small fix Change-Id: Ifdfc1f5845a8dd757dcaf0c36d03bd441882e1a4
1 parent 47a5504 commit 820acbe

File tree

9 files changed

+276
-11
lines changed

9 files changed

+276
-11
lines changed

google-cloud-bigtable/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,12 @@
253253
<artifactId>proto-google-cloud-monitoring-v3</artifactId>
254254
</dependency>
255255

256+
<!-- export custom metrics to cloud console -->
257+
<dependency>
258+
<groupId>com.google.cloud.opentelemetry</groupId>
259+
<artifactId>exporter-metrics</artifactId>
260+
</dependency>
261+
256262
<!-- Test dependencies -->
257263
<dependency>
258264
<groupId>com.google.api.grpc</groupId>

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricRegistry.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientSessionOpenLatency;
2828
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientSessionUptime;
2929
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientTransportLatency;
30+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.CustomAttemptLatency;
3031
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.GrpcMetric;
3132
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.MetricWrapper;
3233
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.PacemakerDelay;
@@ -87,6 +88,8 @@ public class MetricRegistry {
8788

8889
final ClientChannelPoolFallbackCount channelFallbackCountMetric;
8990

91+
final CustomAttemptLatency customAttemptLatencyMetric;
92+
9093
private final Map<String, MetricWrapper<?>> metrics = new HashMap<>();
9194
private final List<String> grpcMetricNames = new ArrayList<>();
9295

@@ -117,6 +120,8 @@ public MetricRegistry() {
117120

118121
channelFallbackCountMetric = register(new ClientChannelPoolFallbackCount());
119122

123+
customAttemptLatencyMetric = register(new CustomAttemptLatency());
124+
120125
// From
121126
// https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java#L138-L165
122127
registerGrpcMetric(
@@ -226,6 +231,8 @@ public class RecorderRegistry {
226231

227232
public final ClientChannelPoolFallbackCount.Recorder channelFallbackCount;
228233

234+
public final CustomAttemptLatency.Recorder customAttemptLatency;
235+
229236
private RecorderRegistry(Meter meter, boolean disableInternalMetrics) {
230237
// Public metrics
231238
operationLatency = operationLatencyMetric.newRecorder(meter);
@@ -260,6 +267,8 @@ private RecorderRegistry(Meter meter, boolean disableInternalMetrics) {
260267
pacemakerDelay = pacemakerDelayMetric.newRecorder(meter);
261268

262269
channelFallbackCount = channelFallbackCountMetric.newRecorder(meter);
270+
271+
customAttemptLatency = customAttemptLatencyMetric.newRecorder(meter);
263272
}
264273
}
265274
}

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/MetricsImpl.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
3131
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
3232
import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtableCloudMonitoringExporter;
33+
import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtableFilteringExporter;
3334
import com.google.cloud.bigtable.data.v2.internal.csm.exporter.BigtablePeriodicReader;
3435
import com.google.cloud.bigtable.data.v2.internal.csm.opencensus.MetricsTracerFactory;
3536
import com.google.cloud.bigtable.data.v2.internal.csm.opencensus.RpcMeasureConstants;
@@ -52,6 +53,8 @@
5253
import com.google.cloud.bigtable.data.v2.internal.session.SessionPoolInfo;
5354
import com.google.cloud.bigtable.data.v2.internal.session.VRpcDescriptor;
5455
import com.google.cloud.bigtable.data.v2.stub.metrics.NoopMetricsProvider;
56+
import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
57+
import com.google.cloud.opentelemetry.metric.MetricConfiguration;
5558
import com.google.common.base.Preconditions;
5659
import com.google.common.base.Splitter;
5760
import com.google.common.base.Suppliers;
@@ -68,17 +71,28 @@
6871
import io.opentelemetry.sdk.OpenTelemetrySdk;
6972
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
7073
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
74+
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
7175
import java.io.Closeable;
7276
import java.io.IOException;
77+
import java.time.Duration;
7378
import java.util.ArrayList;
7479
import java.util.List;
80+
import java.util.Optional;
7581
import java.util.concurrent.ScheduledExecutorService;
7682
import java.util.concurrent.ScheduledFuture;
7783
import java.util.concurrent.TimeUnit;
7884
import javax.annotation.Nullable;
7985
import javax.annotation.concurrent.GuardedBy;
8086

8187
public class MetricsImpl implements Metrics, Closeable {
88+
89+
public static final String CUSTOM_METRIC = "bigtable.internal.enable-custom-metric";
90+
91+
private static final boolean enableCustomMetric =
92+
Optional.ofNullable(System.getProperty(CUSTOM_METRIC))
93+
.map(Boolean::parseBoolean)
94+
.orElse(false);
95+
8296
private final ApiTracerFactory userTracerFactory;
8397
private final @Nullable OpenTelemetrySdk internalOtel;
8498
private final @Nullable MetricRegistry.RecorderRegistry internalRecorder;
@@ -196,7 +210,8 @@ public VRpcTracer newTableTracer(
196210
}
197211
ImmutableList.Builder<VRpcTracer> builder = ImmutableList.builder();
198212
builder.add(
199-
new VRpcTracerImpl(internalRecorder, poolInfo, descriptor.getMethodInfo(), deadline));
213+
new VRpcTracerImpl(
214+
internalRecorder, poolInfo, descriptor.getMethodInfo(), deadline, enableCustomMetric));
200215
if (userRecorder != null) {
201216
builder.add(new VRpcTracerImpl(userRecorder, poolInfo, descriptor.getMethodInfo(), deadline));
202217
}
@@ -300,8 +315,28 @@ public static OpenTelemetrySdk createBuiltinOtel(
300315
metricsEndpoint,
301316
universeDomain);
302317

303-
meterProvider.registerMetricReader(new BigtablePeriodicReader(exporter, executor));
304-
318+
meterProvider.registerMetricReader(
319+
new BigtablePeriodicReader(
320+
new BigtableFilteringExporter(
321+
exporter,
322+
input -> input.getName().startsWith("bigtable.googleapis.com/internal/client")),
323+
executor));
324+
325+
if (enableCustomMetric) {
326+
// Monitored resource and project id are detected at export time
327+
MetricConfiguration metricConfig =
328+
MetricConfiguration.builder()
329+
.setCredentials(credentials)
330+
.setInstrumentationLibraryLabelsEnabled(false)
331+
.build();
332+
meterProvider.registerMetricReader(
333+
PeriodicMetricReader.builder(
334+
new BigtableFilteringExporter(
335+
GoogleCloudMetricExporter.createWithConfiguration(metricConfig),
336+
input -> input.getName().startsWith("bigtable.custom")))
337+
.setInterval(Duration.ofMinutes(1))
338+
.build());
339+
}
305340
return OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
306341
}
307342

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/attributes/Util.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ public static String formatTransportType(@Nullable PeerInfo peerInfo) {
7979
.orElse(TransportType.TRANSPORT_TYPE_UNKNOWN));
8080
}
8181

82+
public static long formatAfeId(@Nullable PeerInfo peerInfo) {
83+
return Optional.ofNullable(peerInfo).map(PeerInfo::getApplicationFrontendId).orElse(0L);
84+
}
85+
8286
public static String transportTypeToString(TransportType transportType) {
8387
String label = transportTypeToStringWithoutFallback(transportType);
8488
if (label != null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.csm.exporter;
17+
18+
import io.opentelemetry.sdk.common.CompletableResultCode;
19+
import io.opentelemetry.sdk.metrics.InstrumentType;
20+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
21+
import io.opentelemetry.sdk.metrics.data.MetricData;
22+
import io.opentelemetry.sdk.metrics.export.MetricExporter;
23+
import java.util.Collection;
24+
import java.util.List;
25+
import java.util.function.Predicate;
26+
import java.util.stream.Collectors;
27+
28+
public class BigtableFilteringExporter implements MetricExporter {
29+
30+
private MetricExporter delegate;
31+
private Predicate<MetricData> filter;
32+
33+
public BigtableFilteringExporter(MetricExporter exporter, Predicate<MetricData> filter) {
34+
this.delegate = exporter;
35+
this.filter = filter;
36+
}
37+
38+
@Override
39+
public CompletableResultCode export(Collection<MetricData> metrics) {
40+
List<MetricData> filtered = metrics.stream().filter(filter).collect(Collectors.toList());
41+
return delegate.export(filtered);
42+
}
43+
44+
@Override
45+
public CompletableResultCode flush() {
46+
return delegate.flush();
47+
}
48+
49+
@Override
50+
public CompletableResultCode shutdown() {
51+
return delegate.shutdown();
52+
}
53+
54+
public void prepareForShutdown() {
55+
if (delegate instanceof BigtableCloudMonitoringExporter) {
56+
((BigtableCloudMonitoringExporter) delegate).prepareForShutdown();
57+
}
58+
}
59+
60+
@Override
61+
public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
62+
return delegate.getAggregationTemporality(instrumentType);
63+
}
64+
}

google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/csm/exporter/BigtablePeriodicReader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@
3434
*/
3535
public class BigtablePeriodicReader implements MetricReader {
3636
private final MetricReader delegate;
37-
private final BigtableCloudMonitoringExporter exporter;
37+
private final BigtableFilteringExporter exporter;
3838

3939
public BigtablePeriodicReader(
40-
BigtableCloudMonitoringExporter exporter, ScheduledExecutorService executor) {
40+
BigtableFilteringExporter exporter, ScheduledExecutorService executor) {
4141
delegate = PeriodicMetricReader.builder(exporter).setExecutor(executor).build();
4242
this.exporter = exporter;
4343
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.csm.metrics;
17+
18+
import com.google.bigtable.v2.ClusterInformation;
19+
import com.google.bigtable.v2.PeerInfo;
20+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
21+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.MethodInfo;
22+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.Util;
23+
import com.google.cloud.bigtable.data.v2.internal.csm.schema.CustomSchema;
24+
import io.grpc.Status;
25+
import io.opentelemetry.api.common.Attributes;
26+
import io.opentelemetry.api.metrics.DoubleHistogram;
27+
import io.opentelemetry.api.metrics.Meter;
28+
import java.time.Duration;
29+
import javax.annotation.Nullable;
30+
31+
/**
32+
* Custom attempt latencies with afe id metric label. This metric is high cardinality and is
33+
* exported as a custom metric.
34+
*/
35+
public class CustomAttemptLatency extends MetricWrapper<CustomSchema> {
36+
private static final String NAME = "bigtable.custom.attempt_latencies";
37+
38+
public CustomAttemptLatency() {
39+
super(CustomSchema.INSTANCE, NAME);
40+
}
41+
42+
public Recorder newRecorder(Meter meter) {
43+
return new Recorder(meter);
44+
}
45+
46+
public static class Recorder {
47+
private final DoubleHistogram instrument;
48+
49+
private Recorder(Meter meter) {
50+
instrument =
51+
meter
52+
.histogramBuilder(NAME)
53+
.setDescription("Client observed latency per RPC attempt.")
54+
.setUnit(Constants.Units.MILLISECOND)
55+
.setExplicitBucketBoundariesAdvice(
56+
Constants.Buckets.AGGREGATION_WITH_MILLIS_HISTOGRAM)
57+
.build();
58+
}
59+
60+
public void record(
61+
ClientInfo clientInfo,
62+
String tableId,
63+
@Nullable PeerInfo peerInfo,
64+
@Nullable ClusterInformation clusterInfo,
65+
MethodInfo methodInfo,
66+
Status.Code code,
67+
Duration latency) {
68+
69+
Attributes attributes =
70+
Attributes.builder()
71+
.put(
72+
Constants.MetricLabels.BIGTABLE_PROJECT_ID_KEY,
73+
clientInfo.getInstanceName().getProjectId())
74+
.put(
75+
Constants.MetricLabels.INSTANCE_ID_KEY,
76+
clientInfo.getInstanceName().getInstanceId())
77+
.put("table", tableId)
78+
.put(Constants.MetricLabels.APP_PROFILE_KEY, clientInfo.getAppProfileId())
79+
.put("cluster", Util.formatClusterIdMetricLabel(clusterInfo))
80+
.put("zone", Util.formatZoneIdMetricLabel(clusterInfo))
81+
.put(Constants.MetricLabels.STATUS_KEY, code.name())
82+
.put(Constants.MetricLabels.METHOD_KEY, methodInfo.getName())
83+
.put("afe_id", Util.formatAfeId(peerInfo))
84+
.build();
85+
86+
instrument.record(toMillis(latency), attributes);
87+
}
88+
}
89+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.data.v2.internal.csm.schema;
17+
18+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.EnvInfo;
20+
import com.google.common.collect.ImmutableList;
21+
import com.google.monitoring.v3.ProjectName;
22+
import io.opentelemetry.api.common.Attributes;
23+
24+
/** Placeholder schema for exporting custom metrics */
25+
public class CustomSchema extends Schema {
26+
private CustomSchema() {
27+
super("custom_schema", ImmutableList.of());
28+
}
29+
30+
public static final CustomSchema INSTANCE = new CustomSchema();
31+
32+
@Override
33+
public ProjectName extractProjectName(Attributes attrs, EnvInfo envInfo, ClientInfo clientInfo) {
34+
return ProjectName.of(clientInfo.getInstanceName().getProjectId());
35+
}
36+
}

0 commit comments

Comments
 (0)