From 4149ed552e9b7fbe2b7d860fff9b3d79782130ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B8rge=20Andersen?= Date: Tue, 17 Dec 2024 19:01:14 +0100 Subject: [PATCH] Add autoconfiguration for client side observation interceptor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [resolves #51] Signed-off-by: Jonas Bjørge Andersen --- .../antora/modules/ROOT/pages/client.adoc | 4 + ...rpcClientObservationAutoConfiguration.java | 41 +++++++++ ...itional-spring-configuration-metadata.json | 7 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...lientObservationAutoConfigurationTest.java | 89 +++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfigurationTest.java diff --git a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc index a60f626..fae8be7 100644 --- a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc +++ b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc @@ -131,3 +131,7 @@ However, by setting the `mergeWithGlobalInterceptors` parameter on the channel f You can use this option if you want to add a per-client interceptor between global interceptors. IMPORTANT: The per-channel interceptors you pass in must either be bean instances marked with `@Order` or regular objects that implement the `Ordered` interface to be properly merged/ordered with the global interceptors. + +== Observability + +Spring gRPC provides an autoconfigured interceptor that can be used to provide observability to your gRPC clients. diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfiguration.java new file mode 100644 index 0000000..7233e17 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024-2024 the original author or 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 + * + * https://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 org.springframework.grpc.autoconfigure.client; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GlobalClientInterceptor; + +@AutoConfiguration( + afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration") +@ConditionalOnClass({ ObservationRegistry.class, ObservationGrpcClientInterceptor.class }) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnProperty(name = "spring.grpc.client.observation.enabled", havingValue = "true", matchIfMissing = true) + +public class GrpcClientObservationAutoConfiguration { + + @Bean + @GlobalClientInterceptor + ObservationGrpcClientInterceptor observationGrpcClientInterceptor(ObservationRegistry observationRegistry) { + return new ObservationGrpcClientInterceptor(observationRegistry); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bbb55ff..5f9372e 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -22,7 +22,12 @@ "type": "java.lang.Boolean", "description": "Whether to enable Observations on the server.", "defaultValue": true - + }, + { + "name": "spring.grpc.client.observations.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the client.", + "defaultValue": true } ] } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 195bf58..cee7caf 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,5 @@ org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration +org.springframework.grpc.autoconfigure.client.GrpcClientObservationAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerAutoConfiguration org.springframework.grpc.autoconfigure.server.health.GrpcServerHealthAutoConfiguration diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfigurationTest.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfigurationTest.java new file mode 100644 index 0000000..fa2acec --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientObservationAutoConfigurationTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024-2024 the original author or 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 + * + * https://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 org.springframework.grpc.autoconfigure.client; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.client.GlobalClientInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link GrpcClientObservationAutoConfiguration}. + */ +class GrpcClientObservationAutoConfigurationTests { + + private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class)); + + private ApplicationContextRunner validContextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class)) + .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); + } + + @Test + void whenObservationRegistryNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationGrpcClientInterceptorNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationGrpcClientInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotProvidedThenAutoConfigSkipped() { + this.baseContextRunner + .run(context -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyEnabledThenAutoConfigNotSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.observation.enabled=true") + .run(context -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyDisabledThenAutoConfigIsSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.observation.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { + this.validContextRunner().run((context) -> { + assertThat(context).hasSingleBean(ObservationGrpcClientInterceptor.class); + assertThat(context.getBeansWithAnnotation(GlobalClientInterceptor.class)).hasEntrySatisfying( + "observationGrpcClientInterceptor", + bean -> assertThat(bean.getClass().isAssignableFrom(ObservationGrpcClientInterceptor.class)) + .isTrue()); + }); + } + +} \ No newline at end of file