diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index 0288371938..df97d94641 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -16,17 +16,23 @@ package org.springframework.boot.actuate.autoconfigure.observation; +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.observation.GlobalObservationConvention; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationPredicate; 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.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +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; @@ -42,9 +48,10 @@ import org.springframework.context.annotation.Configuration; * * @author Moritz Halbritter * @author Brian Clozel + * @author Jonatan Ivanov * @since 3.0.0 */ -@AutoConfiguration(after = CompositeMeterRegistryAutoConfiguration.class) +@AutoConfiguration(after = { CompositeMeterRegistryAutoConfiguration.class, MicrometerTracingAutoConfiguration.class }) @ConditionalOnClass(ObservationRegistry.class) @EnableConfigurationProperties(ObservationProperties.class) public class ObservationAutoConfiguration { @@ -67,35 +74,68 @@ public class ObservationAutoConfiguration { } @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(MeterRegistry.class) - static class MetricsConfiguration { - - @Bean - @ConditionalOnMissingBean(MeterObservationHandler.class) - DefaultMeterObservationHandler defaultMeterObservationHandler(MeterRegistry meterRegistry) { - return new DefaultMeterObservationHandler(meterRegistry); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass("io.micrometer.tracing.handler.TracingObservationHandler") + @ConditionalOnClass(MeterRegistry.class) + @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") static class OnlyMetricsConfiguration { @Bean - OnlyMetricsObservationHandlerGrouping onlyMetricsObservationHandlerGrouping() { - return new OnlyMetricsObservationHandlerGrouping(); + ObservationHandlerGrouping metricsObservationHandlerGrouping() { + return new ObservationHandlerGrouping(MeterObservationHandler.class); } } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(TracingObservationHandler.class) - static class TracingConfiguration { + @ConditionalOnClass(Tracer.class) + @ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry") + static class OnlyTracingConfiguration { @Bean - TracingObservationHandlerGrouping tracingObservationHandlerGrouping() { - return new TracingObservationHandlerGrouping(); + 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) + static class MeterObservationHandlerConfiguration { + + @ConditionalOnMissingBean(Tracer.class) + @Configuration(proxyBeanMethods = false) + static class OnlyMetricsMeterObservationHandlerConfiguration { + + @Bean + DefaultMeterObservationHandler defaultMeterObservationHandler(MeterRegistry meterRegistry) { + return new DefaultMeterObservationHandler(meterRegistry); + } + + } + + @ConditionalOnBean(Tracer.class) + @Configuration(proxyBeanMethods = false) + static class TracingAndMetricsObservationHandlerConfiguration { + + @Bean + TracingAwareMeterObservationHandler tracingAwareMeterObservationHandler( + MeterRegistry meterRegistry, Tracer tracer) { + return new TracingAwareMeterObservationHandler<>(new DefaultMeterObservationHandler(meterRegistry), + tracer); + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java index e5713ec304..fcdbd5fb2f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java @@ -16,24 +16,61 @@ package org.springframework.boot.actuate.autoconfigure.observation; -import java.util.Collection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; import io.micrometer.observation.ObservationRegistry.ObservationConfig; -/** - * Strategy to apply {@link ObservationHandler ObservationHandlers} to an - * {@link ObservationConfig}. - * - * @author Moritz Halbritter - */ -interface ObservationHandlerGrouping { +import org.springframework.util.CollectionUtils; - /** - * Applies the given list of {@code handlers} to the given {@code config}. - * @param handlers the list of observation handlers - * @param config the config to apply the handlers to - */ - void apply(Collection> handlers, ObservationConfig config); +/** + * Groups {@link ObservationHandler ObservationHandlers} by type. + * + * @author Andy Wilkinson + */ +@SuppressWarnings("rawtypes") +class ObservationHandlerGrouping { + + private final List> categories; + + ObservationHandlerGrouping(Class category) { + this(List.of(category)); + } + + ObservationHandlerGrouping(List> categories) { + this.categories = categories; + } + + void apply(List> handlers, ObservationConfig config) { + Map, List>> groupings = new HashMap<>(); + for (ObservationHandler handler : handlers) { + Class category = findCategory(handler); + if (category != null) { + groupings.computeIfAbsent(category, (c) -> new ArrayList<>()).add(handler); + } + else { + config.observationHandler(handler); + } + } + for (Class category : this.categories) { + List> handlerGroup = groupings.get(category); + if (!CollectionUtils.isEmpty(handlerGroup)) { + config.observationHandler(new FirstMatchingCompositeObservationHandler(handlerGroup)); + } + } + } + + 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-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurer.java index c821e9ed30..6c5bbba97c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurer.java @@ -68,8 +68,8 @@ class ObservationRegistryConfigurer { } private void registerHandlers(ObservationRegistry registry) { - this.observationHandlerGrouping.getObject().apply(asOrderedList(this.observationHandlers), - registry.observationConfig()); + this.observationHandlerGrouping.ifAvailable( + (grouping) -> grouping.apply(asOrderedList(this.observationHandlers), registry.observationConfig())); } private void registerObservationPredicates(ObservationRegistry registry) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/OnlyMetricsObservationHandlerGrouping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/OnlyMetricsObservationHandlerGrouping.java deleted file mode 100644 index 32643d521e..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/OnlyMetricsObservationHandlerGrouping.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2022 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.observation; - -import java.util.ArrayList; -import java.util.Collection; -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; - -/** - * {@link ObservationHandlerGrouping} used by {@link ObservationAutoConfiguration} if - * micrometer-tracing is not on the classpath. - * - * Groups all {@link MeterObservationHandler} into a - * {@link FirstMatchingCompositeObservationHandler}. All other handlers are added to the - * {@link ObservationConfig} directly. - * - * @author Moritz Halbritter - */ -class OnlyMetricsObservationHandlerGrouping implements ObservationHandlerGrouping { - - @Override - public void apply(Collection> handlers, ObservationConfig config) { - List> meterObservationHandlers = new ArrayList<>(); - for (ObservationHandler handler : handlers) { - if (handler instanceof MeterObservationHandler) { - meterObservationHandlers.add(handler); - } - else { - config.observationHandler(handler); - } - } - if (!meterObservationHandlers.isEmpty()) { - config.observationHandler(new FirstMatchingCompositeObservationHandler(meterObservationHandlers)); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TracingObservationHandlerGrouping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TracingObservationHandlerGrouping.java deleted file mode 100644 index a26553208f..0000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TracingObservationHandlerGrouping.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-2022 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.observation; - -import java.util.ArrayList; -import java.util.Collection; -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.handler.TracingObservationHandler; - -/** - * {@link ObservationHandlerGrouping} used by {@link ObservationAutoConfiguration} if - * micrometer-tracing is on the classpath. - * - * Groups all {@link TracingObservationHandler} into a - * {@link FirstMatchingCompositeObservationHandler}, and {@link MeterObservationHandler} - * into a {@link FirstMatchingCompositeObservationHandler}. All other handlers are added - * to the {@link ObservationConfig} directly. - * - * @author Moritz Halbritter - */ -class TracingObservationHandlerGrouping implements ObservationHandlerGrouping { - - @Override - public void apply(Collection> handlers, ObservationConfig config) { - List> meterObservationHandlers = new ArrayList<>(); - List> tracingObservationHandlers = new ArrayList<>(); - for (ObservationHandler handler : handlers) { - if (handler instanceof TracingObservationHandler) { - tracingObservationHandlers.add(handler); - } - else if (handler instanceof MeterObservationHandler) { - meterObservationHandlers.add(handler); - } - else { - config.observationHandler(handler); - } - } - if (!tracingObservationHandlers.isEmpty()) { - config.observationHandler(new FirstMatchingCompositeObservationHandler(tracingObservationHandlers)); - } - if (!meterObservationHandlers.isEmpty()) { - config.observationHandler(new FirstMatchingCompositeObservationHandler(meterObservationHandlers)); - } - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 4f074e919f..13a724ac90 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -34,6 +34,7 @@ import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObserv import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; 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; @@ -55,6 +56,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link ObservationAutoConfiguration}. * * @author Moritz Halbritter + * @author Jonatan Ivanov */ class ObservationAutoConfigurationTests { @@ -63,7 +65,80 @@ class ObservationAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); private final ApplicationContextRunner tracingContextRunner = new ApplicationContextRunner() - .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); + .with(MetricsRun.simple()).withUserConfiguration(TracerConfiguration.class) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); + + @Test + void beansShouldNotBeSuppliedWhenMicrometerObservationIsNotOnClassPath() { + this.tracingContextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.observation")) + .run((context) -> { + assertThat(context).hasSingleBean(MeterRegistry.class); + assertThat(context).doesNotHaveBean(ObservationRegistry.class); + assertThat(context).doesNotHaveBean(ObservationHandler.class); + assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.class); + }); + } + + @Test + void supplyObservationRegistryWhenMicrometerCoreAndTracingAreNotOnClassPath() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.core", "io.micrometer.tracing")) + .run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test-observation", observationRegistry).stop(); + assertThat(context).doesNotHaveBean(ObservationHandler.class); + assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.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(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(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(TracingAwareMeterObservationHandler.class); + assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); + assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); + }); + } + + @Test + void supplyMeterHandlerAndGroupingWhenMicrometerCoreAndTracingAreOnClassPathButThereIsNoTracer() { + new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.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(ObservationHandlerGrouping.class); + assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); + }); + } @Test void autoConfiguresDefaultMeterObservationHandler() { @@ -75,9 +150,17 @@ class ObservationAutoConfigurationTests { // Observation leads to a timer MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); assertThat(meterRegistry.get("test-observation").timer().count()).isEqualTo(1); + assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); + assertThat(context).hasSingleBean(ObservationHandler.class); }); } + @Test + void allowsDefaultMeterObservationHandlerToBeDisabled() { + this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ObservationHandler.class)); + } + @Test void autoConfiguresObservationPredicates() { this.contextRunner.withUserConfiguration(ObservationPredicates.class).run((context) -> { @@ -119,6 +202,8 @@ class ObservationAutoConfigurationTests { assertThat(handlers.get(1)).isInstanceOf(CustomMeterObservationHandler.class); assertThat(((CustomMeterObservationHandler) handlers.get(1)).getName()) .isEqualTo("customMeterObservationHandler1"); + assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); + assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); }); } @@ -132,9 +217,26 @@ class ObservationAutoConfigurationTests { 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) -> { @@ -156,6 +258,8 @@ class ObservationAutoConfigurationTests { assertThat(handlers.get(2)).isInstanceOf(CustomMeterObservationHandler.class); assertThat(((CustomMeterObservationHandler) handlers.get(2)).getName()) .isEqualTo("customMeterObservationHandler1"); + assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); + assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); }); } @@ -236,6 +340,27 @@ class ObservationAutoConfigurationTests { } + @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 {