diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 126a5927bd..6756c7cfc4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -95,6 +95,7 @@ dependencies { optional("org.springframework.amqp:spring-rabbit") optional("org.springframework.data:spring-data-cassandra") optional("org.springframework.data:spring-data-couchbase") + optional("org.springframework.data:spring-data-jpa") optional("org.springframework.data:spring-data-ldap") optional("org.springframework.data:spring-data-mongodb") optional("org.springframework.data:spring-data-redis") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index c2b9be11d2..c4c0b2c2f4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -54,6 +54,8 @@ public class MetricsProperties { private final Web web = new Web(); + private final Data data = new Data(); + private final Distribution distribution = new Distribution(); public boolean isUseGlobalRegistry() { @@ -76,6 +78,10 @@ public class MetricsProperties { return this.web; } + public Data getData() { + return this.data; + } + public Distribution getDistribution() { return this.distribution; } @@ -213,6 +219,43 @@ public class MetricsProperties { } + public static class Data { + + private final Repository repository = new Repository(); + + public Repository getRepository() { + return this.repository; + } + + public static class Repository { + + /** + * Name of the metric for sent requests. + */ + private String metricName = "spring.data.repository.invocations"; + + /** + * Auto-timed request settings. + */ + @NestedConfigurationProperty + private final AutoTimeProperties autotime = new AutoTimeProperties(); + + public String getMetricName() { + return this.metricName; + } + + public void setMetricName(String metricName) { + this.metricName = metricName; + } + + public AutoTimeProperties getAutotime() { + return this.autotime; + } + + } + + } + public static class Distribution { /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java new file mode 100644 index 0000000000..c99a9c5fb8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +/** + * {@link BeanPostProcessor} to apply a {@link MetricsRepositoryMethodInvocationListener} + * to all {@link RepositoryFactorySupport repository factories}. + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerBeanPostProcessor implements BeanPostProcessor { + + private final RepositoryFactoryCustomizer customizer; + + MetricsRepositoryMethodInvocationListenerBeanPostProcessor(MetricsRepositoryMethodInvocationListener listener) { + this.customizer = (repositoryFactory) -> repositoryFactory.addInvocationListener(listener); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RepositoryFactoryBeanSupport) { + ((RepositoryFactoryBeanSupport) bean).addRepositoryFactoryCustomizer(this.customizer); + } + return bean; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java new file mode 100644 index 0000000000..708e342cd5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Data.Repository; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +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.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring Data Repository metrics. + * + * @author Phillip Webb + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(org.springframework.data.repository.Repository.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class }) +@ConditionalOnBean(MeterRegistry.class) +@EnableConfigurationProperties(MetricsProperties.class) +public class RepositoryMetricsAutoConfiguration { + + private final MetricsProperties properties; + + public RepositoryMetricsAutoConfiguration(MetricsProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean(RepositoryTagsProvider.class) + public DefaultRepositoryTagsProvider repositoryTagsProvider() { + return new DefaultRepositoryTagsProvider(); + } + + @Bean + @ConditionalOnMissingBean + public MetricsRepositoryMethodInvocationListener metricsRepositoryMethodInvocationListener(MeterRegistry registry, + RepositoryTagsProvider tagsProvider) { + Repository properties = this.properties.getData().getRepository(); + return new MetricsRepositoryMethodInvocationListener(registry, tagsProvider, properties.getMetricName(), + properties.getAutotime()); + } + + @Bean + public static MetricsRepositoryMethodInvocationListenerBeanPostProcessor metricsRepositoryMethodInvocationListenerBeanPostProcessor( + MetricsRepositoryMethodInvocationListener metricsRepositoryMethodInvocationListener) { + return new MetricsRepositoryMethodInvocationListenerBeanPostProcessor( + metricsRepositoryMethodInvocationListener); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java new file mode 100644 index 0000000000..6d724f122c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/data/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 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 Spring Data actuator metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.data; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java new file mode 100644 index 0000000000..9161dbd98e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link MetricsRepositoryMethodInvocationListenerBeanPostProcessor} . + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerBeanPostProcessorTests { + + private MetricsRepositoryMethodInvocationListener listener = mock(MetricsRepositoryMethodInvocationListener.class); + + private MetricsRepositoryMethodInvocationListenerBeanPostProcessor postProcessor = new MetricsRepositoryMethodInvocationListenerBeanPostProcessor( + this.listener); + + @Test + @SuppressWarnings("rawtypes") + void postProcessBeforeInitializationWhenRepositoryFactoryBeanSupportAddsListener() { + RepositoryFactoryBeanSupport bean = mock(RepositoryFactoryBeanSupport.class); + Object result = this.postProcessor.postProcessBeforeInitialization(bean, "name"); + assertThat(result).isSameAs(bean); + ArgumentCaptor customizer = ArgumentCaptor + .forClass(RepositoryFactoryCustomizer.class); + verify(bean).addRepositoryFactoryCustomizer(customizer.capture()); + RepositoryFactorySupport repositoryFactory = mock(RepositoryFactorySupport.class); + customizer.getValue().customize(repositoryFactory); + verify(repositoryFactory).addInvocationListener(this.listener); + } + + @Test + void postProcessBeforeInitializationWhenOtherBeanDoesNothing() { + Object bean = new Object(); + Object result = this.postProcessor.postProcessBeforeInitialization(bean, "name"); + assertThat(result).isSameAs(bean); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java new file mode 100644 index 0000000000..385b17718e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationIntegrationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.data.city.CityRepository; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurationPackage; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RepositoryMetricsAutoConfiguration}. + * + * @author Phillip Webb + */ +public class RepositoryMetricsAutoConfigurationIntegrationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration( + AutoConfigurations.of(HibernateJpaAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, RepositoryMetricsAutoConfiguration.class)) + .withUserConfiguration(EmbeddedDataSourceConfiguration.class, TestConfig.class); + + @Test + void repositoryMethodCallRecordsMetrics() { + this.contextRunner.run((context) -> { + context.getBean(CityRepository.class).count(); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.get("spring.data.repository.invocations").tag("repository", "CityRepository").timer() + .count()).isEqualTo(1); + }); + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigurationPackage + static class TestConfig { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..2fdeb30699 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/RepositoryMetricsAutoConfigurationTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import java.util.Collection; +import java.util.Collections; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.actuate.metrics.data.DefaultRepositoryTagsProvider; +import org.springframework.boot.actuate.metrics.data.MetricsRepositoryMethodInvocationListener; +import org.springframework.boot.actuate.metrics.data.RepositoryTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RepositoryMetricsAutoConfiguration}. + * + * @author Phillip Webb + */ +class RepositoryMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(RepositoryMetricsAutoConfiguration.class)); + + @Test + void backsOffWhenMeterRegistryIsMissing() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RepositoryMetricsAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RepositoryTagsProvider.class)); + } + + @Test + void definesTagsProviderAndListenerWhenMeterRegistryIsPresent() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(DefaultRepositoryTagsProvider.class); + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListener.class); + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListenerBeanPostProcessor.class); + }); + } + + @Test + void tagsProviderBacksOff() { + this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(DefaultRepositoryTagsProvider.class); + assertThat(context).hasSingleBean(TestRepositoryTagsProvider.class); + }); + } + + @Test + void metricsRepositoryMethodInvocationListenerBacksOff() { + this.contextRunner.withUserConfiguration(MetricsRepositoryMethodInvocationListenerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(MetricsRepositoryMethodInvocationListener.class); + assertThat(context).hasSingleBean(TestMetricsRepositoryMethodInvocationListener.class); + }); + } + + @Test + void metricNameCanBeConfigured() { + this.contextRunner.withUserConfiguration(TestController.class) + .withPropertyValues("management.metrics.data.repository.metric-name=datarepo").run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleRepository.class); + Timer timer = registry.get("datarepo").timer(); + assertThat(timer).isNotNull(); + }); + } + + @Test + void autoTimeRequestsCanBeConfigured() { + this.contextRunner.withUserConfiguration(TestController.class) + .withPropertyValues("management.metrics.data.repository.autotime.enabled=true", + "management.metrics.data.repository.autotime.percentiles=0.5,0.7", + "management.metrics.data.repository.autotime.percentiles-histogram=true") + .run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleRepository.class); + Timer timer = registry.get("spring.data.repository.invocations").timer(); + HistogramSnapshot snapshot = timer.takeSnapshot(); + assertThat(snapshot.percentileValues()).hasSize(2); + assertThat(snapshot.percentileValues()[0].percentile()).isEqualTo(0.5); + assertThat(snapshot.percentileValues()[1].percentile()).isEqualTo(0.7); + }); + } + + @Test + void timerWorksWithTimedAnnotationsWhenAutoTimeRequestsIsFalse() { + this.contextRunner.withPropertyValues("management.metrics.data.repository.autotime.enabled=false") + .run((context) -> { + MeterRegistry registry = getInitializedMeterRegistry(context, ExampleAnnotatedRepository.class); + Collection meters = registry.get("spring.data.repository.invocations").meters(); + assertThat(meters).hasSize(1); + Meter meter = meters.iterator().next(); + assertThat(meter.getId().getTag("method")).isEqualTo("count"); + }); + } + + private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext context, + Class repositoryInterface) throws Exception { + MetricsRepositoryMethodInvocationListener listener = context + .getBean(MetricsRepositoryMethodInvocationListener.class); + ReflectionUtils.doWithLocalMethods(repositoryInterface, (method) -> { + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn(State.SUCCESS); + RepositoryMethodInvocation invocation = new RepositoryMethodInvocation(repositoryInterface, method, result, + 10); + listener.afterInvocation(invocation); + }); + return context.getBean(MeterRegistry.class); + } + + @Configuration(proxyBeanMethods = false) + static class TagsProviderConfiguration { + + @Bean + TestRepositoryTagsProvider tagsProvider() { + return new TestRepositoryTagsProvider(); + } + + } + + private static final class TestRepositoryTagsProvider implements RepositoryTagsProvider { + + @Override + public Iterable repositoryTags(RepositoryMethodInvocation invocation) { + return Collections.emptyList(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MetricsRepositoryMethodInvocationListenerConfiguration { + + @Bean + MetricsRepositoryMethodInvocationListener metricsRepositoryMethodInvocationListener(MeterRegistry registry, + RepositoryTagsProvider tagsProvider) { + return new TestMetricsRepositoryMethodInvocationListener(registry, tagsProvider); + } + + } + + static class TestMetricsRepositoryMethodInvocationListener extends MetricsRepositoryMethodInvocationListener { + + TestMetricsRepositoryMethodInvocationListener(MeterRegistry registry, RepositoryTagsProvider tagsProvider) { + super(registry, tagsProvider, "test", AutoTimer.DISABLED); + } + + } + + interface ExampleRepository extends Repository { + + long count(); + + } + + interface ExampleAnnotatedRepository extends Repository { + + @Timed + long count(); + + long delete(); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java new file mode 100644 index 0000000000..9b64dcbc10 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/City.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2021 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.metrics.data.city; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class City implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String state; + + @Column(nullable = false) + private String country; + + @Column(nullable = false) + private String map; + + protected City() { + } + + public City(String name, String country) { + this.name = name; + this.country = country; + } + + public String getName() { + return this.name; + } + + public String getState() { + return this.state; + } + + public String getCountry() { + return this.country; + } + + public String getMap() { + return this.map; + } + + @Override + public String toString() { + return getName() + "," + getState() + "," + getCountry(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java new file mode 100644 index 0000000000..1fd0a63cf5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/data/city/CityRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2021 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.metrics.data.city; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CityRepository extends JpaRepository { + + @Override + Page findAll(Pageable pageable); + + Page findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country, Pageable pageable); + + City findByNameAndCountryAllIgnoringCase(String name, String country); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java new file mode 100644 index 0000000000..801feffa3b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProvider.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import java.lang.reflect.Method; +import java.util.function.Function; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.StringUtils; + +/** + * Default {@link RepositoryTagsProvider} implementation. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public class DefaultRepositoryTagsProvider implements RepositoryTagsProvider { + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + @Override + public Iterable repositoryTags(RepositoryMethodInvocation invocation) { + Tags tags = Tags.empty(); + tags = and(tags, invocation.getRepositoryInterface(), "repository", this::getSimpleClassName); + tags = and(tags, invocation.getMethod(), "method", Method::getName); + tags = and(tags, invocation.getResult().getState(), "state", State::name); + tags = and(tags, invocation.getResult().getError(), "exception", this::getExceptionName, EXCEPTION_NONE); + return tags; + } + + private Tags and(Tags tags, T instance, String key, Function value) { + return and(tags, instance, key, value, null); + } + + private Tags and(Tags tags, T instance, String key, Function value, Tag fallback) { + if (instance != null) { + return tags.and(key, value.apply(instance)); + } + return (fallback != null) ? tags.and(fallback) : tags; + } + + private String getExceptionName(Throwable error) { + return getSimpleClassName(error.getClass()); + } + + private String getSimpleClassName(Class type) { + String simpleName = type.getSimpleName(); + return (!StringUtils.hasText(simpleName)) ? type.getName() : simpleName; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java new file mode 100644 index 0000000000..328c2c9f6f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListener.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; + +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener; + +/** + * Intercepts Spring Data {@code Repository} invocations and records metrics about + * execution time and results. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public class MetricsRepositoryMethodInvocationListener implements RepositoryMethodInvocationListener { + + private final MeterRegistry registry; + + private final RepositoryTagsProvider tagsProvider; + + private final String metricName; + + private final AutoTimer autoTimer; + + /** + * Create a new {@code MetricsRepositoryMethodInvocationListener}. + * @param registry the registry to which metrics are recorded + * @param tagsProvider provider for metrics tags + * @param metricName name of the metric to record + * @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing + */ + public MetricsRepositoryMethodInvocationListener(MeterRegistry registry, RepositoryTagsProvider tagsProvider, + String metricName, AutoTimer autoTimer) { + this.registry = registry; + this.tagsProvider = tagsProvider; + this.metricName = metricName; + this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED; + } + + @Override + public void afterInvocation(RepositoryMethodInvocation invocation) { + Set annotations = TimedAnnotations.get(invocation.getMethod(), invocation.getRepositoryInterface()); + Iterable tags = this.tagsProvider.repositoryTags(invocation); + long duration = invocation.getDuration(TimeUnit.NANOSECONDS); + AutoTimer.apply(this.autoTimer, this.metricName, annotations, + (builder) -> builder.tags(tags).register(this.registry).record(duration, TimeUnit.NANOSECONDS)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java new file mode 100644 index 0000000000..4f6b55f943 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/data/RepositoryTagsProvider.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; + +/** + * Provides {@link Tag Tags} for Spring Data {@link RepositoryMethodInvocation Repository + * invocations}. + * + * @author Phillip Webb + * @since 2.5.0 + */ +@FunctionalInterface +public interface RepositoryTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code invocation}. + * @param invocation the repository invocation + * @return tags to associate with metrics for the invocation + */ + Iterable repositoryTags(RepositoryMethodInvocation invocation); + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java new file mode 100644 index 0000000000..14a5efd69f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/DefaultRepositoryTagsProviderTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import java.io.IOException; +import java.lang.reflect.Method; + +import io.micrometer.core.instrument.Tag; +import org.junit.jupiter.api.Test; + +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultRepositoryTagsProvider}. + * + * @author Phillip Webb + */ +class DefaultRepositoryTagsProviderTests { + + private DefaultRepositoryTagsProvider provider = new DefaultRepositoryTagsProvider(); + + @Test + void repositoryTagsIncludesRepository() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("repository", "ExampleRepository")); + } + + @Test + void repositoryTagsIncludesMethod() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("method", "findById")); + } + + @Test + void repositoryTagsIncludesState() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("state", "SUCCESS")); + } + + @Test + void repositoryTagsIncludesException() { + RepositoryMethodInvocation invocation = createInvocation(new IOException()); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("exception", "IOException")); + } + + @Test + void repositoryTagsWhenNoExceptionIncludesExceptionTagWithNone() { + RepositoryMethodInvocation invocation = createInvocation(); + Iterable tags = this.provider.repositoryTags(invocation); + assertThat(tags).contains(Tag.of("exception", "None")); + } + + private RepositoryMethodInvocation createInvocation() { + return createInvocation(null); + } + + private RepositoryMethodInvocation createInvocation(Throwable error) { + Class repositoryInterface = ExampleRepository.class; + Method method = ReflectionUtils.findMethod(repositoryInterface, "findById", long.class); + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn((error != null) ? State.ERROR : State.SUCCESS); + given(result.getError()).willReturn(error); + return new RepositoryMethodInvocation(repositoryInterface, method, result, 0); + } + + interface ExampleRepository extends Repository { + + Example findById(long id); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java new file mode 100644 index 0000000000..15b766db7c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/data/MetricsRepositoryMethodInvocationListenerTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2021 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.metrics.data; + +import java.lang.reflect.Method; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.AutoTimer; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocation; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult; +import org.springframework.data.repository.core.support.RepositoryMethodInvocationListener.RepositoryMethodInvocationResult.State; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MetricsRepositoryMethodInvocationListener}. + * + * @author Phillip Webb + */ +class MetricsRepositoryMethodInvocationListenerTests { + + private static final String REQUEST_METRICS_NAME = "repository.invocations"; + + private SimpleMeterRegistry registry; + + private MetricsRepositoryMethodInvocationListener listener; + + @BeforeEach + void setup() { + MockClock clock = new MockClock(); + this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); + this.listener = new MetricsRepositoryMethodInvocationListener(this.registry, + new DefaultRepositoryTagsProvider(), REQUEST_METRICS_NAME, AutoTimer.ENABLED); + } + + @Test + void afterInvocationWhenNoTimerAnnotationsAndNoAutoTimerDoesNothing() { + this.listener = new MetricsRepositoryMethodInvocationListener(this.registry, + new DefaultRepositoryTagsProvider(), REQUEST_METRICS_NAME, null); + this.listener.afterInvocation(createInvocation(NoAnnotationsRepository.class)); + assertThat(this.registry.find(REQUEST_METRICS_NAME).timers()).isEmpty(); + } + + @Test + void afterInvocationWhenTimedMethodRecordsMetrics() { + this.listener.afterInvocation(createInvocation(TimedMethodRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + assertMetricsContainsTag("tag1", "value1"); + } + + @Test + void afterInvocationWhenTimedClassRecordsMetrics() { + this.listener.afterInvocation(createInvocation(TimedClassRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + assertMetricsContainsTag("taga", "valuea"); + } + + @Test + void afterInvocationWhenAutoTimedRecordsMetrics() { + this.listener.afterInvocation(createInvocation(NoAnnotationsRepository.class)); + assertMetricsContainsTag("state", "SUCCESS"); + } + + private void assertMetricsContainsTag(String tagKey, String tagValue) { + assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1); + } + + private RepositoryMethodInvocation createInvocation(Class repositoryInterface) { + Method method = ReflectionUtils.findMethod(repositoryInterface, "findById", long.class); + RepositoryMethodInvocationResult result = mock(RepositoryMethodInvocationResult.class); + given(result.getState()).willReturn(State.SUCCESS); + return new RepositoryMethodInvocation(repositoryInterface, method, result, 0); + } + + interface NoAnnotationsRepository extends Repository { + + Example findById(long id); + + } + + interface TimedMethodRepository extends Repository { + + @Timed(extraTags = { "tag1", "value1" }) + Example findById(long id); + + } + + @Timed(extraTags = { "taga", "valuea" }) + interface TimedClassRepository extends Repository { + + Example findById(long id); + + } + + static class Example { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 0c41516622..a94d17173d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -2379,6 +2379,37 @@ You can enable that on the auto-configured `EntityManagerFactory` as shown in th +[[production-ready-metrics-data-repository]] +==== Spring Data Repository Metrics +Auto-configuration enables the instrumentation of all Spring Data `Repository` method invocations. +By default, metrics are generated with the name, `spring.data.repository.invocations`. +The name can be customized by setting the configprop:management.metrics.data.repository.metric-name[] property. + +`@Timed` annotations are supported on `Repository` classes and methods (see <> for details). +If you don't want to record metrics for all `Repository` invocations, you can set configprop:management.metrics.data.repository.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. + +By default, repository invocation related metrics are tagged with the following information: + +|=== +| Tag | Description + +| `repository` +| Simple class name of the source `Repository`. + +| `method` +| The name of the `Repository` method that was invoked. + +| `state` +| The result state (`SUCCESS`, `ERROR`, `CANCELED` or `RUNNING`). + +| `exception` +| Simple class name of any exception that was thrown from the invocation. +|=== + +To replace the default tags, provide a `@Bean` that implements `RepositoryTagsProvider`. + + + [[production-ready-metrics-rabbitmq]] ==== RabbitMQ Metrics Auto-configuration will enable the instrumentation of all available RabbitMQ connection factories with a metric named `rabbitmq`.