From 98506c666dabbfaa819a2f9fe85cbc287e8efac3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Jun 2025 12:13:03 -0700 Subject: [PATCH] Introduce ObservationHandlerGroup interface Add a new `ObservationHandlerGroup` interface that allows grouping logic to be implemented by the metrics and tracing modules. This update removes the need for `ObservabilityAutoConfiguration`. Closes gh-45746 --- .../ObservabilityAutoConfiguration.java | 131 ----- .../observability/package-info.java | 20 - .../MicrometerTracingAutoConfiguration.java | 11 +- ...racingAndMeterObservationHandlerGroup.java | 92 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../ObservabilityAutoConfigurationTests.java | 514 ------------------ ...crometerTracingAutoConfigurationTests.java | 28 + ...gAndMeterObservationHandlerGroupTests.java | 118 ++++ .../MetricsAutoConfiguration.java | 19 + .../autoconfigure/MetricsProperties.java | 27 + .../MetricsAutoConfigurationTests.java | 33 ++ .../ObservationAutoConfiguration.java | 4 +- .../ObservationHandlerGroup.java | 79 +++ .../ObservationHandlerGrouping.java | 82 --- .../ObservationHandlerGroups.java | 81 +++ .../ObservationRegistryConfigurer.java | 25 +- .../ObservationRegistryPostProcessor.java | 8 +- ...itional-spring-configuration-metadata.json | 10 + .../ObservationAutoConfigurationTests.java | 1 - .../ObservationHandlerGroupTests.java | 96 ++++ ...ava => ObservationHandlerGroupsTests.java} | 17 +- ...ability.AutoConfigureObservability.imports | 3 - 22 files changed, 617 insertions(+), 783 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfiguration.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroup.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroupTests.java create mode 100644 spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroup.java delete mode 100644 spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGrouping.java create mode 100644 spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroups.java create mode 100644 spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupTests.java rename spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/{ObservationHandlerGroupingTests.java => ObservationHandlerGroupsTests.java} (86%) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfiguration.java deleted file mode 100644 index 9cab8e896e3..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfiguration.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2012-present 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.boot.actuate.autoconfigure.observability; - -import java.util.List; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler.IgnoredMeters; -import io.micrometer.core.instrument.observation.MeterObservationHandler; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; -import io.micrometer.tracing.handler.TracingObservationHandler; - -import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.observation.autoconfigure.ObservationHandlerGrouping; -import org.springframework.boot.observation.autoconfigure.ObservationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Observation API. - * - * @author Moritz Halbritter - * @author Brian Clozel - * @author Jonatan Ivanov - * @author Vedran Pavic - * @since 3.0.0 - */ -@AutoConfiguration(beforeName = "org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration", - afterName = "org.springframework.boot.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration", - after = MicrometerTracingAutoConfiguration.class) -@ConditionalOnClass({ ObservationRegistry.class, ObservationHandlerGrouping.class }) -public class ObservabilityAutoConfiguration { - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(MeterRegistry.class) - @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") - static class OnlyMetricsConfiguration { - - @Bean - ObservationHandlerGrouping metricsObservationHandlerGrouping() { - return new ObservationHandlerGrouping(MeterObservationHandler.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Tracer.class) - @ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry") - static class OnlyTracingConfiguration { - - @Bean - ObservationHandlerGrouping tracingObservationHandlerGrouping() { - return new ObservationHandlerGrouping(TracingObservationHandler.class); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ MeterRegistry.class, Tracer.class }) - static class MetricsWithTracingConfiguration { - - @Bean - ObservationHandlerGrouping metricsAndTracingObservationHandlerGrouping() { - return new ObservationHandlerGrouping( - List.of(TracingObservationHandler.class, MeterObservationHandler.class)); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(MeterRegistry.class) - @ConditionalOnMissingBean(MeterObservationHandler.class) - @EnableConfigurationProperties(ObservationProperties.class) - static class MeterObservationHandlerConfiguration { - - @ConditionalOnMissingBean(type = "io.micrometer.tracing.Tracer") - @Configuration(proxyBeanMethods = false) - static class OnlyMetricsMeterObservationHandlerConfiguration { - - @Bean - DefaultMeterObservationHandler defaultMeterObservationHandler(MeterRegistry meterRegistry, - ObservationProperties properties) { - return properties.getLongTaskTimer().isEnabled() ? new DefaultMeterObservationHandler(meterRegistry) - : new DefaultMeterObservationHandler(meterRegistry, IgnoredMeters.LONG_TASK_TIMER); - } - - } - - @ConditionalOnBean(Tracer.class) - @Configuration(proxyBeanMethods = false) - static class TracingAndMetricsObservationHandlerConfiguration { - - @Bean - TracingAwareMeterObservationHandler tracingAwareMeterObservationHandler( - MeterRegistry meterRegistry, Tracer tracer, ObservationProperties properties) { - DefaultMeterObservationHandler delegate = properties.getLongTaskTimer().isEnabled() - ? new DefaultMeterObservationHandler(meterRegistry) - : new DefaultMeterObservationHandler(meterRegistry, IgnoredMeters.LONG_TASK_TIMER); - return new TracingAwareMeterObservationHandler<>(delegate, tracer); - } - - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/package-info.java deleted file mode 100644 index dd45a4bf760..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/observability/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-present 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. - */ - -/** - * Auto-configuration for observability (metrics and tracing). - */ -package org.springframework.boot.actuate.autoconfigure.observability; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index 02f6be84f1e..d596a909d63 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -27,6 +27,7 @@ import io.micrometer.tracing.annotation.SpanTagAnnotationHandler; import io.micrometer.tracing.handler.DefaultTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.handler.TracingObservationHandler; import io.micrometer.tracing.propagation.Propagator; import org.aspectj.weaver.Advice; @@ -37,6 +38,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -45,6 +47,7 @@ import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; +import org.springframework.util.ClassUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Tracing API. @@ -54,7 +57,6 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext; * @since 3.0.0 */ @AutoConfiguration -@ConditionalOnClass(Tracer.class) @ConditionalOnBean(Tracer.class) public class MicrometerTracingAutoConfiguration { @@ -75,6 +77,13 @@ public class MicrometerTracingAutoConfiguration { */ public static final int SENDER_TRACING_OBSERVATION_HANDLER_ORDER = 2000; + @Bean + public ObservationHandlerGroup tracingObservationHandlerGroup(Tracer tracer) { + return ClassUtils.isPresent("io.micrometer.core.instrument.MeterRegistry", null) + ? new TracingAndMeterObservationHandlerGroup(tracer) + : ObservationHandlerGroup.of(TracingObservationHandler.class); + } + @Bean @ConditionalOnMissingBean @Order(DEFAULT_TRACING_OBSERVATION_HANDLER_ORDER) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroup.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroup.java new file mode 100644 index 00000000000..4c93ecdfdc6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroup.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-present 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.boot.actuate.autoconfigure.tracing; + +import java.util.ArrayList; +import java.util.List; + +import io.micrometer.core.instrument.observation.MeterObservationHandler; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; +import io.micrometer.tracing.handler.TracingObservationHandler; + +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; + +/** + * {@link ObservationHandlerGroup} that considers both {@link TracingObservationHandler} + * and {@link MeterObservationHandler} types as members. This group takes precedence over + * any regular {@link MeterObservationHandler} group in order to use ensure + * {@link TracingAwareMeterObservationHandler} wrapping is applied during registration. + * + * @author Phillip Webb + */ +class TracingAndMeterObservationHandlerGroup implements ObservationHandlerGroup { + + private final Tracer tracer; + + TracingAndMeterObservationHandlerGroup(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public boolean isMember(ObservationHandler handler) { + return MeterObservationHandler.class.isInstance(handler) || TracingObservationHandler.class.isInstance(handler); + } + + @Override + public int compareTo(ObservationHandlerGroup other) { + if (other instanceof TracingAndMeterObservationHandlerGroup) { + return 0; + } + return MeterObservationHandler.class.isAssignableFrom(other.handlerType()) ? -1 : 1; + } + + @Override + public void registerMembers(ObservationConfig config, List> members) { + List> tracingHandlers = new ArrayList<>(members.size()); + List> metricsHandlers = new ArrayList<>(members.size()); + for (ObservationHandler member : members) { + if (member instanceof MeterObservationHandler meterObservationHandler + && !(member instanceof TracingAwareMeterObservationHandler)) { + metricsHandlers.add(new TracingAwareMeterObservationHandler<>(meterObservationHandler, this.tracer)); + } + else { + tracingHandlers.add(member); + } + } + registerHandlers(config, tracingHandlers); + registerHandlers(config, metricsHandlers); + } + + private void registerHandlers(ObservationConfig config, List> handlers) { + if (handlers.size() == 1) { + config.observationHandler(handlers.get(0)); + } + else if (!handlers.isEmpty()) { + config.observationHandler(new FirstMatchingCompositeObservationHandler(handlers)); + } + } + + @Override + public Class handlerType() { + return TracingObservationHandler.class; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6e8379a1dce..dc63be11aa4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,6 +1,5 @@ org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive.ReactiveCloudFoundryActuatorAutoConfiguration org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.CloudFoundryActuatorAutoConfiguration -org.springframework.boot.actuate.autoconfigure.observability.ObservabilityAutoConfiguration org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfigurationTests.java deleted file mode 100644 index 290d10e566e..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/observability/ObservabilityAutoConfigurationTests.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2012-present 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.boot.actuate.autoconfigure.observability; - -import java.util.ArrayList; -import java.util.List; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.observation.MeterObservationHandler; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.Observation; -import io.micrometer.observation.Observation.Context; -import io.micrometer.observation.ObservationHandler; -import io.micrometer.observation.ObservationHandler.AllMatchingCompositeObservationHandler; -import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.aop.ObservedAspect; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; -import io.micrometer.tracing.handler.TracingObservationHandler; -import org.junit.jupiter.api.Test; -import org.mockito.Answers; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration; -import org.springframework.boot.observation.autoconfigure.ObservationHandlerGrouping; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.annotation.Order; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link ObservabilityAutoConfiguration}. - * - * @author Moritz Halbritter - * @author Jonatan Ivanov - * @author Vedran Pavic - */ -class ObservabilityAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withBean(SimpleMeterRegistry.class) - .withPropertyValues("management.observations.annotations.enabled=true") - .withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) - .withConfiguration( - AutoConfigurations.of(ObservationAutoConfiguration.class, ObservabilityAutoConfiguration.class)); - - private final ApplicationContextRunner tracingContextRunner = new ApplicationContextRunner() - .withBean(SimpleMeterRegistry.class) - .withPropertyValues("management.observations.annotations.enabled=true") - .withUserConfiguration(TracerConfiguration.class) - .withConfiguration( - AutoConfigurations.of(ObservationAutoConfiguration.class, ObservabilityAutoConfiguration.class)); - - @Test - void supplyMeterHandlerAndGroupingWhenMicrometerCoreIsOnClassPathButTracingIsNot() { - this.contextRunner.run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test-observation", observationRegistry).stop(); - assertThat(context).hasSingleBean(ObservationHandler.class); - assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); - assertThat(context).hasSingleBean(ObservedAspect.class); - assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); - assertThat(context).hasBean("metricsObservationHandlerGrouping"); - }); - } - - @Test - void supplyOnlyTracingObservationHandlerGroupingWhenMicrometerCoreIsNotOnClassPathButTracingIs() { - this.tracingContextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.core")).run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test-observation", observationRegistry).stop(); - assertThat(context).doesNotHaveBean(ObservationHandler.class); - assertThat(context).hasSingleBean(ObservedAspect.class); - assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); - assertThat(context).hasBean("tracingObservationHandlerGrouping"); - }); - } - - @Test - void supplyMeterHandlerAndGroupingWhenMicrometerCoreAndTracingAreOnClassPath() { - this.tracingContextRunner.run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - // Intentionally not stopped since that will trigger additional logic in - // TracingAwareMeterObservationHandler that we don't test here - Observation.start("test-observation", observationRegistry); - assertThat(context).hasSingleBean(ObservationHandler.class); - assertThat(context).hasSingleBean(ObservedAspect.class); - assertThat(context).hasSingleBean(TracingAwareMeterObservationHandler.class); - assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); - assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); - }); - } - - @Test - void supplyMeterHandlerAndGroupingWhenMicrometerCoreAndTracingAreOnClassPathButThereIsNoTracer() { - new ApplicationContextRunner().withBean(SimpleMeterRegistry.class) - .withPropertyValues("management.observations.annotations.enabled=true") - .withConfiguration( - AutoConfigurations.of(ObservationAutoConfiguration.class, ObservabilityAutoConfiguration.class)) - .run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test-observation", observationRegistry).stop(); - assertThat(context).hasSingleBean(ObservationHandler.class); - assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); - assertThat(context).hasSingleBean(ObservedAspect.class); - assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); - assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); - }); - } - - @Test - void autoConfiguresDefaultMeterObservationHandler() { - this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("test-observation", observationRegistry).stop(); - // When a DefaultMeterObservationHandler is registered, every stopped - // Observation leads to a timer - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - assertThat(meterRegistry.get("test-observation").timer().count()).isOne(); - assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); - assertThat(context).hasSingleBean(ObservationHandler.class); - assertThat(context).hasSingleBean(ObservedAspect.class); - }); - } - - @Test - void allowsDefaultMeterObservationHandlerToBeDisabled() { - this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)) - .run((context) -> assertThat(context).doesNotHaveBean(ObservationHandler.class)); - } - - @Test - void autoConfiguresObservationHandlers() { - this.contextRunner.withUserConfiguration(ObservationHandlers.class).run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - List> handlers = context.getBean(CalledHandlers.class).getCalledHandlers(); - Observation.start("test-observation", observationRegistry).stop(); - assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); - assertThat(handlers).hasSize(2); - // Multiple MeterObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first one - assertThat(handlers.get(0)).isInstanceOf(CustomMeterObservationHandler.class); - assertThat(((CustomMeterObservationHandler) handlers.get(0)).getName()) - .isEqualTo("customMeterObservationHandler1"); - // Regular handlers are registered last - assertThat(handlers.get(1)).isInstanceOf(CustomObservationHandler.class); - assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); - assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); - }); - } - - @Test - void autoConfiguresObservationHandlerWithCustomContext() { - this.contextRunner.withUserConfiguration(ObservationHandlerWithCustomContextConfiguration.class) - .run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - List> handlers = context.getBean(CalledHandlers.class).getCalledHandlers(); - CustomContext customContext = new CustomContext(); - Observation.start("test-observation", () -> customContext, observationRegistry).stop(); - assertThat(handlers).hasSize(1); - assertThat(handlers.get(0)).isInstanceOf(ObservationHandlerWithCustomContext.class); - assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); - assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); - }); - } - - @Test - void autoConfiguresTracingAwareMeterObservationHandler() { - this.tracingContextRunner.withUserConfiguration(CustomTracingObservationHandlers.class).run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - List> handlers = context.getBean(CalledHandlers.class).getCalledHandlers(); - // Intentionally not stopped since that will trigger additional logic in - // TracingAwareMeterObservationHandler that we don't test here - Observation.start("test-observation", observationRegistry); - assertThat(handlers).hasSize(1); - assertThat(handlers.get(0)).isInstanceOf(CustomTracingObservationHandler.class); - assertThat(context).hasSingleBean(TracingAwareMeterObservationHandler.class); - assertThat(context.getBeansOfType(ObservationHandler.class)).hasSize(2); - }); - } - - @Test - void autoConfiguresObservationHandlerWhenTracingIsActive() { - this.tracingContextRunner.withUserConfiguration(ObservationHandlersTracing.class).run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - List> handlers = context.getBean(CalledHandlers.class).getCalledHandlers(); - Observation.start("test-observation", observationRegistry).stop(); - assertThat(handlers).hasSize(3); - // Multiple TracingObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first one - assertThat(handlers.get(0)).isInstanceOf(CustomTracingObservationHandler.class); - assertThat(((CustomTracingObservationHandler) handlers.get(0)).getName()) - .isEqualTo("customTracingHandler1"); - // Multiple MeterObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first one - assertThat(handlers.get(1)).isInstanceOf(CustomMeterObservationHandler.class); - assertThat(((CustomMeterObservationHandler) handlers.get(1)).getName()) - .isEqualTo("customMeterObservationHandler1"); - // Regular handlers are registered last - assertThat(handlers.get(2)).isInstanceOf(CustomObservationHandler.class); - assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); - assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); - }); - } - - @Test - void shouldEnableLongTaskTimersByDefault() { - this.contextRunner.run((context) -> { - DefaultMeterObservationHandler handler = context.getBean(DefaultMeterObservationHandler.class); - assertThat(handler).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", true); - }); - } - - @Test - void shouldDisableLongTaskTimerIfPropertyIsSet() { - this.contextRunner.withPropertyValues("management.observations.long-task-timer.enabled=false") - .run((context) -> { - DefaultMeterObservationHandler handler = context.getBean(DefaultMeterObservationHandler.class); - assertThat(handler).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", false); - }); - } - - @Test - @SuppressWarnings("unchecked") - void shouldEnableLongTaskTimersForTracingByDefault() { - this.tracingContextRunner.run((context) -> { - TracingAwareMeterObservationHandler tracingHandler = context - .getBean(TracingAwareMeterObservationHandler.class); - Object delegate = ReflectionTestUtils.getField(tracingHandler, "delegate"); - assertThat(delegate).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", true); - }); - } - - @Test - @SuppressWarnings("unchecked") - void shouldDisableLongTaskTimerForTracingIfPropertyIsSet() { - this.tracingContextRunner.withPropertyValues("management.observations.long-task-timer.enabled=false") - .run((context) -> { - TracingAwareMeterObservationHandler tracingHandler = context - .getBean(TracingAwareMeterObservationHandler.class); - Object delegate = ReflectionTestUtils.getField(tracingHandler, "delegate"); - assertThat(delegate).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", false); - }); - } - - @Configuration(proxyBeanMethods = false) - @Import(CalledHandlersConfiguration.class) - static class ObservationHandlers { - - @Bean - @Order(4) - AllMatchingCompositeObservationHandler customAllMatchingCompositeObservationHandler() { - return new AllMatchingCompositeObservationHandler(); - } - - @Bean - @Order(3) - FirstMatchingCompositeObservationHandler customFirstMatchingCompositeObservationHandler() { - return new FirstMatchingCompositeObservationHandler(); - } - - @Bean - @Order(2) - ObservationHandler customObservationHandler(CalledHandlers calledHandlers) { - return new CustomObservationHandler(calledHandlers); - } - - @Bean - @Order(1) - MeterObservationHandler customMeterObservationHandler2(CalledHandlers calledHandlers) { - return new CustomMeterObservationHandler("customMeterObservationHandler2", calledHandlers); - } - - @Bean - @Order(0) - MeterObservationHandler customMeterObservationHandler1(CalledHandlers calledHandlers) { - return new CustomMeterObservationHandler("customMeterObservationHandler1", calledHandlers); - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(CalledHandlersConfiguration.class) - static class ObservationHandlerWithCustomContextConfiguration { - - @Bean - ObservationHandlerWithCustomContext observationHandlerWithCustomContext(CalledHandlers calledHandlers) { - return new ObservationHandlerWithCustomContext(calledHandlers); - } - - } - - @Configuration(proxyBeanMethods = false) - static class TracerConfiguration { - - @Bean - Tracer tracer() { - return mock(Tracer.class); // simulating tracer configuration - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(CalledHandlersConfiguration.class) - static class CustomTracingObservationHandlers { - - @Bean - CustomTracingObservationHandler customTracingHandler1(CalledHandlers calledHandlers) { - return new CustomTracingObservationHandler("customTracingHandler1", calledHandlers); - } - - } - - @Configuration(proxyBeanMethods = false) - @Import(CalledHandlersConfiguration.class) - static class ObservationHandlersTracing { - - @Bean - @Order(6) - CustomTracingObservationHandler customTracingHandler2(CalledHandlers calledHandlers) { - return new CustomTracingObservationHandler("customTracingHandler2", calledHandlers); - } - - @Bean - @Order(5) - CustomTracingObservationHandler customTracingHandler1(CalledHandlers calledHandlers) { - return new CustomTracingObservationHandler("customTracingHandler1", calledHandlers); - } - - @Bean - @Order(4) - AllMatchingCompositeObservationHandler customAllMatchingCompositeObservationHandler() { - return new AllMatchingCompositeObservationHandler(); - } - - @Bean - @Order(3) - FirstMatchingCompositeObservationHandler customFirstMatchingCompositeObservationHandler() { - return new FirstMatchingCompositeObservationHandler(); - } - - @Bean - @Order(2) - ObservationHandler customObservationHandler(CalledHandlers calledHandlers) { - return new CustomObservationHandler(calledHandlers); - } - - @Bean - @Order(1) - MeterObservationHandler customMeterObservationHandler2(CalledHandlers calledHandlers) { - return new CustomMeterObservationHandler("customMeterObservationHandler2", calledHandlers); - } - - @Bean - @Order(0) - MeterObservationHandler customMeterObservationHandler1(CalledHandlers calledHandlers) { - return new CustomMeterObservationHandler("customMeterObservationHandler1", calledHandlers); - } - - } - - private static class CustomTracingObservationHandler implements TracingObservationHandler { - - private final Tracer tracer = mock(Tracer.class, Answers.RETURNS_MOCKS); - - private final String name; - - private final CalledHandlers calledHandlers; - - CustomTracingObservationHandler(String name, CalledHandlers calledHandlers) { - this.name = name; - this.calledHandlers = calledHandlers; - } - - String getName() { - return this.name; - } - - @Override - public Tracer getTracer() { - return this.tracer; - } - - @Override - public void onStart(Context context) { - this.calledHandlers.onCalled(this); - } - - @Override - public boolean supportsContext(Context context) { - return true; - } - - } - - private static class ObservationHandlerWithCustomContext implements ObservationHandler { - - private final CalledHandlers calledHandlers; - - ObservationHandlerWithCustomContext(CalledHandlers calledHandlers) { - this.calledHandlers = calledHandlers; - } - - @Override - public void onStart(CustomContext context) { - this.calledHandlers.onCalled(this); - } - - @Override - public boolean supportsContext(Context context) { - return context instanceof CustomContext; - } - - } - - private static final class CustomContext extends Context { - - } - - private static final class CalledHandlers { - - private final List> calledHandlers = new ArrayList<>(); - - void onCalled(ObservationHandler handler) { - this.calledHandlers.add(handler); - } - - List> getCalledHandlers() { - return this.calledHandlers; - } - - } - - @Configuration(proxyBeanMethods = false) - static class CalledHandlersConfiguration { - - @Bean - CalledHandlers calledHandlers() { - return new CalledHandlers(); - } - - } - - private static class CustomObservationHandler implements ObservationHandler { - - private final CalledHandlers calledHandlers; - - CustomObservationHandler(CalledHandlers calledHandlers) { - this.calledHandlers = calledHandlers; - } - - @Override - public void onStart(Context context) { - this.calledHandlers.onCalled(this); - } - - @Override - public boolean supportsContext(Context context) { - return true; - } - - } - - private static class CustomMeterObservationHandler implements MeterObservationHandler { - - private final CalledHandlers calledHandlers; - - private final String name; - - CustomMeterObservationHandler(String name, CalledHandlers calledHandlers) { - this.name = name; - this.calledHandlers = calledHandlers; - } - - String getName() { - return this.name; - } - - @Override - public void onStart(Context context) { - this.calledHandlers.onCalled(this); - } - - @Override - public boolean supportsContext(Context context) { - return true; - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 4cb67093fa9..4f46080b67f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -20,6 +20,8 @@ import java.util.List; import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.common.annotation.ValueResolver; +import io.micrometer.core.instrument.observation.MeterObservationHandler; +import io.micrometer.observation.ObservationHandler; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.annotation.DefaultNewSpanParser; import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; @@ -36,6 +38,7 @@ import org.aspectj.weaver.Advice; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -179,6 +182,31 @@ class MicrometerTracingAutoConfigurationTests { }); } + @Test + void shouldCreateTracingAndMeterObservationHandlerGroupWhenHasTracing() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ObservationHandlerGroup.class); + ObservationHandlerGroup group = context.getBean(ObservationHandlerGroup.class); + assertThat(group).isInstanceOf(TracingAndMeterObservationHandlerGroup.class); + assertThat(group.isMember(mock(ObservationHandler.class))).isFalse(); + assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue(); + assertThat(group.isMember(mock(MeterObservationHandler.class))).isTrue(); + }); + } + + @Test + void shouldCreateTracingObservationHandlerGroupWhenMetricsIsNotOnClassPath() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withClassLoader(new FilteredClassLoader("io.micrometer.core")) + .run((context) -> { + assertThat(context).hasSingleBean(ObservationHandlerGroup.class); + ObservationHandlerGroup group = context.getBean(ObservationHandlerGroup.class); + assertThat(group).isNotInstanceOf(TracingAndMeterObservationHandlerGroup.class); + assertThat(group.isMember(mock(ObservationHandler.class))).isFalse(); + assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue(); + }); + } + @Configuration(proxyBeanMethods = false) private static final class TracerConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroupTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroupTests.java new file mode 100644 index 00000000000..d30426e69e6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure-all/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingAndMeterObservationHandlerGroupTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-present 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.boot.actuate.autoconfigure.tracing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.micrometer.core.instrument.observation.MeterObservationHandler; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; +import io.micrometer.tracing.handler.TracingObservationHandler; +import org.assertj.core.extractor.Extractors; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link TracingAndMeterObservationHandlerGroup}. + * + * @author Phillip Webb + */ +class TracingAndMeterObservationHandlerGroupTests { + + @Test + void compareToSortsBeforeMeterObservationHandlerGroup() { + ObservationHandlerGroup meterGroup = ObservationHandlerGroup.of(MeterObservationHandler.class); + TracingAndMeterObservationHandlerGroup tracingAndMeterGroup = new TracingAndMeterObservationHandlerGroup( + mock(Tracer.class)); + assertThat(sort(meterGroup, tracingAndMeterGroup)).containsExactly(tracingAndMeterGroup, meterGroup); + assertThat(sort(tracingAndMeterGroup, meterGroup)).containsExactly(tracingAndMeterGroup, meterGroup); + } + + @Test + void isMemberAcceptsMeterObservationHandlerOrTracingObservationHandler() { + TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(mock(Tracer.class)); + assertThat(group.isMember(mock(ObservationHandler.class))).isFalse(); + assertThat(group.isMember(mock(MeterObservationHandler.class))).isTrue(); + assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue(); + } + + @Test + @SuppressWarnings("unchecked") + void registerMembersWrapsMeterObservationHandlersAndRegistersDistinctGroups() { + Tracer tracer = mock(Tracer.class); + TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(tracer); + TracingObservationHandler tracingHandler1 = mock(TracingObservationHandler.class); + TracingObservationHandler tracingHandler2 = mock(TracingObservationHandler.class); + MeterObservationHandler meterHandler1 = mock(MeterObservationHandler.class); + MeterObservationHandler meterHandler2 = mock(MeterObservationHandler.class); + ObservationConfig config = mock(ObservationConfig.class); + List> members = List.of(tracingHandler1, meterHandler1, tracingHandler2, meterHandler2); + group.registerMembers(config, members); + ArgumentCaptor> handlerCaptor = ArgumentCaptor.captor(); + then(config).should(times(2)).observationHandler(handlerCaptor.capture()); + List> actualComposites = handlerCaptor.getAllValues(); + assertThat(actualComposites).hasSize(2); + ObservationHandler tracingComposite = actualComposites.get(0); + assertThat(tracingComposite).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + List> tracingHandlers = (List>) Extractors.byName("handlers") + .apply(tracingComposite); + assertThat(tracingHandlers).containsExactly(tracingHandler1, tracingHandler2); + ObservationHandler metricsComposite = actualComposites.get(1); + assertThat(metricsComposite).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + List> metricsHandlers = (List>) Extractors.byName("handlers") + .apply(metricsComposite); + assertThat(metricsHandlers).hasSize(2); + assertThat(metricsHandlers).extracting("delegate").containsExactly(meterHandler1, meterHandler2); + } + + @Test + void registerMembersOnlyUsesCompositeWhenMoreThanOneHandler() { + Tracer tracer = mock(Tracer.class); + TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(tracer); + TracingObservationHandler tracingHandler1 = mock(TracingObservationHandler.class); + TracingObservationHandler tracingHandler2 = mock(TracingObservationHandler.class); + MeterObservationHandler meterHandler = mock(MeterObservationHandler.class); + ObservationConfig config = mock(ObservationConfig.class); + List> members = List.of(tracingHandler1, meterHandler, tracingHandler2); + group.registerMembers(config, members); + ArgumentCaptor> handlerCaptor = ArgumentCaptor.captor(); + then(config).should(times(2)).observationHandler(handlerCaptor.capture()); + List> actualComposites = handlerCaptor.getAllValues(); + assertThat(actualComposites).hasSize(2); + assertThat(actualComposites.get(0)).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + assertThat(actualComposites.get(1)).isInstanceOf(TracingAwareMeterObservationHandler.class); + } + + private List sort(ObservationHandlerGroup... groups) { + List list = new ArrayList<>(List.of(groups)); + Collections.sort(list); + return list; + } + +} diff --git a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfiguration.java index aa54795e53f..0c0fb5a5ff0 100644 --- a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfiguration.java @@ -22,7 +22,11 @@ import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler.IgnoredMeters; +import io.micrometer.core.instrument.observation.MeterObservationHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -30,6 +34,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; @@ -42,6 +47,7 @@ import org.springframework.core.annotation.Order; * @author Jon Schneider * @author Stephane Nicoll * @author Moritz Halbritter + * @author Phillip Webb * @since 4.0.0 */ @AutoConfiguration(before = CompositeMeterRegistryAutoConfiguration.class) @@ -75,6 +81,19 @@ public class MetricsAutoConfiguration { return new MeterRegistryCloser(meterRegistries.orderedStream().toList()); } + @Bean + ObservationHandlerGroup metricsObservationHandlerGroup() { + return ObservationHandlerGroup.of(MeterObservationHandler.class); + } + + @Bean + DefaultMeterObservationHandler defaultMeterObservationHandler(ObjectProvider meterRegistryProvider, + Clock clock, MetricsProperties properties) { + MeterRegistry meterRegistry = meterRegistryProvider.getIfAvailable(() -> new CompositeMeterRegistry(clock)); + return new DefaultMeterObservationHandler(meterRegistry, + properties.getObservations().getIgnoredMeters().toArray(IgnoredMeters[]::new)); + } + /** * Ensures that {@link MeterRegistry meter registries} are closed early in the * shutdown process. diff --git a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsProperties.java b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsProperties.java index 869d3808759..a1bf8dd816c 100644 --- a/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsProperties.java +++ b/spring-boot-project/spring-boot-metrics/src/main/java/org/springframework/boot/metrics/autoconfigure/MetricsProperties.java @@ -21,8 +21,12 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; + +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler.IgnoredMeters; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -63,6 +67,8 @@ public class MetricsProperties { private final Distribution distribution = new Distribution(); + private final Observations observations = new Observations(); + public boolean isUseGlobalRegistry() { return this.useGlobalRegistry; } @@ -91,6 +97,10 @@ public class MetricsProperties { return this.distribution; } + public Observations getObservations() { + return this.observations; + } + public static class Web { private final Client client = new Client(); @@ -257,4 +267,21 @@ public class MetricsProperties { } + public static class Observations { + + /** + * Meters that should be ignored when recoding observations. + */ + private Set ignoredMeters = new LinkedHashSet<>(); + + public Set getIgnoredMeters() { + return this.ignoredMeters; + } + + public void setIgnoredMeters(Set ignoredMeters) { + this.ignoredMeters = ignoredMeters; + } + + } + } diff --git a/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfigurationTests.java index 6e6fabd7bcd..22371b5476e 100644 --- a/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-metrics/src/test/java/org/springframework/boot/metrics/autoconfigure/MetricsAutoConfigurationTests.java @@ -22,11 +22,15 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.config.MeterFilterReply; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.observation.MeterObservationHandler; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationHandler; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.metrics.autoconfigure.MetricsAutoConfiguration.MeterRegistryCloser; +import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -42,6 +46,7 @@ import static org.mockito.Mockito.mock; * * @author Andy Wilkinson * @author Moritz Halbritter + * @author Phillip Webb */ class MetricsAutoConfigurationTests { @@ -89,6 +94,34 @@ class MetricsAutoConfigurationTests { }); } + @Test + void supplyHandlerAndGroup() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(ObservationHandlerGroup.class); + assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); + ObservationHandlerGroup group = context.getBean(ObservationHandlerGroup.class); + assertThat(group.isMember(mock(ObservationHandler.class))).isFalse(); + assertThat(group.isMember(mock(MeterObservationHandler.class))).isTrue(); + }); + } + + @Test + void shouldEnableLongTaskTimerByDefault() { + this.contextRunner.run((context) -> { + DefaultMeterObservationHandler handler = context.getBean(DefaultMeterObservationHandler.class); + assertThat(handler).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", true); + }); + } + + @Test + void shouldDisableLongTaskTimerIfPropertyIsSet() { + this.contextRunner.withPropertyValues("management.metrics.observations.ignored-meters=long-task-timer") + .run((context) -> { + DefaultMeterObservationHandler handler = context.getBean(DefaultMeterObservationHandler.class); + assertThat(handler).hasFieldOrPropertyWithValue("shouldCreateLongTaskTimer", false); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomClockConfiguration { diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfiguration.java index 92c4d17f4a4..92341398a9a 100644 --- a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfiguration.java @@ -55,10 +55,10 @@ public class ObservationAutoConfiguration { ObjectProvider observationPredicates, ObjectProvider> observationConventions, ObjectProvider> observationHandlers, - ObjectProvider observationHandlerGrouping, + ObjectProvider observationHandlerGroups, ObjectProvider observationFilters) { return new ObservationRegistryPostProcessor(observationRegistryCustomizers, observationPredicates, - observationConventions, observationHandlers, observationHandlerGrouping, observationFilters); + observationConventions, observationHandlers, observationHandlerGroups, observationFilters); } @Bean diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroup.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroup.java new file mode 100644 index 00000000000..fb318c0ca44 --- /dev/null +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroup.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-present 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.boot.observation.autoconfigure; + +import java.util.List; + +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; + +import org.springframework.util.Assert; + +/** + * Group of {@link ObservationHandler ObservationHandlers} that can be registered + * together. The first group claiming membership of a handler is responsible for + * registering it. Groups are {@link Comparable} so that they can be ordered against each + * other. + * + * @author Phillip Webb + * @since 4.0.0 + */ +public interface ObservationHandlerGroup extends Comparable { + + /** + * Return if the given handler is a member of this group. + * @param handler the handler to check + * @return if the handler is a member + */ + default boolean isMember(ObservationHandler handler) { + return handlerType().isInstance(handler); + } + + /** + * Register group members against the given {@link ObservationConfig}. + * @param config the config used to register members + * @param members the group members to register + */ + default void registerMembers(ObservationConfig config, List> members) { + config.observationHandler(new FirstMatchingCompositeObservationHandler(members)); + } + + @Override + default int compareTo(ObservationHandlerGroup other) { + return 0; + } + + /** + * Return the primary type of handler that this group accepts. + * @return the accepted handler type + */ + Class handlerType(); + + /** + * Static factory method to create a {@link ObservationHandlerGroup} with members of + * the given handler type. + * @param the handler type + * @param handlerType the handler type + * @return a new {@link ObservationHandlerGroup} + */ + static > ObservationHandlerGroup of(Class handlerType) { + Assert.notNull(handlerType, "'handlerType' must not be null"); + return () -> handlerType; + } + +} diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGrouping.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGrouping.java deleted file mode 100644 index a0d1d84d53f..00000000000 --- a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGrouping.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-present 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.boot.observation.autoconfigure; - -import java.util.ArrayList; -import java.util.List; - -import io.micrometer.observation.ObservationHandler; -import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; -import io.micrometer.observation.ObservationRegistry.ObservationConfig; - -import org.springframework.util.CollectionUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -/** - * Groups {@link ObservationHandler ObservationHandlers} by type. - * - * @author Andy Wilkinson - * @author Moritz Halbritter - * @since 4.0.0 - */ -@SuppressWarnings("rawtypes") -public class ObservationHandlerGrouping { - - private final List> categories; - - public ObservationHandlerGrouping(Class category) { - this(List.of(category)); - } - - public ObservationHandlerGrouping(List> categories) { - this.categories = categories; - } - - void apply(List> handlers, ObservationConfig config) { - MultiValueMap, ObservationHandler> groupings = new LinkedMultiValueMap<>(); - List> handlersWithoutCategory = new ArrayList<>(); - for (ObservationHandler handler : handlers) { - Class category = findCategory(handler); - if (category != null) { - groupings.add(category, handler); - } - else { - handlersWithoutCategory.add(handler); - } - } - for (Class category : this.categories) { - List> handlerGroup = groupings.get(category); - if (!CollectionUtils.isEmpty(handlerGroup)) { - config.observationHandler(new FirstMatchingCompositeObservationHandler(handlerGroup)); - } - } - for (ObservationHandler observationHandler : handlersWithoutCategory) { - config.observationHandler(observationHandler); - } - } - - private Class findCategory(ObservationHandler handler) { - for (Class category : this.categories) { - if (category.isInstance(handler)) { - return category; - } - } - return null; - } - -} diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroups.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroups.java new file mode 100644 index 00000000000..688dc705e88 --- /dev/null +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroups.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-present 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.boot.observation.autoconfigure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; + +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * A collection {@link ObservationHandlerGroup} instance and supporting registration + * logic. + * + * @author Andy Wilkinson + * @author Moritz Halbritter + * @author Phillip Webb + */ +class ObservationHandlerGroups { + + private final List groups; + + ObservationHandlerGroups(Collection groups) { + this.groups = Collections.unmodifiableList(sort(groups)); + } + + private static List sort(Collection groups) { + ArrayList sortedGroups = new ArrayList<>(groups); + Collections.sort(sortedGroups); + return sortedGroups; + } + + void register(ObservationConfig config, List> handlers) { + MultiValueMap> grouped = new LinkedMultiValueMap<>(); + for (ObservationHandler handler : handlers) { + grouped.add(findGroup(handler), handler); + } + for (ObservationHandlerGroup group : this.groups) { + List> members = grouped.get(group); + if (!CollectionUtils.isEmpty(members)) { + group.registerMembers(config, members); + } + } + List> unclaimed = grouped.get(null); + if (!CollectionUtils.isEmpty(unclaimed)) { + for (ObservationHandler handler : unclaimed) { + config.observationHandler(handler); + } + } + } + + private ObservationHandlerGroup findGroup(ObservationHandler handler) { + for (ObservationHandlerGroup group : this.groups) { + if (group.isMember(handler)) { + return group; + } + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryConfigurer.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryConfigurer.java index 29217b534e1..8e86bbfed12 100644 --- a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryConfigurer.java +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryConfigurer.java @@ -32,7 +32,7 @@ import org.springframework.boot.util.LambdaSafe; * {@link ObservationRegistry observation registries}. Installs * {@link ObservationPredicate observation predicates} and * {@link GlobalObservationConvention global observation conventions} into the - * {@link ObservationRegistry}. Also uses a {@link ObservationHandlerGrouping} to group + * {@link ObservationRegistry}. Also uses a {@link ObservationHandlerGroups} to group * handlers, which are then added to the {@link ObservationRegistry}. * * @author Moritz Halbritter @@ -47,7 +47,7 @@ class ObservationRegistryConfigurer { private final ObjectProvider> observationHandlers; - private final ObjectProvider observationHandlerGrouping; + private final ObjectProvider observationHandlerGroups; private final ObjectProvider observationFilters; @@ -55,13 +55,13 @@ class ObservationRegistryConfigurer { ObjectProvider observationPredicates, ObjectProvider> observationConventions, ObjectProvider> observationHandlers, - ObjectProvider observationHandlerGrouping, + ObjectProvider observationHandlerGroups, ObjectProvider observationFilters) { this.customizers = customizers; this.observationPredicates = observationPredicates; this.observationConventions = observationConventions; this.observationHandlers = observationHandlers; - this.observationHandlerGrouping = observationHandlerGrouping; + this.observationHandlerGroups = observationHandlerGroups; this.observationFilters = observationFilters; } @@ -74,14 +74,9 @@ class ObservationRegistryConfigurer { } private void registerHandlers(ObservationRegistry registry) { - ObservationHandlerGrouping grouping = this.observationHandlerGrouping.getIfAvailable(); - List> orderedHandlers = asOrderedList(this.observationHandlers); - if (grouping != null) { - grouping.apply(orderedHandlers, registry.observationConfig()); - } - else { - orderedHandlers.forEach((handler) -> registry.observationConfig().observationHandler(handler)); - } + ObservationHandlerGroups groups = new ObservationHandlerGroups(this.observationHandlerGroups.stream().toList()); + List> orderedHandlers = this.observationHandlers.orderedStream().toList(); + groups.register(registry.observationConfig(), orderedHandlers); } private void registerObservationPredicates(ObservationRegistry registry) { @@ -98,13 +93,9 @@ class ObservationRegistryConfigurer { @SuppressWarnings("unchecked") private void customize(ObservationRegistry registry) { - LambdaSafe.callbacks(ObservationRegistryCustomizer.class, asOrderedList(this.customizers), registry) + LambdaSafe.callbacks(ObservationRegistryCustomizer.class, this.customizers.orderedStream().toList(), registry) .withLogger(ObservationRegistryConfigurer.class) .invoke((customizer) -> customizer.customize(registry)); } - private List asOrderedList(ObjectProvider provider) { - return provider.orderedStream().toList(); - } - } diff --git a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryPostProcessor.java b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryPostProcessor.java index c5e92755c1b..4c10d172093 100644 --- a/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryPostProcessor.java +++ b/spring-boot-project/spring-boot-observation/src/main/java/org/springframework/boot/observation/autoconfigure/ObservationRegistryPostProcessor.java @@ -43,7 +43,7 @@ class ObservationRegistryPostProcessor implements BeanPostProcessor { private final ObjectProvider> observationHandlers; - private final ObjectProvider observationHandlerGrouping; + private final ObjectProvider observationHandlerGroups; private final ObjectProvider observationFilters; @@ -53,13 +53,13 @@ class ObservationRegistryPostProcessor implements BeanPostProcessor { ObjectProvider observationPredicates, ObjectProvider> observationConventions, ObjectProvider> observationHandlers, - ObjectProvider observationHandlerGrouping, + ObjectProvider observationHandlerGroups, ObjectProvider observationFilters) { this.observationRegistryCustomizers = observationRegistryCustomizers; this.observationPredicates = observationPredicates; this.observationConventions = observationConventions; this.observationHandlers = observationHandlers; - this.observationHandlerGrouping = observationHandlerGrouping; + this.observationHandlerGroups = observationHandlerGroups; this.observationFilters = observationFilters; } @@ -75,7 +75,7 @@ class ObservationRegistryPostProcessor implements BeanPostProcessor { if (this.configurer == null) { this.configurer = new ObservationRegistryConfigurer(this.observationRegistryCustomizers, this.observationPredicates, this.observationConventions, this.observationHandlers, - this.observationHandlerGrouping, this.observationFilters); + this.observationHandlerGroups, this.observationFilters); } return this.configurer; } diff --git a/spring-boot-project/spring-boot-observation/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-observation/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 358dc1cb771..a2ff31258e0 100644 --- a/spring-boot-project/spring-boot-observation/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-observation/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -6,6 +6,16 @@ "type": "java.lang.Boolean", "description": "Whether auto-configuration of Micrometer annotations is enabled.", "defaultValue": false + }, + { + "name": "management.observations.annotations.long-lask-timer.enabled", + "type": "java.lang.boolean", + "description": "Whether to create a LongTaskTimer for every observation.", + "defaultValue": true, + "deprecation": { + "level": "error", + "replacement": "management.metrics.observations.ignored-meters" + } } ] } diff --git a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfigurationTests.java index 4cdc983e604..f0c41888352 100644 --- a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationAutoConfigurationTests.java @@ -68,7 +68,6 @@ class ObservationAutoConfigurationTests { ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test-observation", observationRegistry).stop(); assertThat(context).hasSingleBean(ObservedAspect.class); - assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.class); }); } diff --git a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupTests.java b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupTests.java new file mode 100644 index 00000000000..dd11ebcddc9 --- /dev/null +++ b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-present 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.boot.observation.autoconfigure; + +import java.util.List; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ObservationHandlerGroup}. + * + * @author Phillip Webb + */ +class ObservationHandlerGroupTests { + + @Test + void isMemberWhenHandlerIsInstanceOfHandlerTypeReturnsTrue() { + ObservationHandlerGroup group = ObservationHandlerGroup.of(TestObservationHandler.class); + assertThat(group.isMember(mock(TestObservationHandler.class))).isTrue(); + assertThat(group.isMember(mock(TestObservationHandlerSubclass.class))).isTrue(); + } + + @Test + void isMemberWhenHandlerIsNotInstanceOfHandlerTypeReturnsFalse() { + ObservationHandlerGroup group = ObservationHandlerGroup.of(TestObservationHandler.class); + assertThat(group.isMember(mock(ObservationHandler.class))).isFalse(); + assertThat(group.isMember(mock(OtherObservationHandler.class))).isFalse(); + } + + @Test + void registerMembersRegistersUsingFirstMatchingCompositeObservationHandler() { + ObservationHandlerGroup group = ObservationHandlerGroup.of(TestObservationHandler.class); + TestObservationHandler handler1 = mock(TestObservationHandler.class); + TestObservationHandler handler2 = mock(TestObservationHandler.class); + ObservationConfig config = mock(ObservationConfig.class); + group.registerMembers(config, List.of(handler1, handler2)); + ArgumentCaptor> registeredHandler = ArgumentCaptor.captor(); + then(config).should().observationHandler(registeredHandler.capture()); + assertThat(registeredHandler.getValue()).isInstanceOf(FirstMatchingCompositeObservationHandler.class) + .extracting("handlers") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(handler1, handler2); + } + + @Test + void compareToReturnsZero() { + ObservationHandlerGroup group1 = ObservationHandlerGroup.of(TestObservationHandler.class); + ObservationHandlerGroup group2 = ObservationHandlerGroup.of(TestObservationHandler.class); + assertThat(group1.compareTo(group1)).isZero(); + assertThat(group1.compareTo(group2)).isZero(); + assertThat(group2.compareTo(group1)).isZero(); + } + + @Test + void ofCreatesHandlerGroup() { + ObservationHandlerGroup group = ObservationHandlerGroup.of(TestObservationHandler.class); + assertThat(group.handlerType()).isEqualTo(TestObservationHandler.class); + } + + interface TestObservationHandler extends ObservationHandler { + + } + + interface TestObservationHandlerSubclass extends TestObservationHandler { + + } + + interface OtherObservationHandler extends ObservationHandler { + + } + +} diff --git a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupingTests.java b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupsTests.java similarity index 86% rename from spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupingTests.java rename to spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupsTests.java index 3ab5109d95f..5df4682a1e1 100644 --- a/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupingTests.java +++ b/spring-boot-project/spring-boot-observation/src/test/java/org/springframework/boot/observation/autoconfigure/ObservationHandlerGroupsTests.java @@ -31,22 +31,25 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link ObservationHandlerGrouping}. + * Tests for {@link ObservationHandlerGroups}. * * @author Moritz Halbritter */ -class ObservationHandlerGroupingTests { +class ObservationHandlerGroupsTests { + + private static final ObservationHandlerGroup GROUP_A = ObservationHandlerGroup.of(ObservationHandlerA.class); + + private static final ObservationHandlerGroup GROUP_B = ObservationHandlerGroup.of(ObservationHandlerB.class); @Test void shouldGroupCategoriesIntoFirstMatchingHandlerAndRespectCategoryOrder() { - ObservationHandlerGrouping grouping = new ObservationHandlerGrouping( - List.of(ObservationHandlerA.class, ObservationHandlerB.class)); + ObservationHandlerGroups grouping = new ObservationHandlerGroups(List.of(GROUP_A, GROUP_B)); ObservationConfig config = new ObservationConfig(); ObservationHandlerA handlerA1 = new ObservationHandlerA("a1"); ObservationHandlerA handlerA2 = new ObservationHandlerA("a2"); ObservationHandlerB handlerB1 = new ObservationHandlerB("b1"); ObservationHandlerB handlerB2 = new ObservationHandlerB("b2"); - grouping.apply(List.of(handlerB1, handlerB2, handlerA1, handlerA2), config); + grouping.register(config, List.of(handlerB1, handlerB2, handlerA1, handlerA2)); List> handlers = getObservationHandlers(config); assertThat(handlers).hasSize(2); // Category A is first @@ -63,12 +66,12 @@ class ObservationHandlerGroupingTests { @Test void uncategorizedHandlersShouldBeOrderedAfterCategories() { - ObservationHandlerGrouping grouping = new ObservationHandlerGrouping(ObservationHandlerA.class); + ObservationHandlerGroups grouping = new ObservationHandlerGroups(List.of(GROUP_A)); ObservationConfig config = new ObservationConfig(); ObservationHandlerA handlerA1 = new ObservationHandlerA("a1"); ObservationHandlerA handlerA2 = new ObservationHandlerA("a2"); ObservationHandlerB handlerB1 = new ObservationHandlerB("b1"); - grouping.apply(List.of(handlerB1, handlerA1, handlerA2), config); + grouping.register(config, List.of(handlerB1, handlerA1, handlerA2)); List> handlers = getObservationHandlers(config); assertThat(handlers).hasSize(2); // Category A is first diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports index 50078b9540e..52873e13128 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports @@ -11,6 +11,3 @@ org.springframework.boot.metrics.autoconfigure.MetricsAutoConfiguration # Tracing org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfiguration org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration - -# Observability -org.springframework.boot.actuate.autoconfigure.observability.ObservabilityAutoConfiguration