Skip to content

Commit 1080244

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

File tree

14 files changed

+1366
-13
lines changed

14 files changed

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

0 commit comments

Comments
 (0)