Skip to content

Commit ae6ad25

Browse files
feat: add metrics
stack-info: PR: #9691, branch: igorbernstein2/stack/4
1 parent d35ee71 commit ae6ad25

File tree

15 files changed

+1340
-13
lines changed

15 files changed

+1340
-13
lines changed

bigtable/bigtable-proxy/pom.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<!-- dep versions -->
2424
<libraries-bom.version>26.50.0</libraries-bom.version>
2525

26+
<otel.version>1.44.1</otel.version>
27+
<exporter-metrics.version>0.33.0</exporter-metrics.version>
2628
<slf4j.version>2.0.16</slf4j.version>
2729
<logback.version>1.5.12</logback.version>
2830
<auto-value.version>1.11.0</auto-value.version>
@@ -40,6 +42,20 @@
4042
<type>pom</type>
4143
<scope>import</scope>
4244
</dependency>
45+
<dependency>
46+
<groupId>io.opentelemetry</groupId>
47+
<artifactId>opentelemetry-bom</artifactId>
48+
<version>${otel.version}</version>
49+
<type>pom</type>
50+
<scope>import</scope>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-bom</artifactId>
55+
<version>5.14.2</version>
56+
<type>pom</type>
57+
<scope>import</scope>
58+
</dependency>
4359
</dependencies>
4460
</dependencyManagement>
4561

@@ -85,6 +101,23 @@
85101
<artifactId>proto-google-common-protos</artifactId>
86102
</dependency>
87103

104+
<!-- Metrics -->
105+
<dependency>
106+
<groupId>io.opentelemetry</groupId>
107+
<artifactId>opentelemetry-sdk</artifactId>
108+
<!-- version managed by opentelemetry-bom -->
109+
</dependency>
110+
<dependency>
111+
<groupId>io.opentelemetry</groupId>
112+
<artifactId>opentelemetry-sdk-metrics</artifactId>
113+
<!-- version managed by opentelemetry-bom -->
114+
</dependency>
115+
<dependency>
116+
<groupId>com.google.cloud.opentelemetry</groupId>
117+
<artifactId>exporter-metrics</artifactId>
118+
<version>${exporter-metrics.version}</version>
119+
</dependency>
120+
88121
<!-- Logging -->
89122
<dependency>
90123
<groupId>org.slf4j</groupId>
@@ -138,6 +171,12 @@
138171
<version>${truth.version}</version>
139172
<scope>test</scope>
140173
</dependency>
174+
<dependency>
175+
<groupId>org.mockito</groupId>
176+
<artifactId>mockito-core</artifactId>
177+
<!-- version managed by mockito-bom -->
178+
<scope>test</scope>
179+
</dependency>
141180
</dependencies>
142181

143182
<build>

bigtable/bigtable-proxy/src/main/java/com/google/cloud/bigtable/examples/proxy/commands/Serve.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import com.google.bigtable.v2.BigtableGrpc;
2323
import com.google.cloud.bigtable.examples.proxy.core.ProxyHandler;
2424
import com.google.cloud.bigtable.examples.proxy.core.Registry;
25+
import com.google.cloud.bigtable.examples.proxy.metrics.InstrumentedCallCredentials;
26+
import com.google.cloud.bigtable.examples.proxy.metrics.Metrics;
27+
import com.google.cloud.bigtable.examples.proxy.metrics.MetricsImpl;
2528
import com.google.common.collect.ImmutableMap;
2629
import com.google.longrunning.OperationsGrpc;
2730
import io.grpc.CallCredentials;
@@ -68,10 +71,17 @@ public class Serve implements Callable<Void> {
6871
showDefaultValue = Visibility.ALWAYS)
6972
Endpoint adminEndpoint = Endpoint.create("bigtableadmin.googleapis.com", 443);
7073

