diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInstruments.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInstruments.java new file mode 100644 index 000000000..d105ab0e8 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInstruments.java @@ -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 A66. 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(); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInterceptor.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInterceptor.java new file mode 100644 index 000000000..0465c65e3 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientInterceptor.java @@ -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. + * + * Note: 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 ClientCall interceptCall( + MethodDescriptor 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 call = + next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory)); + + // TODO(dnvindhya): Collect the actual response/error in the SimpleForwardingClientCall + return new SimpleForwardingClientCall(call) { + @Override + public void start(Listener responseListener, Metadata headers) { + delegate().start( + new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onClose(Status status, Metadata trailers) { + super.onClose(status, trailers); + } + }, + headers); + } + }; + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientStreamTracers.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientStreamTracers.java new file mode 100644 index 000000000..7ac506700 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsClientStreamTracers.java @@ -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. + * + *

+ * On the client-side, a factory is created for each call, and the factory creates a stream tracer for each attempt. + * + * Note: 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); + } + + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsMeters.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsMeters.java new file mode 100644 index 000000000..728a7ccad --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/MetricsMeters.java @@ -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 attemptCounter; + + private MetricsMeters(Builder builder) { + this.attemptCounter = builder.attemptCounter; + } + + public MeterProvider getAttemptCounter() { + return this.attemptCounter; + } + + public static Builder newBuilder() { + return new Builder(); + } + + static class Builder { + + private MeterProvider attemptCounter; + + private Builder() {} + + public Builder setAttemptCounter(MeterProvider counter) { + this.attemptCounter = counter; + return this; + } + + public MetricsMeters build() { + return new MetricsMeters(this); + } + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/package-info.java new file mode 100644 index 000000000..6f6782a25 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metrics/package-info.java @@ -0,0 +1,5 @@ +/** + * A package containing client side classes for grpc metric collection. + */ + +package net.devh.boot.grpc.client.metrics;