Skip to content

Commit

Permalink
feat: Adds structure for gRPC client metrics instrumentation (#1021)
Browse files Browse the repository at this point in the history
* added a new grpc client metric `grpc.client.attempt.started`
  • Loading branch information
DNVindhya committed Jan 9, 2024
1 parent 4087bbd commit 3afb9e4
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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.MeterRegistry;

/*
* The instruments used to record metrics on client.
*/
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.
*/
private static final String CLIENT_ATTEMPT_STARTED = "grpc.client.attempt.started";

static MetricsMeters newClientMetricsMeters(MeterRegistry registry) {
MetricsMeters.Builder builder = MetricsMeters.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));
return builder.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
import io.micrometer.core.instrument.MeterRegistry;

/**
* A gRPC client interceptor that collects gRPC metrics.
*
* <b>Note:</b> This class uses experimental grpc-java-API features.
*/
public class MetricsClientInterceptor implements ClientInterceptor {

private final MetricsMeters metricsMeters;

/**
* 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);
}

@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {

/*
* This is a per call ClientStreamTracer.Factory which creates a new stream tracer for each attempt under the
* 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);

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) {
super.onClose(status, trailers);
}
},
headers);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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 static com.google.common.base.Preconditions.checkNotNull;

import io.grpc.ClientStreamTracer;
import io.grpc.ClientStreamTracer.StreamInfo;
import io.grpc.Metadata;
import io.micrometer.core.instrument.Tags;

/**
* Provides factories for {@link io.grpc.StreamTracer} that records metrics.
*
* <p>
* On the client-side, a factory is created for each call, and the factory creates a stream tracer for each attempt.
*
* <b>Note:</b> This class uses experimental grpc-java-API features.
*/
public final class MetricsClientStreamTracers {

private MetricsClientStreamTracers() {}

private static final class ClientTracer extends ClientStreamTracer {
private final CallAttemptsTracerFactory attemptsState;
private final StreamInfo info;
private final String fullMethodName;

ClientTracer(CallAttemptsTracerFactory attemptsState, StreamInfo info, String fullMethodName) {
this.attemptsState = attemptsState;
this.info = info;
this.fullMethodName = fullMethodName;
}

}

static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
private final String fullMethodName;
private final MetricsMeters metricsMeters;
private boolean attemptRecorded;

CallAttemptsTracerFactory(String fullMethodName, MetricsMeters metricsMeters) {
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
this.metricsMeters = checkNotNull(metricsMeters, "metricsMeters");

// Record here in case newClientStreamTracer() would never be called.
this.metricsMeters.getAttemptCounter()
.withTags(Tags.of("grpc.method", fullMethodName))
.increment();
this.attemptRecorded = true;
}

@Override
public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata metadata) {
if (!this.attemptRecorded) {
this.metricsMeters.getAttemptCounter()
.withTags((Tags.of("grpc.method", fullMethodName)))
.increment();
} else {
this.attemptRecorded = false;
}
return new ClientTracer(this, info, fullMethodName);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.Meter.MeterProvider;

/*
* Collection of metrics meters.
*/
public class MetricsMeters {

private MeterProvider<Counter> attemptCounter;

private MetricsMeters(Builder builder) {
this.attemptCounter = builder.attemptCounter;
}

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

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

static class Builder {

private MeterProvider<Counter> attemptCounter;

private Builder() {}

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

public MetricsMeters build() {
return new MetricsMeters(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* A package containing client side classes for grpc metric collection.
*/

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

0 comments on commit 3afb9e4

Please sign in to comment.