Add Spring Data Repository metrics support
Add support for Spring Data Repository metrics by integrating with Spring Data's new `RepositoryMethodInvocationListener` support. Closes gh-22217pull/26041/head
parent
1893f935b4
commit
f03f74ff0a
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
@ -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<RepositoryFactoryCustomizer> 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);
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<Meter> 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<Tag> 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<Example, Long> {
|
||||
|
||||
long count();
|
||||
|
||||
}
|
||||
|
||||
interface ExampleAnnotatedRepository extends Repository<Example, Long> {
|
||||
|
||||
@Timed
|
||||
long count();
|
||||
|
||||
long delete();
|
||||
|
||||
}
|
||||
|
||||
static class Example {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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<City, Long> {
|
||||
|
||||
@Override
|
||||
Page<City> findAll(Pageable pageable);
|
||||
|
||||
Page<City> findByNameLikeAndCountryLikeAllIgnoringCase(String name, String country, Pageable pageable);
|
||||
|
||||
City findByNameAndCountryAllIgnoringCase(String name, String country);
|
||||
|
||||
}
|
@ -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<Tag> 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 <T> Tags and(Tags tags, T instance, String key, Function<T, String> value) {
|
||||
return and(tags, instance, key, value, null);
|
||||
}
|
||||
|
||||
private <T> Tags and(Tags tags, T instance, String key, Function<T, String> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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<Timed> annotations = TimedAnnotations.get(invocation.getMethod(), invocation.getRepositoryInterface());
|
||||
Iterable<Tag> 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));
|
||||
}
|
||||
|
||||
}
|
@ -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<Tag> repositoryTags(RepositoryMethodInvocation invocation);
|
||||
|
||||
}
|
@ -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<Tag> tags = this.provider.repositoryTags(invocation);
|
||||
assertThat(tags).contains(Tag.of("repository", "ExampleRepository"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repositoryTagsIncludesMethod() {
|
||||
RepositoryMethodInvocation invocation = createInvocation();
|
||||
Iterable<Tag> tags = this.provider.repositoryTags(invocation);
|
||||
assertThat(tags).contains(Tag.of("method", "findById"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repositoryTagsIncludesState() {
|
||||
RepositoryMethodInvocation invocation = createInvocation();
|
||||
Iterable<Tag> tags = this.provider.repositoryTags(invocation);
|
||||
assertThat(tags).contains(Tag.of("state", "SUCCESS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repositoryTagsIncludesException() {
|
||||
RepositoryMethodInvocation invocation = createInvocation(new IOException());
|
||||
Iterable<Tag> tags = this.provider.repositoryTags(invocation);
|
||||
assertThat(tags).contains(Tag.of("exception", "IOException"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repositoryTagsWhenNoExceptionIncludesExceptionTagWithNone() {
|
||||
RepositoryMethodInvocation invocation = createInvocation();
|
||||
Iterable<Tag> 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, Long> {
|
||||
|
||||
Example findById(long id);
|
||||
|
||||
}
|
||||
|
||||
static class Example {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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, Long> {
|
||||
|
||||
Example findById(long id);
|
||||
|
||||
}
|
||||
|
||||
interface TimedMethodRepository extends Repository<Example, Long> {
|
||||
|
||||
@Timed(extraTags = { "tag1", "value1" })
|
||||
Example findById(long id);
|
||||
|
||||
}
|
||||
|
||||
@Timed(extraTags = { "taga", "valuea" })
|
||||
interface TimedClassRepository extends Repository<Example, Long> {
|
||||
|
||||
Example findById(long id);
|
||||
|
||||
}
|
||||
|
||||
static class Example {
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue