Skip to content

Commit 3afb9e4

Browse files
authored
feat: Adds structure for gRPC client metrics instrumentation (#1021)
* added a new grpc client metric `grpc.client.attempt.started`
1 parent 4087bbd commit 3afb9e4

File tree

5 files changed

+268
-0
lines changed

5 files changed

+268
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring Authors
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 net.devh.boot.grpc.client.metrics;
18+
19+
import io.micrometer.core.instrument.Counter;
20+
import io.micrometer.core.instrument.MeterRegistry;
21+
22+
/*
23+
* The instruments used to record metrics on client.
24+
*/
25+
public final class MetricsClientInstruments {
26+
27+
private MetricsClientInstruments() {}
28+
29+
/*
30+
* This is a client side metric defined in gRFC <a
31+
* href="https://github.com/grpc/proposal/blob/master/A66-otel-stats.md">A66</a>. Please note that this is the name
32+
* used for instrumentation and can be changed by exporters in an unpredictable manner depending on the destination.
33+
*/
34+
private static final String CLIENT_ATTEMPT_STARTED = "grpc.client.attempt.started";
35+
36+
static MetricsMeters newClientMetricsMeters(MeterRegistry registry) {
37+
MetricsMeters.Builder builder = MetricsMeters.newBuilder();
38+
39+
builder.setAttemptCounter(Counter.builder(CLIENT_ATTEMPT_STARTED)
40+
.description(
41+
"The total number of RPC attempts started from the client side, including "
42+
+ "those that have not completed.")
43+
.baseUnit("attempt")
44+
.withRegistry(registry));
45+
return builder.build();
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring Authors
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 net.devh.boot.grpc.client.metrics;
18+
19+
import io.grpc.CallOptions;
20+
import io.grpc.Channel;
21+
import io.grpc.ClientCall;
22+
import io.grpc.ClientInterceptor;
23+
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
24+
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
25+
import io.grpc.Metadata;
26+
import io.grpc.MethodDescriptor;
27+
import io.grpc.Status;
28+
import io.micrometer.core.instrument.MeterRegistry;
29+
30+
/**
31+
* A gRPC client interceptor that collects gRPC metrics.
32+
*
33+
* <b>Note:</b> This class uses experimental grpc-java-API features.
34+
*/
35+
public class MetricsClientInterceptor implements ClientInterceptor {
36+
37+
private final MetricsMeters metricsMeters;
38+
39+
/**
40+
* Creates a new gRPC client interceptor that collects metrics into the given
41+
* {@link io.micrometer.core.instrument.MeterRegistry}.
42+
*
43+
* @param registry The MeterRegistry to use.
44+
*/
45+
public MetricsClientInterceptor(MeterRegistry registry) {
46+
this.metricsMeters = MetricsClientInstruments.newClientMetricsMeters(registry);
47+
}
48+
49+
@Override
50+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
51+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
52+
53+
/*
54+
* This is a per call ClientStreamTracer.Factory which creates a new stream tracer for each attempt under the
55+
* same call. Each call needs a dedicated factory as they share the same method descriptor.
56+
*/
57+
final MetricsClientStreamTracers.CallAttemptsTracerFactory tracerFactory =
58+
new MetricsClientStreamTracers.CallAttemptsTracerFactory(method.getFullMethodName(),
59+
metricsMeters);
60+
61+
ClientCall<ReqT, RespT> call =
62+
next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory));
63+
64+
// TODO(dnvindhya): Collect the actual response/error in the SimpleForwardingClientCall
65+
return new SimpleForwardingClientCall<ReqT, RespT>(call) {
66+
@Override
67+
public void start(Listener<RespT> responseListener, Metadata headers) {
68+
delegate().start(
69+
new SimpleForwardingClientCallListener<RespT>(responseListener) {
70+
@Override
71+
public void onClose(Status status, Metadata trailers) {
72+
super.onClose(status, trailers);
73+
}
74+
},
75+
headers);
76+
}
77+
};
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring Authors
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 net.devh.boot.grpc.client.metrics;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import io.grpc.ClientStreamTracer;
22+
import io.grpc.ClientStreamTracer.StreamInfo;
23+
import io.grpc.Metadata;
24+
import io.micrometer.core.instrument.Tags;
25+
26+
/**
27+
* Provides factories for {@link io.grpc.StreamTracer} that records metrics.
28+
*
29+
* <p>
30+
* On the client-side, a factory is created for each call, and the factory creates a stream tracer for each attempt.
31+
*
32+
* <b>Note:</b> This class uses experimental grpc-java-API features.
33+
*/
34+
public final class MetricsClientStreamTracers {
35+
36+
private MetricsClientStreamTracers() {}
37+
38+
private static final class ClientTracer extends ClientStreamTracer {
39+
private final CallAttemptsTracerFactory attemptsState;
40+
private final StreamInfo info;
41+
private final String fullMethodName;
42+
43+
ClientTracer(CallAttemptsTracerFactory attemptsState, StreamInfo info, String fullMethodName) {
44+
this.attemptsState = attemptsState;
45+
this.info = info;
46+
this.fullMethodName = fullMethodName;
47+
}
48+
49+
}
50+
51+
static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
52+
private final String fullMethodName;
53+
private final MetricsMeters metricsMeters;
54+
private boolean attemptRecorded;
55+
56+
CallAttemptsTracerFactory(String fullMethodName, MetricsMeters metricsMeters) {
57+
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
58+
this.metricsMeters = checkNotNull(metricsMeters, "metricsMeters");
59+
60+
// Record here in case newClientStreamTracer() would never be called.
61+
this.metricsMeters.getAttemptCounter()
62+
.withTags(Tags.of("grpc.method", fullMethodName))
63+
.increment();
64+
this.attemptRecorded = true;
65+
}
66+
67+
@Override
68+
public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata metadata) {
69+
if (!this.attemptRecorded) {
70+
this.metricsMeters.getAttemptCounter()
71+
.withTags((Tags.of("grpc.method", fullMethodName)))
72+
.increment();
73+
} else {
74+
this.attemptRecorded = false;
75+
}
76+
return new ClientTracer(this, info, fullMethodName);
77+
}
78+
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2016-2023 The gRPC-Spring Authors
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 net.devh.boot.grpc.client.metrics;
18+
19+
import io.micrometer.core.instrument.Counter;
20+
import io.micrometer.core.instrument.Meter.MeterProvider;
21+
22+
/*
23+
* Collection of metrics meters.
24+
*/
25+
public class MetricsMeters {
26+
27+
private MeterProvider<Counter> attemptCounter;
28+
29+
private MetricsMeters(Builder builder) {
30+
this.attemptCounter = builder.attemptCounter;
31+
}
32+
33+
public MeterProvider<Counter> getAttemptCounter() {
34+
return this.attemptCounter;
35+
}
36+
37+
public static Builder newBuilder() {
38+
return new Builder();
39+
}
40+
41+
static class Builder {
42+
43+
private MeterProvider<Counter> attemptCounter;
44+
45+
private Builder() {}
46+
47+
public Builder setAttemptCounter(MeterProvider<Counter> counter) {
48+
this.attemptCounter = counter;
49+
return this;
50+
}
51+
52+
public MetricsMeters build() {
53+
return new MetricsMeters(this);
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* A package containing client side classes for grpc metric collection.
3+
*/
4+
5+
package net.devh.boot.grpc.client.metrics;

0 commit comments

Comments
 (0)