Skip to content

Commit a188d8a

Browse files
authored
feat: Add gRPC server metrics (#1031)
Add server metrics defined in gRFC A66: OpenTelemetry Metrics https://github.com/grpc/proposal/blob/master/A66-otel-stats.md
1 parent f72eda3 commit a188d8a

File tree

5 files changed

+603
-0
lines changed

5 files changed

+603
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.server.metrics;
18+
19+
import java.time.Duration;
20+
21+
import io.micrometer.core.instrument.Counter;
22+
import io.micrometer.core.instrument.DistributionSummary;
23+
import io.micrometer.core.instrument.MeterRegistry;
24+
import io.micrometer.core.instrument.Timer;
25+
import io.micrometer.core.instrument.binder.BaseUnits;
26+
27+
/*
28+
* The instruments used to record metrics on server.
29+
*/
30+
public final class MetricsServerInstruments {
31+
32+
private MetricsServerInstruments() {}
33+
34+
/*
35+
* Server side metrics defined in gRFC <a
36+
* href="https://github.com/grpc/proposal/blob/master/A66-otel-stats.md">A66</a>. Please note that these are the
37+
* names used for instrumentation and can be changed by exporters in an unpredictable manner depending on the
38+
* destination.
39+
*/
40+
private static final String SERVER_CALL_STARTED = "grpc.server.call.started";
41+
private static final String SERVER_SENT_COMPRESSED_MESSAGE_SIZE =
42+
"grpc.server.call.sent_total_compressed_message_size";
43+
private static final String SERVER_RECEIVED_COMPRESSED_MESSAGE_SIZE =
44+
"grpc.server.call.rcvd_total_compressed_message_size";
45+
private static final String SERVER_CALL_DURATION =
46+
"grpc.server.call.duration";
47+
private static final double[] DEFAULT_SIZE_BUCKETS =
48+
new double[] {1024d, 2048d, 4096d, 16384d, 65536d, 262144d, 1048576d,
49+
4194304d, 16777216d, 67108864d, 268435456d, 1073741824d, 4294967296d};
50+
private static final Duration[] DEFAULT_LATENCY_BUCKETS =
51+
new Duration[] {Duration.ofNanos(10000), Duration.ofNanos(50000), Duration.ofNanos(100000),
52+
Duration.ofNanos(300000), Duration.ofNanos(600000), Duration.ofNanos(800000),
53+
Duration.ofMillis(1), Duration.ofMillis(2), Duration.ofMillis(3), Duration.ofMillis(4),
54+
Duration.ofMillis(5), Duration.ofMillis(6), Duration.ofMillis(8), Duration.ofMillis(10),
55+
Duration.ofMillis(13), Duration.ofMillis(16), Duration.ofMillis(20), Duration.ofMillis(25),
56+
Duration.ofMillis(30), Duration.ofMillis(40), Duration.ofMillis(50), Duration.ofMillis(65),
57+
Duration.ofMillis(80), Duration.ofMillis(100), Duration.ofMillis(130), Duration.ofMillis(160),
58+
Duration.ofMillis(200), Duration.ofMillis(250), Duration.ofMillis(300), Duration.ofMillis(400),
59+
Duration.ofMillis(500), Duration.ofMillis(650), Duration.ofMillis(800),
60+
Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(5), Duration.ofSeconds(10),
61+
Duration.ofSeconds(20), Duration.ofSeconds(50), Duration.ofSeconds(100)};
62+
63+
static MetricsServerMeters newServerMetricsMeters(MeterRegistry registry) {
64+
MetricsServerMeters.Builder builder = MetricsServerMeters.newBuilder();
65+
66+
builder.setServerCallCounter(Counter.builder(SERVER_CALL_STARTED)
67+
.description("The total number of RPC attempts started from the server side, including "
68+
+ "those that have not completed.")
69+
.baseUnit("call")
70+
.withRegistry(registry));
71+
72+
builder.setSentMessageSizeDistribution(DistributionSummary.builder(
73+
SERVER_SENT_COMPRESSED_MESSAGE_SIZE)
74+
.description("Compressed message bytes sent per server call")
75+
.baseUnit(BaseUnits.BYTES)
76+
.serviceLevelObjectives(DEFAULT_SIZE_BUCKETS)
77+
.withRegistry(registry));
78+
79+
builder.setReceivedMessageSizeDistribution(DistributionSummary.builder(
80+
SERVER_RECEIVED_COMPRESSED_MESSAGE_SIZE)
81+
.description("Compressed message bytes received per server call")
82+
.baseUnit(BaseUnits.BYTES)
83+
.serviceLevelObjectives(DEFAULT_SIZE_BUCKETS)
84+
.withRegistry(registry));
85+
86+
builder.setServerCallDuration(Timer.builder(SERVER_CALL_DURATION)
87+
.description("Time taken to complete a call from server transport's perspective")
88+
.serviceLevelObjectives(DEFAULT_LATENCY_BUCKETS)
89+
.withRegistry(registry));
90+
91+
return builder.build();
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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.server.metrics;
18+
19+
import io.micrometer.core.instrument.Counter;
20+
import io.micrometer.core.instrument.DistributionSummary;
21+
import io.micrometer.core.instrument.Meter.MeterProvider;
22+
import io.micrometer.core.instrument.Timer;
23+
24+
/*
25+
* Collection of server metrics meters.
26+
*/
27+
public class MetricsServerMeters {
28+
29+
private MeterProvider<Counter> serverCallCounter;
30+
private MeterProvider<DistributionSummary> sentMessageSizeDistribution;
31+
private MeterProvider<DistributionSummary> receivedMessageSizeDistribution;
32+
private MeterProvider<Timer> serverCallDuration;
33+
34+
private MetricsServerMeters(Builder builder) {
35+
this.serverCallCounter = builder.serverCallCounter;
36+
this.sentMessageSizeDistribution = builder.sentMessageSizeDistribution;
37+
this.receivedMessageSizeDistribution = builder.receivedMessageSizeDistribution;
38+
this.serverCallDuration = builder.serverCallDuration;
39+
}
40+
41+
public MeterProvider<Counter> getServerCallCounter() {
42+
return this.serverCallCounter;
43+
}
44+
45+
public MeterProvider<DistributionSummary> getSentMessageSizeDistribution() {
46+
return this.sentMessageSizeDistribution;
47+
}
48+
49+
public MeterProvider<DistributionSummary> getReceivedMessageSizeDistribution() {
50+
return this.receivedMessageSizeDistribution;
51+
}
52+
53+
public MeterProvider<Timer> getServerCallDuration() {
54+
return this.serverCallDuration;
55+
}
56+
57+
public static Builder newBuilder() {
58+
return new Builder();
59+
}
60+
61+
static class Builder {
62+
63+
private MeterProvider<Counter> serverCallCounter;
64+
private MeterProvider<DistributionSummary> sentMessageSizeDistribution;
65+
private MeterProvider<DistributionSummary> receivedMessageSizeDistribution;
66+
private MeterProvider<Timer> serverCallDuration;
67+
68+
private Builder() {}
69+
70+
public Builder setServerCallCounter(MeterProvider<Counter> counter) {
71+
this.serverCallCounter = counter;
72+
return this;
73+
}
74+
75+
public Builder setSentMessageSizeDistribution(MeterProvider<DistributionSummary> distribution) {
76+
this.sentMessageSizeDistribution = distribution;
77+
return this;
78+
}
79+
80+
public Builder setReceivedMessageSizeDistribution(MeterProvider<DistributionSummary> distribution) {
81+
this.receivedMessageSizeDistribution = distribution;
82+
return this;
83+
}
84+
85+
public Builder setServerCallDuration(MeterProvider<Timer> timer) {
86+
this.serverCallDuration = timer;
87+
return this;
88+
}
89+
90+
public MetricsServerMeters build() {
91+
return new MetricsServerMeters(this);
92+
}
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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.server.metrics;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
23+
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
24+
import java.util.function.Supplier;
25+
26+
import com.google.common.base.Stopwatch;
27+
28+
import io.grpc.Metadata;
29+
import io.grpc.ServerStreamTracer;
30+
import io.grpc.Status;
31+
import io.micrometer.core.instrument.MeterRegistry;
32+
import io.micrometer.core.instrument.Tags;
33+
34+
/**
35+
* Provides factories for {@link io.grpc.StreamTracer} that records metrics.
36+
*
37+
* <p>
38+
* On the server-side, there is only one ServerStream per each ServerCall, and ServerStream starts earlier than the
39+
* ServerCall. Therefore, only one tracer is created per stream/call and it's the tracer that reports the metrics
40+
* summary.
41+
*
42+
* <b>Note:</b> This class uses experimental grpc-java-API features.
43+
*/
44+
public final class MetricsServerStreamTracers {
45+
46+
private static final Supplier<Stopwatch> STOPWATCH_SUPPLIER = Stopwatch::createUnstarted;
47+
private final Supplier<Stopwatch> stopwatchSupplier;
48+
49+
public MetricsServerStreamTracers() {
50+
this(STOPWATCH_SUPPLIER);
51+
}
52+
53+
public MetricsServerStreamTracers(Supplier<Stopwatch> stopwatchSupplier) {
54+
this.stopwatchSupplier = checkNotNull(stopwatchSupplier, "stopwatchSupplier");
55+
}
56+
57+
/**
58+
* Returns a {@link io.grpc.ServerStreamTracer.Factory} with default metrics definitions.
59+
*
60+
* @param registry The MeterRegistry used to create the metrics.
61+
*/
62+
public ServerStreamTracer.Factory getMetricsServerTracerFactory(MeterRegistry registry) {
63+
return new MetricsServerTracerFactory(registry);
64+
}
65+
66+
/**
67+
* Returns a {@link io.grpc.ServerStreamTracer.Factory} with metrics definitions from custom
68+
* {@link MetricsServerMeters}.
69+
*
70+
* @param meters The MetricsServerMeters used to configure the metrics definitions.
71+
*/
72+
public ServerStreamTracer.Factory getMetricsServerTracerFactory(MetricsServerMeters meters) {
73+
return new MetricsServerTracerFactory(meters);
74+
}
75+
76+
private static final class ServerTracer extends ServerStreamTracer {
77+
private final MetricsServerStreamTracers tracer;
78+
private final String fullMethodName;
79+
private final MetricsServerMeters metricsServerMeters;
80+
private final Stopwatch stopwatch;
81+
private static final AtomicLongFieldUpdater<ServerTracer> outboundWireSizeUpdater =
82+
AtomicLongFieldUpdater.newUpdater(ServerTracer.class, "outboundWireSize");
83+
private static final AtomicLongFieldUpdater<ServerTracer> inboundWireSizeUpdater =
84+
AtomicLongFieldUpdater.newUpdater(ServerTracer.class, "inboundWireSize");
85+
private static final AtomicIntegerFieldUpdater<ServerTracer> streamClosedUpdater =
86+
AtomicIntegerFieldUpdater.newUpdater(ServerTracer.class, "streamClosed");
87+
private volatile long outboundWireSize;
88+
private volatile long inboundWireSize;
89+
private volatile int streamClosed;
90+
91+
92+
ServerTracer(MetricsServerStreamTracers tracer, String fullMethodName, MetricsServerMeters meters) {
93+
this.tracer = checkNotNull(tracer, "tracer");
94+
this.fullMethodName = fullMethodName;
95+
this.metricsServerMeters = meters;
96+
// start stopwatch
97+
this.stopwatch = tracer.stopwatchSupplier.get().start();
98+
}
99+
100+
@Override
101+
public void serverCallStarted(ServerCallInfo<?, ?> callInfo) {
102+
this.metricsServerMeters.getServerCallCounter()
103+
.withTags(Tags.of("grpc.method", this.fullMethodName))
104+
.increment();
105+
}
106+
107+
@Override
108+
public void outboundWireSize(long bytes) {
109+
outboundWireSizeUpdater.getAndAdd(this, bytes);
110+
}
111+
112+
@Override
113+
public void inboundWireSize(long bytes) {
114+
inboundWireSizeUpdater.getAndAdd(this, bytes);
115+
}
116+
117+
@Override
118+
public void streamClosed(Status status) {
119+
if (streamClosedUpdater.getAndSet(this, 1) != 0) {
120+
return;
121+
}
122+
long callLatencyNanos = stopwatch.elapsed(TimeUnit.NANOSECONDS);
123+
124+
Tags serverMetricTags =
125+
Tags.of("grpc.method", this.fullMethodName, "grpc.status", status.getCode().toString());
126+
this.metricsServerMeters.getServerCallDuration()
127+
.withTags(serverMetricTags)
128+
.record(callLatencyNanos, TimeUnit.NANOSECONDS);
129+
this.metricsServerMeters.getSentMessageSizeDistribution()
130+
.withTags(serverMetricTags)
131+
.record(outboundWireSize);
132+
this.metricsServerMeters.getReceivedMessageSizeDistribution()
133+
.withTags(serverMetricTags)
134+
.record(inboundWireSize);
135+
}
136+
}
137+
138+
final class MetricsServerTracerFactory extends ServerStreamTracer.Factory {
139+
140+
private final MetricsServerMeters metricsServerMeters;
141+
142+
MetricsServerTracerFactory(MeterRegistry registry) {
143+
this(MetricsServerInstruments.newServerMetricsMeters(registry));
144+
}
145+
146+
MetricsServerTracerFactory(MetricsServerMeters metricsServerMeters) {
147+
this.metricsServerMeters = metricsServerMeters;
148+
}
149+
150+
@Override
151+
public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) {
152+
return new ServerTracer(MetricsServerStreamTracers.this, fullMethodName, this.metricsServerMeters);
153+
}
154+
}
155+
156+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.server.metrics;
18+
19+
import java.util.concurrent.TimeUnit;
20+
import java.util.function.Supplier;
21+
22+
import com.google.common.base.Stopwatch;
23+
import com.google.common.base.Ticker;
24+
25+
/**
26+
* A manipulated clock that exports a {@link com.google.common.base.Ticker}.
27+
*/
28+
public final class FakeClock {
29+
private long currentTimeNanos;
30+
private final Ticker ticker =
31+
new Ticker() {
32+
@Override
33+
public long read() {
34+
return currentTimeNanos;
35+
}
36+
};
37+
38+
private final Supplier<Stopwatch> stopwatchSupplier = () -> Stopwatch.createUnstarted(ticker);
39+
40+
/**
41+
* Forward the time by the given duration.
42+
*/
43+
public void forwardTime(long value, TimeUnit unit) {
44+
currentTimeNanos += unit.toNanos(value);
45+
}
46+
47+
/**
48+
* Provides a stopwatch instance that uses the fake clock ticker.
49+
*/
50+
public Supplier<Stopwatch> getStopwatchSupplier() {
51+
return stopwatchSupplier;
52+
}
53+
}

0 commit comments

Comments
 (0)