Skip to content

Commit 0a83000

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

File tree

14 files changed

+1325
-13
lines changed

14 files changed

+1325
-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

@@ -93,6 +109,23 @@
93109
<artifactId>proto-google-common-protos</artifactId>
94110
</dependency>
95111

112+
<!-- Metrics -->
113+
<dependency>
114+
<groupId>io.opentelemetry</groupId>
115+
<artifactId>opentelemetry-sdk</artifactId>
116+
<!-- version managed by opentelemetry-bom -->
117+
</dependency>
118+
<dependency>
119+
<groupId>io.opentelemetry</groupId>
120+
<artifactId>opentelemetry-sdk-metrics</artifactId>
121+
<!-- version managed by opentelemetry-bom -->
122+
</dependency>
123+
<dependency>
124+
<groupId>com.google.cloud.opentelemetry</groupId>
125+
<artifactId>exporter-metrics</artifactId>
126+
<version>${exporter-metrics.version}</version>
127+
</dependency>
128+
96129
<!-- Logging -->
97130
<dependency>
98131
<groupId>org.slf4j</groupId>
@@ -146,6 +179,12 @@
146179
<version>${truth.version}</version>
147180
<scope>test</scope>
148181
</dependency>
182+
<dependency>
183+
<groupId>org.mockito</groupId>
184+
<artifactId>mockito-core</artifactId>
185+
<!-- version managed by mockito-bom -->
186+
<scope>test</scope>
187+
</dependency>
149188
</dependencies>
150189

151190
<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
@@ -23,6 +23,9 @@
2323
import com.google.bigtable.v2.BigtableGrpc;
2424
import com.google.cloud.bigtable.examples.proxy.core.ProxyHandler;
2525
import com.google.cloud.bigtable.examples.proxy.core.Registry;
26+
import com.google.cloud.bigtable.examples.proxy.metrics.InstrumentedCallCredentials;
27+
import com.google.cloud.bigtable.examples.proxy.metrics.Metrics;
28+
import com.google.cloud.bigtable.examples.proxy.metrics.MetricsImpl;
2629
import com.google.common.collect.ImmutableMap;
2730
import com.google.longrunning.OperationsGrpc;
2831
import io.grpc.CallCredentials;
@@ -69,10 +72,17 @@ public class Serve implements Callable<Void> {
6972
showDefaultValue = Visibility.ALWAYS)
7073
Endpoint adminEndpoint = Endpoint.create("bigtableadmin.googleapis.com", 443);
7174

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

7787
@Override
7888
public Void call() throws Exception {
@@ -103,18 +113,23 @@ void start() throws IOException {
103113
if (credentials == null) {
104114
credentials = GoogleCredentials.getApplicationDefault();
105115
}
106-
CallCredentials callCredentials = MoreCallCredentials.from(credentials);
116+
CallCredentials callCredentials =
117+
new InstrumentedCallCredentials(MoreCallCredentials.from(credentials));
118+
119+
if (metrics == null) {
120+
metrics = new MetricsImpl(credentials, metricsProjectId);
121+
}
107122

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

119134
server =
120135
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
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.bigtable.examples.proxy.core;
1818

19+
import com.google.cloud.bigtable.examples.proxy.metrics.Tracer;
1920
import io.grpc.ClientCall;
2021
import io.grpc.Metadata;
2122
import io.grpc.ServerCall;
@@ -24,15 +25,20 @@
2425

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

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

132138
@Override
133139
public void onClose(Status status, Metadata trailers) {
140+
tracer.onCallFinished(status);
141+
134142
serverCall.close(status, trailers);
135143
}
136144

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
@@ -16,6 +16,9 @@
1616

1717
package com.google.cloud.bigtable.examples.proxy.core;
1818

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

35+
private final Metrics metrics;
3236
private final Channel channel;
3337
private final CallCredentials callCredentials;
3438

35-
public ProxyHandler(Channel channel, CallCredentials callCredentials) {
39+
public ProxyHandler(Metrics metrics, Channel channel, CallCredentials callCredentials) {
40+
this.metrics = metrics;
3641
this.channel = channel;
3742
this.callCredentials = callCredentials;
3843
}
3944

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

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

50-
CallProxy<ReqT, RespT> proxy = new CallProxy<>(serverCall, clientCall);
60+
CallProxy<ReqT, RespT> proxy = new CallProxy<>(tracer, serverCall, clientCall);
5161
clientCall.start(proxy.clientCallListener, headers);
5262
serverCall.request(1);
5363
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)