diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 70aadb3991a7..04d9da39e855 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -47,7 +47,9 @@ dependencies { optional("com.zaxxer:HikariCP") optional("io.dropwizard.metrics:metrics-jmx") optional("io.lettuce:lettuce-core") + optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-core") + optional("io.micrometer:micrometer-tracing-api") optional("io.micrometer:micrometer-binders") optional("io.micrometer:micrometer-registry-appoptics") optional("io.micrometer:micrometer-registry-atlas") { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/NoTracingObservationHandlerGrouping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/NoTracingObservationHandlerGrouping.java new file mode 100644 index 000000000000..c190ed217da3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/NoTracingObservationHandlerGrouping.java @@ -0,0 +1,64 @@ +/* + * 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 NoTracingObservationHandlerGrouping 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(castToRawType(meterObservationHandlers))); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List castToRawType(List> handlers) { + // See https://github.com/micrometer-metrics/micrometer/issues/3064 + return (List) handlers; + } + +} 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 new file mode 100644 index 000000000000..3e71e41d675e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -0,0 +1,98 @@ +/* + * 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 io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.Observation.GlobalTagsProvider; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationPredicate; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.handler.TracingObservationHandler; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Observation API. + * + * @author Moritz Halbritter + * @since 3.0.0 + */ +@AutoConfiguration(after = CompositeMeterRegistryAutoConfiguration.class) +@ConditionalOnClass(ObservationRegistry.class) +public class ObservationAutoConfiguration { + + @Bean + static ObservationRegistryPostProcessor observationRegistryPostProcessor( + ObjectProvider> observationRegistryCustomizers, + ObjectProvider observationPredicates, + ObjectProvider> tagProviders, + ObjectProvider> observationHandlers, + ObjectProvider observationHandlerGrouping) { + return new ObservationRegistryPostProcessor(observationRegistryCustomizers, observationPredicates, tagProviders, + observationHandlers, observationHandlerGrouping); + } + + @Bean + @ConditionalOnMissingBean + ObservationRegistry observationRegistry() { + return ObservationRegistry.create(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(MeterRegistry.class) + static class MetricsConfiguration { + + @Bean + TimerObservationHandlerObservationRegistryCustomizer enableTimerObservationHandler( + MeterRegistry meterRegistry) { + return new TimerObservationHandlerObservationRegistryCustomizer(meterRegistry); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("io.micrometer.tracing.handler.TracingObservationHandler") + static class NoTracingConfiguration { + + @Bean + ObservationHandlerGrouping noTracingObservationHandlerGrouping() { + return new NoTracingObservationHandlerGrouping(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(TracingObservationHandler.class) + static class TracingConfiguration { + + @Bean + ObservationHandlerGrouping tracingObservationHandlerGrouping() { + return new TracingObservationHandlerGrouping(); + } + + } + +} 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 new file mode 100644 index 000000000000..e5713ec304f1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java @@ -0,0 +1,39 @@ +/* + * 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.Collection; + +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; + +/** + * Strategy to apply {@link ObservationHandler ObservationHandlers} to an + * {@link ObservationConfig}. + * + * @author Moritz Halbritter + */ +interface ObservationHandlerGrouping { + + /** + * 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); + +} 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 new file mode 100644 index 000000000000..e4f0c88d35f6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurer.java @@ -0,0 +1,95 @@ +/* + * 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.List; + +import io.micrometer.observation.Observation.GlobalTagsProvider; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationPredicate; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.util.LambdaSafe; + +/** + * Configurer to apply {@link ObservationRegistryCustomizer customizers} to + * {@link ObservationRegistry observation registries}. Installs + * {@link ObservationPredicate observation predicates} and {@link GlobalTagsProvider + * global tag providers} into the {@link ObservationRegistry}. Also uses a + * {@link ObservationHandlerGrouping} to group handlers, which are then added to the + * {@link ObservationRegistry}. + * + * @author Moritz Halbritter + */ +class ObservationRegistryConfigurer { + + private final ObjectProvider> customizers; + + private final ObjectProvider observationPredicates; + + private final ObjectProvider> tagProviders; + + private final ObjectProvider> observationHandlers; + + private final ObjectProvider observationHandlerGrouping; + + ObservationRegistryConfigurer(ObjectProvider> customizers, + ObjectProvider observationPredicates, + ObjectProvider> tagProviders, + ObjectProvider> observationHandlers, + ObjectProvider observationHandlerGrouping) { + this.customizers = customizers; + this.observationPredicates = observationPredicates; + this.tagProviders = tagProviders; + this.observationHandlers = observationHandlers; + this.observationHandlerGrouping = observationHandlerGrouping; + } + + void configure(ObservationRegistry registry) { + registerObservationPredicates(registry); + registerGlobalTagsProvider(registry); + registerHandlers(registry); + customize(registry); + } + + private void registerHandlers(ObservationRegistry registry) { + this.observationHandlerGrouping.getObject().apply(asOrderedList(this.observationHandlers), + registry.observationConfig()); + } + + private void registerObservationPredicates(ObservationRegistry registry) { + this.observationPredicates.orderedStream().forEach( + (observationPredicate) -> registry.observationConfig().observationPredicate(observationPredicate)); + } + + private void registerGlobalTagsProvider(ObservationRegistry registry) { + this.tagProviders.orderedStream() + .forEach((tagProvider) -> registry.observationConfig().tagsProvider(tagProvider)); + } + + @SuppressWarnings("unchecked") + private void customize(ObservationRegistry registry) { + LambdaSafe.callbacks(ObservationRegistryCustomizer.class, asOrderedList(this.customizers), 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-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryCustomizer.java new file mode 100644 index 000000000000..cff8cccbcdb3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryCustomizer.java @@ -0,0 +1,38 @@ +/* + * 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 io.micrometer.observation.ObservationRegistry; + +/** + * Callback interface that can be used to customize auto-configured + * {@link ObservationRegistry observation registries}. + * + * @param the registry type to customize + * @author Moritz Halbritter + * @since 3.0.0 + */ +@FunctionalInterface +public interface ObservationRegistryCustomizer { + + /** + * Customize the given {@code registry}. + * @param registry the registry to customize + */ + void customize(T registry); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryPostProcessor.java new file mode 100644 index 000000000000..c3404c1fd8f1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryPostProcessor.java @@ -0,0 +1,78 @@ +/* + * 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 io.micrometer.observation.Observation.GlobalTagsProvider; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationPredicate; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * {@link BeanPostProcessor} that delegates to a lazily created + * {@link ObservationRegistryConfigurer} to post-process {@link ObservationRegistry} + * beans. + * + * @author Moritz Halbritter + */ +class ObservationRegistryPostProcessor implements BeanPostProcessor { + + private final ObjectProvider> observationRegistryCustomizers; + + private final ObjectProvider observationPredicates; + + private final ObjectProvider> tagProviders; + + private final ObjectProvider> observationHandlers; + + private final ObjectProvider observationHandlerGrouping; + + private volatile ObservationRegistryConfigurer configurer; + + ObservationRegistryPostProcessor(ObjectProvider> observationRegistryCustomizers, + ObjectProvider observationPredicates, + ObjectProvider> tagProviders, + ObjectProvider> observationHandlers, + ObjectProvider observationHandlerGrouping) { + this.observationRegistryCustomizers = observationRegistryCustomizers; + this.observationPredicates = observationPredicates; + this.tagProviders = tagProviders; + this.observationHandlers = observationHandlers; + this.observationHandlerGrouping = observationHandlerGrouping; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ObservationRegistry) { + getConfigurer().configure((ObservationRegistry) bean); + } + return bean; + } + + private ObservationRegistryConfigurer getConfigurer() { + if (this.configurer == null) { + this.configurer = new ObservationRegistryConfigurer(this.observationRegistryCustomizers, + this.observationPredicates, this.tagProviders, this.observationHandlers, + this.observationHandlerGrouping); + } + return this.configurer; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizer.java new file mode 100644 index 000000000000..bcc03ec94391 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizer.java @@ -0,0 +1,42 @@ +/* + * 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 io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.TimerObservationHandler; +import io.micrometer.observation.ObservationRegistry; + +/** + * Registers the {@link TimerObservationHandler} with a {@link ObservationRegistry}. + * + * @author Moritz Halbritter + */ +class TimerObservationHandlerObservationRegistryCustomizer + implements ObservationRegistryCustomizer { + + private final MeterRegistry meterRegistry; + + TimerObservationHandlerObservationRegistryCustomizer(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public void customize(ObservationRegistry registry) { + registry.observationConfig().observationHandler(new TimerObservationHandler(this.meterRegistry)); + } + +} 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 new file mode 100644 index 000000000000..950ca16e909f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/TracingObservationHandlerGrouping.java @@ -0,0 +1,75 @@ +/* + * 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 MeterObservationHandler} into a + * {@link FirstMatchingCompositeObservationHandler}, and all + * {@link TracingObservationHandler} 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 MeterObservationHandler) { + meterObservationHandlers.add(handler); + } + else if (handler instanceof TracingObservationHandler) { + tracingObservationHandlers.add(handler); + } + else { + config.observationHandler(handler); + } + } + + if (!meterObservationHandlers.isEmpty()) { + config.observationHandler( + new FirstMatchingCompositeObservationHandler(castToRawType(meterObservationHandlers))); + } + if (!tracingObservationHandlers.isEmpty()) { + config.observationHandler( + new FirstMatchingCompositeObservationHandler(castToRawType(tracingObservationHandlers))); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List castToRawType(List> handlers) { + // See https://github.com/micrometer-metrics/micrometer/issues/3064 + return (List) handlers; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/package-info.java new file mode 100644 index 000000000000..e1a8cc58b5ae --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Auto-configuration for the Micrometer Observation API. + */ +package org.springframework.boot.actuate.autoconfigure.observation; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6d131bcb9c99..0ba704f63db8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -81,6 +81,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsA org.springframework.boot.actuate.autoconfigure.mongo.MongoHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.mongo.MongoReactiveHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration +org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.redis.RedisHealthContributorAutoConfiguration @@ -97,4 +98,4 @@ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoC org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration -org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration \ No newline at end of file +org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration 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 new file mode 100644 index 000000000000..fdd38c979636 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -0,0 +1,372 @@ +/* + * 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.List; + +import io.micrometer.common.Tag; +import io.micrometer.common.Tags; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.MeterObservationHandler; +import io.micrometer.core.instrument.search.MeterNotFoundException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.Observation.GlobalTagsProvider; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.AllMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationPredicate; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.TracingObservationHandler; +import org.junit.jupiter.api.Test; +import org.mockito.Answers; +import org.mockito.Mockito; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link ObservationAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class ObservationAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withClassLoader(new FilteredClassLoader("io.micrometer.tracing")) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); + + private final ApplicationContextRunner tracingContextRunner = new ApplicationContextRunner() + .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); + + @Test + void autoConfiguresTimerObservationHandler() { + this.contextRunner.run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("test-observation", observationRegistry).stop(); + // When a TimerObservationHandler is registered, every stopped Observation + // leads to a timer + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.get("test-observation").timer().count()).isEqualTo(1); + }); + } + + @Test + void allowsTimerObservationHandlerToBeDisabled() { + this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(TimerObservationHandlerObservationRegistryCustomizer.class)); + } + + @Test + void autoConfiguresObservationPredicates() { + this.contextRunner.withUserConfiguration(ObservationPredicates.class).run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + // This is allowed by ObservationPredicates.customPredicate + Observation.start("observation1", observationRegistry).start().stop(); + // This isn't allowed by ObservationPredicates.customPredicate + Observation.start("observation2", observationRegistry).start().stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.get("observation1").timer().count()).isEqualTo(1); + assertThatThrownBy(() -> meterRegistry.get("observation2").timer()) + .isInstanceOf(MeterNotFoundException.class); + }); + } + + @Test + void autoConfiguresGlobalTagsProvider() { + this.contextRunner.withUserConfiguration(GlobalTagsProviders.class).run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Context micrometerContext = new Context(); + Observation.start("test-observation", micrometerContext, observationRegistry).stop(); + assertThat(micrometerContext.getAllTags()).containsExactly(Tag.of("tag1", "value1")); + }); + } + + @Test + void autoConfiguresObservationHandler() { + 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); + assertThat(handlers).hasSize(2); + // Regular handlers are registered first + assertThat(handlers.get(0)).isInstanceOf(CustomObservationHandler.class); + // 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"); + }); + } + + @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); + assertThat(handlers).hasSize(3); + // Regular handlers are registered first + assertThat(handlers.get(0)).isInstanceOf(CustomObservationHandler.class); + // 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"); + // Multiple TracingObservationHandler are wrapped in + // FirstMatchingCompositeObservationHandler, which calls only the first + // one + assertThat(handlers.get(2)).isInstanceOf(CustomTracingObservationHandler.class); + assertThat(((CustomTracingObservationHandler) handlers.get(2)).getName()) + .isEqualTo("customTracingHandler1"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ObservationPredicates { + + @Bean + ObservationPredicate customPredicate() { + return (s, context) -> s.equals("observation1"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GlobalTagsProviders { + + @Bean + GlobalTagsProvider customTagsProvider() { + return new GlobalTagsProvider<>() { + @Override + public boolean supportsContext(Context context) { + return true; + } + + @Override + public Tags getLowCardinalityTags(Context context) { + return Tags.of("tag1", "value1"); + } + }; + } + + } + + @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 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 = Mockito.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 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/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurerIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurerIntegrationTests.java new file mode 100644 index 000000000000..e9abbafae5c9 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationRegistryConfigurerIntegrationTests.java @@ -0,0 +1,121 @@ +/* + * 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.List; + +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ObservationRegistryConfigurer} and + * {@link ObservationRegistryPostProcessor}. + * + * @author Moritz Halbritter + */ +class ObservationRegistryConfigurerIntegrationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)); + + @Test + void customizersAreCalledInOrder() { + this.contextRunner.withUserConfiguration(Customizers.class).run((context) -> { + CalledCustomizers calledCustomizers = context.getBean(CalledCustomizers.class); + Customizer1 customizer1 = context.getBean(Customizer1.class); + Customizer2 customizer2 = context.getBean(Customizer2.class); + + assertThat(calledCustomizers.getCustomizers()).containsExactly(customizer1, customizer2); + }); + } + + @Configuration(proxyBeanMethods = false) + static class Customizers { + + @Bean + CalledCustomizers calledCustomizers() { + return new CalledCustomizers(); + } + + @Bean + @Order(1) + Customizer1 customizer1(CalledCustomizers calledCustomizers) { + return new Customizer1(calledCustomizers); + } + + @Bean + @Order(2) + Customizer2 customizer2(CalledCustomizers calledCustomizers) { + return new Customizer2(calledCustomizers); + } + + } + + private static class CalledCustomizers { + + private final List> customizers = new ArrayList<>(); + + void onCalled(ObservationRegistryCustomizer customizer) { + this.customizers.add(customizer); + } + + List> getCustomizers() { + return this.customizers; + } + + } + + private static class Customizer1 implements ObservationRegistryCustomizer { + + private final CalledCustomizers calledCustomizers; + + Customizer1(CalledCustomizers calledCustomizers) { + this.calledCustomizers = calledCustomizers; + } + + @Override + public void customize(ObservationRegistry registry) { + this.calledCustomizers.onCalled(this); + } + + } + + private static class Customizer2 implements ObservationRegistryCustomizer { + + private final CalledCustomizers calledCustomizers; + + Customizer2(CalledCustomizers calledCustomizers) { + this.calledCustomizers = calledCustomizers; + } + + @Override + public void customize(ObservationRegistry registry) { + this.calledCustomizers.onCalled(this); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizerTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizerTests.java new file mode 100644 index 000000000000..7180a55e036d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/TimerObservationHandlerObservationRegistryCustomizerTests.java @@ -0,0 +1,45 @@ +/* + * 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 io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TimerObservationHandlerObservationRegistryCustomizer}. + * + * @author Moritz Halbritter + */ +class TimerObservationHandlerObservationRegistryCustomizerTests { + + @Test + void customizeInstallsTimerObservationHandler() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + TimerObservationHandlerObservationRegistryCustomizer sut = new TimerObservationHandlerObservationRegistryCustomizer( + meterRegistry); + ObservationRegistry observationRegistry = ObservationRegistry.create(); + sut.customize(observationRegistry); + Observation.start("test-1", observationRegistry).stop(); + assertThat(meterRegistry.find("test-1").timer().count()).isEqualTo(1); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 575c38e1d2d3..15af104f6a46 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -22,8 +22,10 @@ dependencies { optional("com.sun.mail:jakarta.mail") optional("com.zaxxer:HikariCP") optional("io.lettuce:lettuce-core") + optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-core") optional("io.micrometer:micrometer-binders") + optional("io.micrometer:micrometer-tracing-api") optional("io.micrometer:micrometer-registry-prometheus") optional("io.prometheus:simpleclient_pushgateway") { exclude(group: "javax.xml.bind", module: "jaxb-api") diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 102cf0795729..712a02dab846 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -976,7 +976,7 @@ bom { ] } } - library("Micrometer", "2.0.0-M3") { + library("Micrometer", "2.0.0-SNAPSHOT") { group("io.micrometer") { modules = [ "micrometer-registry-stackdriver" { @@ -988,6 +988,13 @@ bom { ] } } + library("Micrometer Tracing", "1.0.0-SNAPSHOT") { + group("io.micrometer") { + modules = [ + "micrometer-tracing-api" + ] + } + } library("MIMEPull", "1.9.15") { group("org.jvnet.mimepull") { modules = [ @@ -1342,7 +1349,7 @@ bom { ] } } - library("Spring Batch", "5.0.0-M2") { + library("Spring Batch", "5.0.0-SNAPSHOT") { group("org.springframework.batch") { modules = [ "spring-batch-core", diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle index fc3ad6256402..74a593dd2906 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle @@ -7,6 +7,7 @@ description = "Starter for using Spring Boot's Actuator which provides productio dependencies { api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) api(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) + api("io.micrometer:micrometer-observation") api("io.micrometer:micrometer-core") api("io.micrometer:micrometer-binders") }