Skip to content

Commit

Permalink
feat: Add gRPC client attempt metrics (#1037)
Browse files Browse the repository at this point in the history
* add client attempt and client call metrics
  • Loading branch information
DNVindhya authored Jan 26, 2024
1 parent 78203b0 commit 9c4f9ee
Show file tree
Hide file tree
Showing 8 changed files with 995 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@

package net.devh.boot.grpc.client.metrics;

import java.time.Duration;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.BaseUnits;

/*
* The instruments used to record metrics on client.
Expand All @@ -27,21 +32,70 @@ public final class MetricsClientInstruments {
private MetricsClientInstruments() {}

/*
* This is a client side metric defined in gRFC <a
* href="https://github.com/grpc/proposal/blob/master/A66-otel-stats.md">A66</a>. Please note that this is the name
* used for instrumentation and can be changed by exporters in an unpredictable manner depending on the destination.
* Client side metrics defined in gRFC <a
* href="https://github.com/grpc/proposal/blob/master/A66-otel-stats.md">A66</a>. Please note that these are the
* names used for instrumentation and can be changed by exporters in an unpredictable manner depending on the
* destination.
*/
private static final String CLIENT_ATTEMPT_STARTED = "grpc.client.attempt.started";
private static final String CLIENT_ATTEMPT_SENT_COMPRESSED_MESSAGE_SIZE =
"grpc.client.attempt.sent_total_compressed_message_size";
private static final String CLIENT_ATTEMPT_RECEIVED_COMPRESSED_MESSAGE_SIZE =
"grpc.client.attempt.rcvd_total_compressed_message_size";
private static final String CLIENT_ATTEMPT_DURATION =
"grpc.client.attempt.duration";
private static final String CLIENT_CALL_DURATION =
"grpc.client.call.duration";
private static final double[] DEFAULT_SIZE_BUCKETS =
new double[] {1024d, 2048d, 4096d, 16384d, 65536d, 262144d, 1048576d,
4194304d, 16777216d, 67108864d, 268435456d, 1073741824d, 4294967296d};
private static final Duration[] DEFAULT_LATENCY_BUCKETS =
new Duration[] {Duration.ofNanos(10000), Duration.ofNanos(50000), Duration.ofNanos(100000),
Duration.ofNanos(300000), Duration.ofNanos(600000), Duration.ofNanos(800000),
Duration.ofMillis(1), Duration.ofMillis(2), Duration.ofMillis(3), Duration.ofMillis(4),
Duration.ofMillis(5), Duration.ofMillis(6), Duration.ofMillis(8), Duration.ofMillis(10),
Duration.ofMillis(13), Duration.ofMillis(16), Duration.ofMillis(20), Duration.ofMillis(25),
Duration.ofMillis(30), Duration.ofMillis(40), Duration.ofMillis(50), Duration.ofMillis(65),
Duration.ofMillis(80), Duration.ofMillis(100), Duration.ofMillis(130), Duration.ofMillis(160),
Duration.ofMillis(200), Duration.ofMillis(250), Duration.ofMillis(300), Duration.ofMillis(400),
Duration.ofMillis(500), Duration.ofMillis(650), Duration.ofMillis(800),
Duration.ofSeconds(1), Duration.ofSeconds(2), Duration.ofSeconds(5), Duration.ofSeconds(10),
Duration.ofSeconds(20), Duration.ofSeconds(50), Duration.ofSeconds(100)};

static MetricsMeters newClientMetricsMeters(MeterRegistry registry) {
MetricsMeters.Builder builder = MetricsMeters.newBuilder();
static MetricsClientMeters newClientMetricsMeters(MeterRegistry registry) {
MetricsClientMeters.Builder builder = MetricsClientMeters.newBuilder();

builder.setAttemptCounter(Counter.builder(CLIENT_ATTEMPT_STARTED)
.description(
"The total number of RPC attempts started from the client side, including "
+ "those that have not completed.")
.baseUnit("attempt")
.withRegistry(registry));

builder.setSentMessageSizeDistribution(DistributionSummary.builder(
CLIENT_ATTEMPT_SENT_COMPRESSED_MESSAGE_SIZE)
.description("Compressed message bytes sent per client call attempt")
.baseUnit(BaseUnits.BYTES)
.serviceLevelObjectives(DEFAULT_SIZE_BUCKETS)
.withRegistry(registry));

builder.setReceivedMessageSizeDistribution(DistributionSummary.builder(
CLIENT_ATTEMPT_RECEIVED_COMPRESSED_MESSAGE_SIZE)
.description("Compressed message bytes received per call attempt")
.baseUnit(BaseUnits.BYTES)
.serviceLevelObjectives(DEFAULT_SIZE_BUCKETS)
.withRegistry(registry));

builder.setClientAttemptDuration(Timer.builder(CLIENT_ATTEMPT_DURATION)
.description("Time taken to complete a client call attempt")
.serviceLevelObjectives(DEFAULT_LATENCY_BUCKETS)
.withRegistry(registry));

builder.setClientCallDuration(Timer.builder(CLIENT_CALL_DURATION)
.description("Time taken by gRPC to complete an RPC from application's perspective")
.serviceLevelObjectives(DEFAULT_LATENCY_BUCKETS)
.withRegistry(registry));

return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

package net.devh.boot.grpc.client.metrics;

import java.util.function.Supplier;

import com.google.common.base.Stopwatch;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
Expand All @@ -34,16 +38,22 @@
*/
public class MetricsClientInterceptor implements ClientInterceptor {

private final MetricsMeters metricsMeters;
private final MetricsClientMeters metricsClientMeters;
private final Supplier<Stopwatch> stopwatchSupplier;

/**
* Creates a new gRPC client interceptor that collects metrics into the given
* {@link io.micrometer.core.instrument.MeterRegistry}.
*
* @param registry The MeterRegistry to use.
*/
public MetricsClientInterceptor(MeterRegistry registry) {
this.metricsMeters = MetricsClientInstruments.newClientMetricsMeters(registry);
public MetricsClientInterceptor(MeterRegistry registry, Supplier<Stopwatch> stopwatchSupplier) {
this(MetricsClientInstruments.newClientMetricsMeters(registry), stopwatchSupplier);
}

public MetricsClientInterceptor(MetricsClientMeters meters, Supplier<Stopwatch> stopwatchSupplier) {
this.metricsClientMeters = meters;
this.stopwatchSupplier = stopwatchSupplier;
}

@Override
Expand All @@ -55,20 +65,22 @@ public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
* same call. Each call needs a dedicated factory as they share the same method descriptor.
*/
final MetricsClientStreamTracers.CallAttemptsTracerFactory tracerFactory =
new MetricsClientStreamTracers.CallAttemptsTracerFactory(method.getFullMethodName(),
metricsMeters);
new MetricsClientStreamTracers.CallAttemptsTracerFactory(
new MetricsClientStreamTracers(stopwatchSupplier),
method.getFullMethodName(),
metricsClientMeters);

ClientCall<ReqT, RespT> call =
next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory));

// TODO(dnvindhya): Collect the actual response/error in the SimpleForwardingClientCall
return new SimpleForwardingClientCall<ReqT, RespT>(call) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
delegate().start(
new SimpleForwardingClientCallListener<RespT>(responseListener) {
@Override
public void onClose(Status status, Metadata trailers) {
tracerFactory.callEnded(status);
super.onClose(status, trailers);
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2016-2023 The gRPC-Spring Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package net.devh.boot.grpc.client.metrics;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Meter.MeterProvider;
import io.micrometer.core.instrument.Timer;

/*
* Collection of client metrics meters.
*/
public class MetricsClientMeters {

private MeterProvider<Counter> attemptCounter;
private MeterProvider<DistributionSummary> sentMessageSizeDistribution;
private MeterProvider<DistributionSummary> receivedMessageSizeDistribution;
private MeterProvider<Timer> clientAttemptDuration;
private MeterProvider<Timer> clientCallDuration;

private MetricsClientMeters(Builder builder) {
this.attemptCounter = builder.attemptCounter;
this.sentMessageSizeDistribution = builder.sentMessageSizeDistribution;
this.receivedMessageSizeDistribution = builder.receivedMessageSizeDistribution;
this.clientAttemptDuration = builder.clientAttemptDuration;
this.clientCallDuration = builder.clientCallDuration;
}

public MeterProvider<Counter> getAttemptCounter() {
return this.attemptCounter;
}

public MeterProvider<DistributionSummary> getSentMessageSizeDistribution() {
return this.sentMessageSizeDistribution;
}

public MeterProvider<DistributionSummary> getReceivedMessageSizeDistribution() {
return this.receivedMessageSizeDistribution;
}

public MeterProvider<Timer> getClientAttemptDuration() {
return this.clientAttemptDuration;
}

public MeterProvider<Timer> getClientCallDuration() {
return this.clientCallDuration;
}

public static Builder newBuilder() {
return new Builder();
}

static class Builder {

private MeterProvider<Counter> attemptCounter;
private MeterProvider<DistributionSummary> sentMessageSizeDistribution;
private MeterProvider<DistributionSummary> receivedMessageSizeDistribution;
private MeterProvider<Timer> clientAttemptDuration;
private MeterProvider<Timer> clientCallDuration;

private Builder() {}

public Builder setAttemptCounter(MeterProvider<Counter> counter) {
this.attemptCounter = counter;
return this;
}

public Builder setSentMessageSizeDistribution(MeterProvider<DistributionSummary> distribution) {
this.sentMessageSizeDistribution = distribution;
return this;
}

public Builder setReceivedMessageSizeDistribution(MeterProvider<DistributionSummary> distribution) {
this.receivedMessageSizeDistribution = distribution;
return this;
}

public Builder setClientAttemptDuration(MeterProvider<Timer> timer) {
this.clientAttemptDuration = timer;
return this;
}

public Builder setClientCallDuration(MeterProvider<Timer> timer) {
this.clientCallDuration = timer;
return this;
}

public MetricsClientMeters build() {
return new MetricsClientMeters(this);
}
}
}
Loading

0 comments on commit 9c4f9ee

Please sign in to comment.