Skip to content
This repository was archived by the owner on May 8, 2026. It is now read-only.

Commit 751f614

Browse files
chore: define strongly typed metric wrappers
Change-Id: Ia788364f22d718850f2909826e4a356442c9a009
1 parent d9b9fda commit 751f614

33 files changed

Lines changed: 2716 additions & 0 deletions

google-cloud-bigtable/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,26 @@
338338
<artifactId>junit</artifactId>
339339
<scope>test</scope>
340340
</dependency>
341+
<dependency>
342+
<groupId>org.junit.jupiter</groupId>
343+
<artifactId>junit-jupiter-api</artifactId>
344+
<scope>test</scope>
345+
</dependency>
346+
<dependency>
347+
<groupId>org.junit.jupiter</groupId>
348+
<artifactId>junit-jupiter</artifactId>
349+
<scope>test</scope>
350+
</dependency>
341351
<dependency>
342352
<groupId>org.mockito</groupId>
343353
<artifactId>mockito-core</artifactId>
344354
<scope>test</scope>
345355
</dependency>
356+
<dependency>
357+
<groupId>org.mockito</groupId>
358+
<artifactId>mockito-junit-jupiter</artifactId>
359+
<scope>test</scope>
360+
</dependency>
346361
<dependency>
347362
<groupId>com.google.guava</groupId>
348363
<artifactId>guava-testlib</artifactId>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2025 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+
17+
package com.google.cloud.bigtable.data.v2.internal.csm;
18+
19+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlFactor;
20+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientBatchWriteFlowControlTargetQps;
21+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.ClientPerConnectionErrorCount;
22+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.GrpcMetric;
23+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.MetricWrapper;
24+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.PacemakerDelay;
25+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableApplicationBlockingLatency;
26+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency;
27+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableAttemptLatency2;
28+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableClientBlockingLatency;
29+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableConnectivityErrorCount;
30+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableFirstResponseLatency;
31+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableOperationLatency;
32+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRemainingDeadline;
33+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableRetryCount;
34+
import com.google.cloud.bigtable.data.v2.internal.csm.metrics.TableServerLatency;
35+
import com.google.common.collect.ImmutableList;
36+
import io.opentelemetry.api.metrics.Meter;
37+
import io.opentelemetry.api.metrics.MeterProvider;
38+
import java.util.ArrayList;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
43+
/**
44+
* Repository for all client metrics. This class has 2 audiences:
45+
*
46+
* <ul>
47+
* <li>VRpcTracer, which reference each metric directly
48+
* <li>Exporter, which will look up each metric by name and use the {@link MetricWrapper}
49+
* interface to augment the {@code MonitoredResource} and {@code Metric Labels}
50+
* </ul>
51+
*/
52+
public class MetricRegistry {
53+
static final String METER_NAME = "bigtable.googleapis.com/internal/client/";
54+
55+
final TableOperationLatency operationLatencyMetric;
56+
final TableAttemptLatency attemptLatencyMetric;
57+
final TableAttemptLatency2 attemptLatency2Metric;
58+
final TableRetryCount retryCountMetric;
59+
final TableFirstResponseLatency firstResponseLantencyMetric;
60+
final TableServerLatency serverLatencyMetric;
61+
final TableConnectivityErrorCount connectivityErrorCountMetric;
62+
final TableApplicationBlockingLatency applicationBlockingLatencyMetric;
63+
final TableClientBlockingLatency clientBlockingLatencyMetric;
64+
final ClientPerConnectionErrorCount perConnectionErrorCountMetric;
65+
final TableRemainingDeadline remainingDeadlineMetric;
66+
final ClientBatchWriteFlowControlFactor batchWriteFlowControlFactorMetric;
67+
final ClientBatchWriteFlowControlTargetQps batchWriteFlowControlTargetQpsMetric;
68+
69+
final PacemakerDelay pacemakerDelayMetric;
70+
71+
private final Map<String, MetricWrapper<?>> metrics = new HashMap<>();
72+
private final List<String> grpcMetricNames = new ArrayList<>();
73+
74+
public MetricRegistry() {
75+
operationLatencyMetric = register(new TableOperationLatency());
76+
attemptLatencyMetric = register(new TableAttemptLatency());
77+
attemptLatency2Metric = register(new TableAttemptLatency2());
78+
retryCountMetric = register(new TableRetryCount());
79+
firstResponseLantencyMetric = register(new TableFirstResponseLatency());
80+
serverLatencyMetric = register(new TableServerLatency());
81+
connectivityErrorCountMetric = register(new TableConnectivityErrorCount());
82+
applicationBlockingLatencyMetric = register(new TableApplicationBlockingLatency());
83+
clientBlockingLatencyMetric = register(new TableClientBlockingLatency());
84+
perConnectionErrorCountMetric = register(new ClientPerConnectionErrorCount());
85+
remainingDeadlineMetric = register(new TableRemainingDeadline());
86+
batchWriteFlowControlFactorMetric = register(new ClientBatchWriteFlowControlFactor());
87+
batchWriteFlowControlTargetQpsMetric = register(new ClientBatchWriteFlowControlTargetQps());
88+
89+
pacemakerDelayMetric = register(new PacemakerDelay());
90+
91+
// From
92+
// https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/rls/src/main/java/io/grpc/rls/CachingRlsLbClient.java#L138-L165
93+
registerGrpcMetric(
94+
"grpc.client.attempt.duration",
95+
ImmutableList.of("grpc.lb.locality", "grpc.status", "grpc.method", "grpc.target"));
96+
registerGrpcMetric(
97+
"grpc.lb.rls.default_target_picks",
98+
ImmutableList.of(
99+
"grpc.target",
100+
"grpc.lb.rls.server_target",
101+
"grpc.lb.rls.data_plane_target",
102+
"grpc.lb.pick_result"));
103+
registerGrpcMetric(
104+
"grpc.lb.rls.target_picks",
105+
ImmutableList.of(
106+
"grpc.target",
107+
"grpc.lb.rls.server_target",
108+
"grpc.lb.rls.data_plane_target",
109+
"grpc.lb.pick_result"));
110+
registerGrpcMetric(
111+
"grpc.lb.rls.failed_picks", ImmutableList.of("grpc.target", "grpc.lb.rls.server_target"));
112+
113+
// From
114+
// https://github.com/grpc/grpc-java/blob/31fdb6c2268b4b1c8ba6c995ee46c58e84a831aa/xds/src/main/java/io/grpc/xds/XdsClientMetricReporterImpl.java#L67-L94
115+
// TODO: "grpc.xds_client.connected"
116+
registerGrpcMetric(
117+
"grpc.xds_client.server_failure", ImmutableList.of("grpc.target", "grpc.xds.server"));
118+
// TODO: "grpc.xds_client.resource_updates_valid",
119+
registerGrpcMetric(
120+
"grpc.xds_client.resource_updates_invalid",
121+
ImmutableList.of("grpc.target", "grpc.xds.server", "grpc.xds.resource_type"));
122+
// TODO: "grpc.xds_client.resources"
123+
124+
// From
125+
// https://github.com/grpc/proposal/blob/86990145a7cef9e5473a132709b2556fec00c4c6/A94-subchannel-otel-metrics.md
126+
registerGrpcMetric(
127+
"grpc.subchannel.disconnections",
128+
ImmutableList.of(
129+
"grpc.target", "grpc.lb.backend_service", "grpc.lb.locality", "grpc.disconnect_error"));
130+
131+
registerGrpcMetric(
132+
"grpc.subchannel.connection_attempts_succeeded",
133+
ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality"));
134+
135+
registerGrpcMetric(
136+
"grpc.subchannel.connection_attempts_failed",
137+
ImmutableList.of("grpc.target", "grpc.lb.backend_service", "grpc.lb.locality"));
138+
139+
registerGrpcMetric(
140+
"grpc.subchannel.open_connections",
141+
ImmutableList.of(
142+
"grpc.target", "grpc.security_level", "grpc.lb.backend_service", "grpc.lb.locality"));
143+
}
144+
145+
private void registerGrpcMetric(String name, List<String> labels) {
146+
grpcMetricNames.add(name);
147+
register(new GrpcMetric(name, labels));
148+
}
149+
150+
private <T extends MetricWrapper<?>> T register(T instrument) {
151+
metrics.put(instrument.getName(), instrument);
152+
return instrument;
153+
}
154+
155+
List<String> getGrpcMetricNames() {
156+
return ImmutableList.copyOf(grpcMetricNames);
157+
}
158+
159+
MetricWrapper<?> getMetric(String name) {
160+
return metrics.get(name);
161+
}
162+
163+
public RecorderRegistry newRecorderRegistry(MeterProvider meterProvider) {
164+
return new RecorderRegistry(meterProvider.get(METER_NAME));
165+
}
166+
167+
public class RecorderRegistry {
168+
public final TableOperationLatency.Recorder operationLatency;
169+
public final TableAttemptLatency.Recorder attemptLatency;
170+
public final TableAttemptLatency2.Recorder attemptLatency2;
171+
public final TableRetryCount.Recorder retryCount;
172+
public final TableFirstResponseLatency.Recorder firstResponseLantency;
173+
public final TableServerLatency.Recorder serverLatency;
174+
public final TableConnectivityErrorCount.Recorder connectivityErrorCount;
175+
public final TableApplicationBlockingLatency.Recorder applicationBlockingLatency;
176+
public final TableClientBlockingLatency.Recorder clientBlockingLatency;
177+
public final ClientPerConnectionErrorCount.Recorder perConnectionErrorCount;
178+
public final TableRemainingDeadline.Recorder remainingDeadline;
179+
public final ClientBatchWriteFlowControlTargetQps.Recorder batchWriteFlowControlTargetQps;
180+
public final ClientBatchWriteFlowControlFactor.Recorder batchWriteFlowControlFactor;
181+
182+
final PacemakerDelay.Recorder pacemakerDelay;
183+
184+
private RecorderRegistry(Meter meter) {
185+
operationLatency = operationLatencyMetric.newRecorder(meter);
186+
attemptLatency = attemptLatencyMetric.newRecorder(meter);
187+
attemptLatency2 = attemptLatency2Metric.newRecorder(meter);
188+
retryCount = retryCountMetric.newRecorder(meter);
189+
firstResponseLantency = firstResponseLantencyMetric.newRecorder(meter);
190+
serverLatency = serverLatencyMetric.newRecorder(meter);
191+
connectivityErrorCount = connectivityErrorCountMetric.newRecorder(meter);
192+
applicationBlockingLatency = applicationBlockingLatencyMetric.newRecorder(meter);
193+
clientBlockingLatency = clientBlockingLatencyMetric.newRecorder(meter);
194+
perConnectionErrorCount = perConnectionErrorCountMetric.newRecorder(meter);
195+
remainingDeadline = remainingDeadlineMetric.newRecorder(meter);
196+
batchWriteFlowControlTargetQps = batchWriteFlowControlTargetQpsMetric.newRecorder(meter);
197+
batchWriteFlowControlFactor = batchWriteFlowControlFactorMetric.newRecorder(meter);
198+
199+
pacemakerDelay = pacemakerDelayMetric.newRecorder(meter);
200+
}
201+
}
202+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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;
17+
18+
import com.google.cloud.bigtable.data.v2.internal.csm.MetricRegistry.RecorderRegistry;
19+
import com.google.cloud.bigtable.data.v2.internal.csm.attributes.ClientInfo;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
class Pacemaker implements Runnable {
24+
25+
static final Duration PACEMAKER_INTERVAL = Duration.ofMillis(100);
26+
27+
private final RecorderRegistry registry;
28+
private final ClientInfo clientInfo;
29+
private final String executorName;
30+
31+
private Instant prev;
32+
33+
Pacemaker(RecorderRegistry registry, ClientInfo clientInfo, String name) {
34+
this.prev = Instant.now();
35+
this.registry = registry;
36+
this.clientInfo = clientInfo;
37+
this.executorName = name;
38+
}
39+
40+
@Override
41+
public void run() {
42+
Instant current = Instant.now();
43+
Duration delta = Duration.between(prev, current).minus(PACEMAKER_INTERVAL);
44+
prev = current;
45+
registry.pacemakerDelay.record(
46+
clientInfo, executorName, delta.isNegative() ? Duration.ZERO : delta);
47+
}
48+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2025 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+
17+
package com.google.cloud.bigtable.data.v2.internal.csm.attributes;
18+
19+
import com.google.auto.value.AutoValue;
20+
import com.google.bigtable.v2.InstanceName;
21+
import com.google.cloud.bigtable.Version;
22+
import java.lang.management.ManagementFactory;
23+
import java.net.InetAddress;
24+
import java.net.UnknownHostException;
25+
import java.util.UUID;
26+
import java.util.concurrent.atomic.AtomicLong;
27+
import java.util.logging.Level;
28+
import java.util.logging.Logger;
29+
30+
/**
31+
* A value class to capture parameters that the client was instantiated with. These parameters will
32+
* be used by the Exporter to derive MonitoredResource for GrpcMetrics.
33+
*/
34+
@AutoValue
35+
public abstract class ClientInfo {
36+
private static final Logger logger = Logger.getLogger(ClientInfo.class.getName());
37+
38+
private static final AtomicLong uidSuffix = new AtomicLong(0);
39+
40+
/** The name and version of the client. */
41+
public abstract String getClientName();
42+
43+
/** A unique identifier to disambiguate TimeSeries from multiple processes on the same VM. */
44+
public abstract String getUid();
45+
46+
public abstract InstanceName getInstanceName();
47+
48+
public abstract String getAppProfileId();
49+
50+
public abstract Builder toBuilder();
51+
52+
public static Builder builder() {
53+
return new AutoValue_ClientInfo.Builder()
54+
.setClientName("java-bigtable/" + Version.VERSION)
55+
.setUid(computeUid() + "-" + uidSuffix.getAndIncrement());
56+
}
57+
58+
@AutoValue.Builder
59+
public abstract static class Builder {
60+
protected abstract Builder setClientName(String name);
61+
62+
protected abstract Builder setUid(String uid);
63+
64+
public abstract Builder setInstanceName(InstanceName name);
65+
66+
public abstract Builder setAppProfileId(String appProfileId);
67+
68+
public abstract ClientInfo build();
69+
}
70+
71+
private static String computeUid() {
72+
final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
73+
// If jvm doesn't have the expected format, fallback to the local hostname
74+
if (jvmName.indexOf('@') < 1) {
75+
String hostname = "localhost";
76+
try {
77+
hostname = InetAddress.getLocalHost().getHostName();
78+
} catch (UnknownHostException e) {
79+
logger.log(Level.INFO, "Unable to get the hostname.", e);
80+
}
81+
// Generate a random number and use the same format "random_number@hostname".
82+
return "java-" + UUID.randomUUID() + "@" + hostname;
83+
}
84+
return "java-" + UUID.randomUUID() + jvmName;
85+
}
86+
}

0 commit comments

Comments
 (0)