From f8555b9071878890e940cd18a2ae3ce44661d841 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 21 Apr 2021 13:57:00 +0200 Subject: [PATCH] Add metrics for task execution and scheduling Closes gh-23818 --- .../TaskExecutorMetricsAutoConfiguration.java | 127 +++++++++++++++ .../metrics/task/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 1 + ...ExecutorMetricsAutoConfigurationTests.java | 147 ++++++++++++++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 7 + 5 files changed, 302 insertions(+) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.java new file mode 100644 index 0000000000..0157eba56f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfiguration.java @@ -0,0 +1,127 @@ +/* + * 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.task; + +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.function.Supplier; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +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.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.StringUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for metrics on all available + * {@link ThreadPoolTaskExecutor task executors} and {@link ThreadPoolTaskScheduler task + * schedulers}. + * + * @author Stephane Nicoll + * @since 2.6.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, + TaskExecutionAutoConfiguration.class, TaskSchedulingAutoConfiguration.class }) +@ConditionalOnClass(ExecutorServiceMetrics.class) +@ConditionalOnBean({ Executor.class, MeterRegistry.class }) +public class TaskExecutorMetricsAutoConfiguration { + + private static final String TASK_EXECUTOR_SUFFIX = "taskExecutor"; + + private static final String TASK_SCHEDULER_SUFFIX = "taskScheduler"; + + @Autowired + public void bindTaskExecutorsToRegistry(Map executors, MeterRegistry registry) { + executors.forEach((beanName, executor) -> { + if (executor instanceof ThreadPoolTaskExecutor) { + monitor(registry, safeGetThreadPoolExecutor((ThreadPoolTaskExecutor) executor), + () -> getTaskExecutorName(beanName)); + } + else if (executor instanceof ThreadPoolTaskScheduler) { + monitor(registry, safeGetThreadPoolExecutor((ThreadPoolTaskScheduler) executor), + () -> getTaskSchedulerName(beanName)); + } + }); + } + + private void monitor(MeterRegistry registry, ThreadPoolExecutor threadPoolExecutor, Supplier beanName) { + if (threadPoolExecutor != null) { + ExecutorServiceMetrics.monitor(registry, threadPoolExecutor, beanName.get()); + } + } + + private ThreadPoolExecutor safeGetThreadPoolExecutor(ThreadPoolTaskExecutor taskExecutor) { + try { + return taskExecutor.getThreadPoolExecutor(); + } + catch (IllegalStateException ex) { + return null; + } + } + + private ThreadPoolExecutor safeGetThreadPoolExecutor(ThreadPoolTaskScheduler taskScheduler) { + try { + return taskScheduler.getScheduledThreadPoolExecutor(); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * Get the name of a {@link ThreadPoolTaskExecutor} based on its {@code beanName}. + * @param beanName the name of the {@link ThreadPoolTaskExecutor} bean + * @return a name for the given task executor + */ + private String getTaskExecutorName(String beanName) { + if (beanName.length() > TASK_EXECUTOR_SUFFIX.length() + && StringUtils.endsWithIgnoreCase(beanName, TASK_EXECUTOR_SUFFIX)) { + return beanName.substring(0, beanName.length() - TASK_EXECUTOR_SUFFIX.length()); + } + return beanName; + } + + /** + * Get the name of a {@link ThreadPoolTaskScheduler} based on its {@code beanName}. + * @param beanName the name of the {@link ThreadPoolTaskScheduler} bean + * @return a name for the given task scheduler + */ + private String getTaskSchedulerName(String beanName) { + if (beanName.equals(TASK_SCHEDULER_SUFFIX)) { + return "application"; + } + if (beanName.length() > TASK_SCHEDULER_SUFFIX.length() + && StringUtils.endsWithIgnoreCase(beanName, TASK_SCHEDULER_SUFFIX)) { + return beanName.substring(0, beanName.length() - TASK_SCHEDULER_SUFFIX.length()); + } + return beanName; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/package-info.java new file mode 100644 index 0000000000..04479a4a66 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/task/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 task execution and scheduling metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.task; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index ed0cca8ecc..08a70b7174 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -74,6 +74,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetric org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..cd34698efe --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/task/TaskExecutorMetricsAutoConfigurationTests.java @@ -0,0 +1,147 @@ +/* + * 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.task; + +import java.util.Collection; +import java.util.concurrent.Executor; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TaskExecutorMetricsAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class TaskExecutorMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(TaskExecutorMetricsAutoConfiguration.class)); + + @Test + void taskExecutorUsingAutoConfigurationIsInstrumented() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).singleElement() + .satisfies((meter) -> assertThat(meter.getId().getTag("name")).isEqualTo("application")); + }); + } + + @Test + void taskExecutorsWithCustomNamesAreInstrumented() { + this.contextRunner.withBean("firstTaskExecutor", Executor.class, ThreadPoolTaskExecutor::new) + .withBean("customName", ThreadPoolTaskExecutor.class, ThreadPoolTaskExecutor::new).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).map((meter) -> meter.getId().getTag("name")).containsExactlyInAnyOrder("first", + "customName"); + }); + } + + @Test + void threadPoolTaskExecutorWithNoTaskExecutorIsIgnored() { + ThreadPoolTaskExecutor unavailableTaskExecutor = mock(ThreadPoolTaskExecutor.class); + given(unavailableTaskExecutor.getThreadPoolExecutor()).willThrow(new IllegalStateException("Test")); + this.contextRunner.withBean("firstTaskExecutor", ThreadPoolTaskExecutor.class, ThreadPoolTaskExecutor::new) + .withBean("customName", ThreadPoolTaskExecutor.class, () -> unavailableTaskExecutor).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).singleElement() + .satisfies((meter) -> assertThat(meter.getId().getTag("name")).isEqualTo("first")); + }); + } + + @Test + void taskExecutorInstrumentationCanBeDisabled() { + this.contextRunner.withPropertyValues("management.metrics.enable.executor=false") + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("executor.completed").tags("name", "application").functionCounter()) + .isNull(); + }); + } + + @Test + void taskSchedulerUsingAutoConfigurationIsInstrumented() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .withUserConfiguration(SchedulingTestConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).singleElement() + .satisfies((meter) -> assertThat(meter.getId().getTag("name")).isEqualTo("application")); + }); + } + + @Test + void taskSchedulersWithCustomNamesAreInstrumented() { + this.contextRunner.withBean("firstTaskScheduler", Executor.class, ThreadPoolTaskScheduler::new) + .withBean("customName", ThreadPoolTaskScheduler.class, ThreadPoolTaskScheduler::new).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).map((meter) -> meter.getId().getTag("name")).containsExactlyInAnyOrder("first", + "customName"); + }); + } + + @Test + void threadPoolTaskSchedulerWithNoTaskExecutorIsIgnored() { + ThreadPoolTaskScheduler unavailableTaskExecutor = mock(ThreadPoolTaskScheduler.class); + given(unavailableTaskExecutor.getScheduledThreadPoolExecutor()).willThrow(new IllegalStateException("Test")); + this.contextRunner.withBean("firstTaskScheduler", ThreadPoolTaskScheduler.class, ThreadPoolTaskScheduler::new) + .withBean("customName", ThreadPoolTaskScheduler.class, () -> unavailableTaskExecutor).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + Collection meters = registry.get("executor.completed").functionCounters(); + assertThat(meters).singleElement() + .satisfies((meter) -> assertThat(meter.getId().getTag("name")).isEqualTo("first")); + }); + } + + @Test + void taskSchedulerInstrumentationCanBeDisabled() { + this.contextRunner.withPropertyValues("management.metrics.enable.executor=false") + .withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .withUserConfiguration(SchedulingTestConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("executor.completed").tags("name", "application").functionCounter()) + .isNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableScheduling + static class SchedulingTestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 47765a8460..b80976a584 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -586,6 +586,13 @@ Details are published under the `log4j2.events.` or `logback.events.` meter name +[[actuator.metrics.supported.tasks]] +==== Task Execution and Scheduling Metrics +Auto-configuration enables the instrumentation of all available `ThreadPoolTaskExecutor` and `ThreadPoolTaskScheduler` beans, as long as the underling `ThreadPoolExecutor` is available. +Metrics are tagged by the name of the executor that is derived from the bean name. + + + [[actuator.metrics.supported.spring-mvc]] ==== Spring MVC Metrics Auto-configuration enables the instrumentation of all requests handled by Spring MVC controllers and functional handlers.