74+
@Option(
75+
names = "--metrics-project-id",
76+
required = true,
77+
description = "The project id where metrics should be exported")
78+
String metricsProjectId = null;
79+
7180
ManagedChannel adminChannel = null;
7281
ManagedChannel dataChannel = null;
7382
Credentials credentials = null;
7483
Server server;
84+
Metrics metrics;
7585

7686
@Override
7787
public Void call() throws Exception {
@@ -101,18 +111,23 @@ void start() throws IOException {
101111
if (credentials == null) {
102112
credentials = GoogleCredentials.getApplicationDefault();
103113
}
104-
CallCredentials callCredentials = MoreCallCredentials.from(credentials);
114+
CallCredentials callCredentials =
115+
new InstrumentedCallCredentials(MoreCallCredentials.from(credentials));
116+
117+
if (metrics == null) {
118+
metrics = new MetricsImpl(credentials, metricsProjectId);
119+
}
105120

106121
Map<String, ServerCallHandler<byte[], byte[]>> serviceMap =
107122
ImmutableMap.of(
108123
BigtableGrpc.SERVICE_NAME,
109-
new ProxyHandler<>(dataChannel, callCredentials),
124+
new ProxyHandler<>(metrics, dataChannel, callCredentials),
110125
BigtableInstanceAdminGrpc.SERVICE_NAME,
111-
new ProxyHandler<>(adminChannel, callCredentials),
126+
new ProxyHandler<>(metrics, adminChannel, callCredentials),
112127
BigtableTableAdminGrpc.SERVICE_NAME,
113-
new ProxyHandler<>(adminChannel, callCredentials),
128+
new ProxyHandler<>(metrics, adminChannel, callCredentials),
114129
OperationsGrpc.SERVICE_NAME,
115-
new ProxyHandler<>(adminChannel, callCredentials));
130+
new ProxyHandler<>(metrics, adminChannel, callCredentials));
116131

117132
server =
118133
NettyServerBuilder.forAddress(

bigtable/bigtable-proxy/src/main/java/com/google/cloud/bigtable/examples/proxy/core/CallProxy.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.google.cloud.bigtable.examples.proxy.core;
1717

18+
import com.google.cloud.bigtable.examples.proxy.metrics.Tracer;
1819
import io.grpc.ClientCall;
1920
import io.grpc.Metadata;
2021
import io.grpc.ServerCall;
@@ -23,15 +24,20 @@
2324

2425
/** A per gppc RPC proxy. */
2526
class CallProxy<ReqT, RespT> {
27+
28+
private final Tracer tracer;
2629
final RequestProxy serverCallListener;
2730
final ResponseProxy clientCallListener;
2831

2932
/**
33+
* @param tracer a lifecycle observer to publish metrics.
3034
* @param serverCall the incoming server call. This will be triggered a customer client.
3135
* @param clientCall the outgoing call to Bigtable service. This will be created by {@link
3236
* ProxyHandler}
3337
*/
34-
public CallProxy(ServerCall<ReqT, RespT> serverCall, ClientCall<ReqT, RespT> clientCall) {
38+
public CallProxy(
39+
Tracer tracer, ServerCall<ReqT, RespT> serverCall, ClientCall<ReqT, RespT> clientCall) {
40+
this.tracer = tracer;
3541
// Listen for incoming request messages and send them to the upstream ClientCall
3642
// The RequestProxy will respect back pressure from the ClientCall and only request a new
3743
// message from the incoming rpc when the upstream client call is ready,
@@ -129,6 +135,8 @@ public ResponseProxy(ServerCall<?, RespT> serverCall) {
129135

130136
@Override
131137
public void onClose(Status status, Metadata trailers) {
138+
tracer.onCallFinished(status);
139+
132140
serverCall.close(status, trailers);
133141
}
134142

bigtable/bigtable-proxy/src/main/java/com/google/cloud/bigtable/examples/proxy/core/ProxyHandler.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package com.google.cloud.bigtable.examples.proxy.core;
1717

18+
import com.google.cloud.bigtable.examples.proxy.metrics.CallLabels;
19+
import com.google.cloud.bigtable.examples.proxy.metrics.Metrics;
20+
import com.google.cloud.bigtable.examples.proxy.metrics.Tracer;
1821
import io.grpc.CallCredentials;
1922
import io.grpc.CallOptions;
2023
import io.grpc.Channel;
@@ -28,25 +31,32 @@ public final class ProxyHandler<ReqT, RespT> implements ServerCallHandler<ReqT,
2831
private final Metadata.Key<String> AUTHORIZATION_KEY =
2932
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
3033

34+
private final Metrics metrics;
3135
private final Channel channel;
3236
private final CallCredentials callCredentials;
3337

34-
public ProxyHandler(Channel channel, CallCredentials callCredentials) {
38+
public ProxyHandler(Metrics metrics, Channel channel, CallCredentials callCredentials) {
39+
this.metrics = metrics;
3540
this.channel = channel;
3641
this.callCredentials = callCredentials;
3742
}
3843

3944
@Override
4045
public ServerCall.Listener<ReqT> startCall(ServerCall<ReqT, RespT> serverCall, Metadata headers) {
41-
// Strip incoming credentials
42-
headers.removeAll(AUTHORIZATION_KEY);
46+
CallLabels callLabels = CallLabels.create(serverCall.getMethodDescriptor(), headers);
47+
Tracer tracer = new Tracer(metrics, callLabels);
48+
4349
// Inject proxy credentials
4450
CallOptions callOptions = CallOptions.DEFAULT.withCallCredentials(callCredentials);
51+
callOptions = tracer.injectIntoCallOptions(callOptions);
52+
53+
// Strip incoming credentials
54+
headers.removeAll(AUTHORIZATION_KEY);
4555

4656
ClientCall<ReqT, RespT> clientCall =
4757
channel.newCall(serverCall.getMethodDescriptor(), callOptions);
4858

49-
CallProxy<ReqT, RespT> proxy = new CallProxy<>(serverCall, clientCall);
59+
CallProxy<ReqT, RespT> proxy = new CallProxy<>(tracer, serverCall, clientCall);
5060
clientCall.start(proxy.clientCallListener, headers);
5161
serverCall.request(1);
5262
clientCall.request(1);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Copyright 2024 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.examples.proxy.metrics;
17+
18+
import com.google.auto.value.AutoValue;
19+
import io.grpc.Metadata;
20+
import io.grpc.Metadata.Key;
21+
import io.grpc.MethodDescriptor;
22+
import io.opentelemetry.api.common.Attributes;
23+
import java.net.URLDecoder;
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.Optional;
26+
27+
/**
28+
* A value class to encapsulate call identity.
29+
*
30+
* <p>This call extracts relevant information from request headers and makes it accessible to
31+
* metrics & the upstream client. The primary headers consulted are:
32+
*
33+
* <ul>
34+
* <li>{@code x-goog-request-params} - contains the resource and app profile id
35+
* <li>{@code x-goog-api-client} - contains the client info of the downstream client
36+
*/
37+
@AutoValue
38+
public abstract class CallLabels {
39+
private static final Key<String> REQUEST_PARAMS =
40+
Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER);
41+
private static final Key<String> API_CLIENT =
42+
Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER);
43+
44+
enum ResourceNameType {
45+
Parent("parent", 0),
46+
Name("name", 1),
47+
TableName("table_name", 2);
48+
49+
private final String name;
50+
private final int priority;
51+
52+
ResourceNameType(String name, int priority) {
53+
this.name = name;
54+
this.priority = priority;
55+
}
56+
}
57+
58+
@AutoValue
59+
abstract static class ResourceName {
60+
61+
abstract ResourceNameType getType();
62+
63+
abstract String getValue();
64+
65+
static ResourceName create(ResourceNameType type, String value) {
66+
return new AutoValue_CallLabels_ResourceName(type, value);
67+
}
68+
}
69+
70+
abstract Optional<String> getApiClient();
71+
72+
public abstract Optional<String> getResourceName();
73+
74+
public abstract Optional<String> getAppProfileId();
75+
76+
abstract String getMethodName();
77+
78+
public abstract Attributes getOtelAttributes();
79+
80+
public static CallLabels create(MethodDescriptor<?, ?> method, Metadata headers) {
81+
Optional<String> apiClient = Optional.ofNullable(headers.get(API_CLIENT));
82+
83+
String requestParams = Optional.ofNullable(headers.get(REQUEST_PARAMS)).orElse("");
84+
String[] encodedKvPairs = requestParams.split("&");
85+
Optional<String> resourceName = extractResourceName(encodedKvPairs).map(ResourceName::getValue);
86+
Optional<String> appProfile = extractAppProfileId(encodedKvPairs);
87+
88+
return create(method, apiClient, resourceName, appProfile);
89+
}
90+
91+
public static CallLabels create(
92+
MethodDescriptor<?, ?> method,
93+
Optional<String> apiClient,
94+
Optional<String> resourceName,
95+
Optional<String> appProfile) {
96+
Attributes otelAttrs =
97+
Attributes.builder()
98+
.put(MetricsImpl.API_CLIENT_KEY, apiClient.orElse("<missing>"))
99+
.put(MetricsImpl.RESOURCE_KEY, resourceName.orElse("<missing>"))
100+
.put(MetricsImpl.APP_PROFILE_KEY, appProfile.orElse("<missing>"))
101+
.put(MetricsImpl.METHOD_KEY, method.getFullMethodName())
102+
.build();
103+
return new AutoValue_CallLabels(
104+
apiClient, resourceName, appProfile, method.getFullMethodName(), otelAttrs);
105+
}
106+
107+
private static Optional<ResourceName> extractResourceName(String[] encodedKvPairs) {
108+
Optional<ResourceName> resourceName = Optional.empty();
109+
110+
for (String encodedKv : encodedKvPairs) {
111+
String[] split = encodedKv.split("=", 2);
112+
if (split.length != 2) {
113+
continue;
114+
}
115+
String encodedKey = split[0];
116+
String encodedValue = split[1];
117+
if (encodedKey.isEmpty() || encodedValue.isEmpty()) {
118+
continue;
119+
}
120+
121+
Optional<ResourceNameType> newType = findType(encodedKey);
122+
123+
if (newType.isEmpty()) {
124+
continue;
125+
}
126+
// Skip if we previously found a resource name and the new resource name type has a lower
127+
// priority
128+
if (resourceName.isPresent()
129+
&& newType.get().priority <= resourceName.get().getType().priority) {
130+
continue;
131+
}
132+
String decodedValue = percentDecode(encodedValue);
133+
134+
resourceName = Optional.of(ResourceName.create(newType.get(), decodedValue));
135+
}
136+
return resourceName;
137+
}
138+
139+
private static Optional<ResourceNameType> findType(String encodedKey) {
140+
String decodedKey = percentDecode(encodedKey);
141+
142+
for (ResourceNameType type : ResourceNameType.values()) {
143+
if (type.name.equals(decodedKey)) {
144+
return Optional.of(type);
145+
}
146+
}
147+
return Optional.empty();
148+
}
149+
150+
private static Optional<String> extractAppProfileId(String[] encodedKvPairs) {
151+
for (String encodedPair : encodedKvPairs) {
152+
if (!encodedPair.startsWith("app_profile_id=")) {
153+
continue;
154+
}
155+
String[] parts = encodedPair.split("=", 2);
156+
String encodedValue = parts.length > 1 ? parts[1] : "";
157+
return Optional.of(percentDecode(encodedValue));
158+
}
159+
return Optional.empty();
160+
}
161+
162+
private static String percentDecode(String s) {
163+
return URLDecoder.decode(s, StandardCharsets.UTF_8);
164+
}
165+
}

0 commit comments

Comments
 (0)