Add auto-configuration for Micrometer 2.0.0 Observation API

- Adds a ObservationRegistry bean
- Add support for ObservationRegistryCustomizers
- Enables timer creation for observations if micrometer-core is on
  the classpath
- Registers ObservationPredicate, GlobalTagsProvider and
  ObservationHandler on the MeterRegistry
- Applies grouping to the ObservationHandlers: MeterObservationHandler
  are added to a FirstMatchingCompositeObservationHandler
- If micrometer-tracing is on the classpath, the
  TracingObservationHandler are added to a
  FirstMatchingCompositeObservationHandler

Closes gh-29666
This commit is contained in:
Moritz Halbritter 2022-03-10 14:59:05 +01:00 committed by Moritz Halbritter
parent 4a8901b94c
commit 5ab9112bbc
17 changed files with 1102 additions and 2 deletions

View File

@ -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") {

View File

@ -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<ObservationRegistryCustomizer<?>> observationRegistryCustomizers,
ObjectProvider<ObservationPredicate> observationPredicates,
ObjectProvider<GlobalTagsProvider<?>> tagProviders,
ObjectProvider<ObservationHandler<?>> observationHandlers,
ObjectProvider<ObservationHandlerGrouping> 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 OnlyMetricsObservationHandlerGrouping();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(TracingObservationHandler.class)
static class TracingConfiguration {
@Bean
ObservationHandlerGrouping tracingObservationHandlerGrouping() {
return new TracingObservationHandlerGrouping();
}
}
}

View File

@ -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<ObservationHandler<?>> handlers, ObservationConfig config);
}

View File

@ -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<ObservationRegistryCustomizer<?>> customizers;
private final ObjectProvider<ObservationPredicate> observationPredicates;
private final ObjectProvider<GlobalTagsProvider<?>> tagProviders;
private final ObjectProvider<ObservationHandler<?>> observationHandlers;
private final ObjectProvider<ObservationHandlerGrouping> observationHandlerGrouping;
ObservationRegistryConfigurer(ObjectProvider<ObservationRegistryCustomizer<?>> customizers,
ObjectProvider<ObservationPredicate> observationPredicates,
ObjectProvider<GlobalTagsProvider<?>> tagProviders,
ObjectProvider<ObservationHandler<?>> observationHandlers,
ObjectProvider<ObservationHandlerGrouping> 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 <T> List<T> asOrderedList(ObjectProvider<T> provider) {
return provider.orderedStream().toList();
}
}

View File

@ -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 <T> the registry type to customize
* @author Moritz Halbritter
* @since 3.0.0
*/
@FunctionalInterface
public interface ObservationRegistryCustomizer<T extends ObservationRegistry> {
/**
* Customize the given {@code registry}.
* @param registry the registry to customize
*/
void customize(T registry);
}

View File

@ -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<ObservationRegistryCustomizer<?>> observationRegistryCustomizers;
private final ObjectProvider<ObservationPredicate> observationPredicates;
private final ObjectProvider<GlobalTagsProvider<?>> tagProviders;
private final ObjectProvider<ObservationHandler<?>> observationHandlers;
private final ObjectProvider<ObservationHandlerGrouping> observationHandlerGrouping;
private volatile ObservationRegistryConfigurer configurer;
ObservationRegistryPostProcessor(ObjectProvider<ObservationRegistryCustomizer<?>> observationRegistryCustomizers,
ObjectProvider<ObservationPredicate> observationPredicates,
ObjectProvider<GlobalTagsProvider<?>> tagProviders,
ObjectProvider<ObservationHandler<?>> observationHandlers,
ObjectProvider<ObservationHandlerGrouping> 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;
}
}

View File

@ -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 OnlyMetricsObservationHandlerGrouping implements ObservationHandlerGrouping {
@Override
public void apply(Collection<ObservationHandler<?>> handlers, ObservationConfig config) {
List<ObservationHandler<?>> 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<ObservationHandler> castToRawType(List<ObservationHandler<?>> handlers) {
// See https://github.com/micrometer-metrics/micrometer/issues/3064
return (List) handlers;
}
}

View File

@ -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<ObservationRegistry> {
private final MeterRegistry meterRegistry;
TimerObservationHandlerObservationRegistryCustomizer(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public void customize(ObservationRegistry registry) {
registry.observationConfig().observationHandler(new TimerObservationHandler(this.meterRegistry));
}
}

View File

@ -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<ObservationHandler<?>> handlers, ObservationConfig config) {
List<ObservationHandler<?>> meterObservationHandlers = new ArrayList<>();
List<ObservationHandler<?>> 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<ObservationHandler> castToRawType(List<ObservationHandler<?>> handlers) {
// See https://github.com/micrometer-metrics/micrometer/issues/3064
return (List) handlers;
}
}

View File

@ -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;

View File

@ -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
org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration

View File

@ -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<ObservationHandler<?>> 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<ObservationHandler<?>> 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<Context> customObservationHandler(CalledHandlers calledHandlers) {
return new CustomObservationHandler(calledHandlers);
}
@Bean
@Order(1)
MeterObservationHandler<Context> customMeterObservationHandler2(CalledHandlers calledHandlers) {
return new CustomMeterObservationHandler("customMeterObservationHandler2", calledHandlers);
}
@Bean
@Order(0)
MeterObservationHandler<Context> 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<Context> customObservationHandler(CalledHandlers calledHandlers) {
return new CustomObservationHandler(calledHandlers);
}
@Bean
@Order(1)
MeterObservationHandler<Context> customMeterObservationHandler2(CalledHandlers calledHandlers) {
return new CustomMeterObservationHandler("customMeterObservationHandler2", calledHandlers);
}
@Bean
@Order(0)
MeterObservationHandler<Context> customMeterObservationHandler1(CalledHandlers calledHandlers) {
return new CustomMeterObservationHandler("customMeterObservationHandler1", calledHandlers);
}
}
private static class CustomTracingObservationHandler implements TracingObservationHandler<Context> {
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<ObservationHandler<?>> calledHandlers = new ArrayList<>();
void onCalled(ObservationHandler<?> handler) {
this.calledHandlers.add(handler);
}
List<ObservationHandler<?>> getCalledHandlers() {
return this.calledHandlers;
}
}
@Configuration(proxyBeanMethods = false)
static class CalledHandlersConfiguration {
@Bean
CalledHandlers calledHandlers() {
return new CalledHandlers();
}
}
private static class CustomObservationHandler implements ObservationHandler<Context> {
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<Context> {
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;
}
}
}

View File

@ -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<ObservationRegistryCustomizer<?>> customizers = new ArrayList<>();
void onCalled(ObservationRegistryCustomizer<?> customizer) {
this.customizers.add(customizer);
}
List<ObservationRegistryCustomizer<?>> getCustomizers() {
return this.customizers;
}
}
private static class Customizer1 implements ObservationRegistryCustomizer<ObservationRegistry> {
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<ObservationRegistry> {
private final CalledCustomizers calledCustomizers;
Customizer2(CalledCustomizers calledCustomizers) {
this.calledCustomizers = calledCustomizers;
}
@Override
public void customize(ObservationRegistry registry) {
this.calledCustomizers.onCalled(this);
}
}
}

View File

@ -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);
}
}

View File

@ -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")

View File

@ -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 = [

View File

@ -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")
}