From 2e67963bfeebfb66f4bde2ba426ee71b174e68a3 Mon Sep 17 00:00:00 2001 From: bono007 Date: Sun, 5 Sep 2021 21:05:50 -0500 Subject: [PATCH 1/2] Add startup time metrics See gh-27878 --- .../StartupTimeMetricsAutoConfiguration.java | 50 ++++++++ .../metrics/startup/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 1 + ...rtupTimeMetricsAutoConfigurationTests.java | 92 +++++++++++++ .../JettyMetricsAutoConfigurationTests.java | 18 +-- .../TomcatMetricsAutoConfigurationTests.java | 5 +- .../metrics/startup/StartupTimeMetrics.java | 109 ++++++++++++++++ .../actuate/metrics/startup/package-info.java | 20 +++ .../startup/StartupTimeMetricsTests.java | 121 ++++++++++++++++++ .../RestartApplicationListenerTests.java | 3 +- .../boot/SpringApplication.java | 10 +- .../boot/SpringApplicationRunListener.java | 35 +++++ .../boot/SpringApplicationRunListeners.java | 13 +- .../context/event/ApplicationReadyEvent.java | 30 ++++- .../event/ApplicationStartedEvent.java | 28 ++++ .../event/EventPublishingRunListener.java | 21 ++- .../boot/SpringApplicationTests.java | 37 ++++++ ...gApplicationAdminMXBeanRegistrarTests.java | 8 +- .../ApplicationPidFileWriterTests.java | 3 +- .../EventPublishingRunListenerTests.java | 7 +- 20 files changed, 601 insertions(+), 30 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java new file mode 100644 index 0000000000..d98f3d51de --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.startup; + +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.metrics.startup.StartupTimeMetrics; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupTimeMetrics}. + * + * @author Chris Bono + * @since 2.6.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass(MeterRegistry.class) +@ConditionalOnBean(MeterRegistry.class) +public class StartupTimeMetricsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + StartupTimeMetrics startupTimeMetrics(MeterRegistry meterRegistry) { + return new StartupTimeMetrics(meterRegistry); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/package-info.java new file mode 100644 index 0000000000..a8af9891d6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/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 actuator startup time metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.startup; 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 554f906bf7..3cec6d499b 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 @@ -75,6 +75,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoCon org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsAutoConfiguration,\ 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,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..bb442f37cb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java @@ -0,0 +1,92 @@ +/* + * 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.startup; + +import java.time.Duration; + +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.metrics.startup.StartupTimeMetrics; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StartupTimeMetricsAutoConfiguration}. + * + * @author Chris Bono + */ +class StartupTimeMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(StartupTimeMetricsAutoConfiguration.class)); + + @Test + void startupTimeMetricsAreRecorded() { + this.contextRunner.run((context) -> { + context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(2500))); + context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(3000))); + assertThat(context).hasSingleBean(StartupTimeMetrics.class); + SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); + assertThat(registry.find("application.started.time").timeGauge()).isNotNull(); + assertThat(registry.find("application.ready.time").timeGauge()).isNotNull(); + }); + } + + @Test + void startupTimeMetricsCanBeDisabled() { + this.contextRunner.withPropertyValues("management.metrics.enable.application.started.time:false", + "management.metrics.enable.application.ready.time:false").run((context) -> { + context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(2500))); + context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(3000))); + SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); + assertThat(registry.find("application.started.time").timeGauge()).isNull(); + assertThat(registry.find("application.ready.time").timeGauge()).isNull(); + }); + } + + @Test + void customStartupTimeMetricsAreRespected() { + this.contextRunner.withUserConfiguration(CustomStartupTimeMetricsConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(StartupTimeMetrics.class) + .hasBean("customStartTimeMetrics")); + } + + @Configuration(proxyBeanMethods = false) + static class CustomStartupTimeMetricsConfiguration { + + @Bean + StartupTimeMetrics customStartTimeMetrics() { + return new StartupTimeMetrics(new SimpleMeterRegistry(), Tags.empty(), "myapp.started", "myapp.ready"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java index 6af671e950..7a8f84c2a3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.jetty; +import java.time.Duration; + import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -58,7 +60,7 @@ class JettyMetricsAutoConfigurationTests { .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(JettyServerThreadPoolMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull(); @@ -73,7 +75,7 @@ class JettyMetricsAutoConfigurationTests { .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull(); }); @@ -95,7 +97,7 @@ class JettyMetricsAutoConfigurationTests { .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull(); @@ -110,7 +112,7 @@ class JettyMetricsAutoConfigurationTests { .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull(); }); @@ -125,7 +127,7 @@ class JettyMetricsAutoConfigurationTests { MeterRegistryConfiguration.class) .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class) .hasBean("customJettyConnectionMetricsBinder"); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); @@ -144,7 +146,7 @@ class JettyMetricsAutoConfigurationTests { "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull(); @@ -161,7 +163,7 @@ class JettyMetricsAutoConfigurationTests { "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull(); }); @@ -178,7 +180,7 @@ class JettyMetricsAutoConfigurationTests { "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class) .hasBean("customJettySslHandshakeMetricsBinder"); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java index 640c940562..017a9cbf18 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat; +import java.time.Duration; import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; @@ -62,7 +63,7 @@ class TomcatMetricsAutoConfigurationTests { .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); assertThat(context).hasSingleBean(TomcatMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull(); @@ -79,7 +80,7 @@ class TomcatMetricsAutoConfigurationTests { .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> { context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext())); + context.getSourceApplicationContext(), Duration.ZERO)); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull(); assertThat(registry.find("tomcat.threads.current").meter()).isNotNull(); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java new file mode 100644 index 0000000000..4356d0c757 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java @@ -0,0 +1,109 @@ +/* + * 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.startup; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.SmartApplicationListener; + +/** + * Binds application startup metrics in response to {@link ApplicationStartedEvent} and + * {@link ApplicationReadyEvent}. + * + * @author Chris Bono + * @since 2.6.0 + */ +public class StartupTimeMetrics implements SmartApplicationListener { + + private final MeterRegistry meterRegistry; + + private final String applicationStartedTimeMetricName; + + private final String applicationReadyTimeMetricName; + + private final Iterable tags; + + public StartupTimeMetrics(MeterRegistry meterRegistry) { + this(meterRegistry, Collections.emptyList(), "application.started.time", "application.ready.time"); + } + + public StartupTimeMetrics(MeterRegistry meterRegistry, Iterable tags, String applicationStartedTimeMetricName, + String applicationReadyTimeMetricName) { + this.meterRegistry = meterRegistry; + this.tags = (tags != null) ? tags : Collections.emptyList(); + this.applicationStartedTimeMetricName = applicationStartedTimeMetricName; + this.applicationReadyTimeMetricName = applicationReadyTimeMetricName; + } + + @Override + public boolean supportsEventType(Class eventType) { + return ApplicationStartedEvent.class.isAssignableFrom(eventType) + || ApplicationReadyEvent.class.isAssignableFrom(eventType); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ApplicationStartedEvent) { + onApplicationStarted((ApplicationStartedEvent) event); + } + if (event instanceof ApplicationReadyEvent) { + onApplicationReady((ApplicationReadyEvent) event); + } + } + + private void onApplicationStarted(ApplicationStartedEvent event) { + if (event.getStartupTime() == null) { + return; + } + TimeGauge + .builder(this.applicationStartedTimeMetricName, () -> event.getStartupTime().toMillis(), + TimeUnit.MILLISECONDS) + .tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication())) + .description("Time taken (ms) to start the application").register(this.meterRegistry); + } + + private void onApplicationReady(ApplicationReadyEvent event) { + if (event.getStartupTime() == null) { + return; + } + TimeGauge + .builder(this.applicationReadyTimeMetricName, () -> event.getStartupTime().toMillis(), + TimeUnit.MILLISECONDS) + .tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication())) + .description("Time taken (ms) for the application to be ready to serve requests") + .register(this.meterRegistry); + } + + private Iterable maybeDcorateTagsWithApplicationInfo(SpringApplication springApplication) { + Class mainClass = springApplication.getMainApplicationClass(); + if (mainClass == null) { + return this.tags; + } + return Tags.concat(this.tags, "main-application-class", mainClass.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/package-info.java new file mode 100644 index 0000000000..1a99ca8496 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/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. + */ + +/** + * Actuator support for startup metrics. + */ +package org.springframework.boot.actuate.metrics.startup; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java new file mode 100644 index 0000000000..d6ef5f3855 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java @@ -0,0 +1,121 @@ +/* + * 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.startup; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link StartupTimeMetrics}. + * + * @author Chris Bono + */ +class StartupTimeMetricsTests { + + private static final long APP_STARTED_TIME_MS = 2500; + + private static final long APP_RUNNING_TIME_MS = 2900; + + private MeterRegistry registry; + + private StartupTimeMetrics metrics; + + @BeforeEach + void prepareUnit() { + this.registry = new SimpleMeterRegistry(); + this.metrics = new StartupTimeMetrics(this.registry); + } + + @Test + void metricsRecordedWithoutCustomTags() { + this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); + this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); + assertMetricExistsWithValue("application.started.time", APP_STARTED_TIME_MS); + assertMetricExistsWithValue("application.ready.time", APP_RUNNING_TIME_MS); + } + + @Test + void metricsRecordedWithCustomTagsAndMetricNames() { + Tags tags = Tags.of("foo", "bar"); + this.metrics = new StartupTimeMetrics(this.registry, tags, "m1", "m2"); + this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); + this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); + assertMetricExistsWithCustomTagsAndValue("m1", tags, APP_STARTED_TIME_MS); + assertMetricExistsWithCustomTagsAndValue("m2", tags, APP_RUNNING_TIME_MS); + } + + @Test + void metricsRecordedWithoutMainAppClassTagWhenMainAppClassNotAvailable() { + this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); + this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); + assertThat(this.registry.find("application.started.time").timeGauge()).isNotNull(); + assertThat(this.registry.find("application.ready.time").timeGauge()).isNotNull(); + } + + @Test + void metricsNotRecordedWhenStartupTimeNotAvailable() { + this.metrics.onApplicationEvent(applicationStartedEvent(null)); + this.metrics.onApplicationEvent(applicationReadyEvent(null)); + assertThat(this.registry.find("application.started.time").timeGauge()).isNull(); + assertThat(this.registry.find("application.ready.time").timeGauge()).isNull(); + } + + private ApplicationStartedEvent applicationStartedEvent(Long startupTimeMs) { + SpringApplication application = mock(SpringApplication.class); + doReturn(TestMainApplication.class).when(application).getMainApplicationClass(); + return new ApplicationStartedEvent(application, null, null, + (startupTimeMs != null) ? Duration.ofMillis(startupTimeMs) : null); + } + + private ApplicationReadyEvent applicationReadyEvent(Long startupTimeMs) { + SpringApplication application = mock(SpringApplication.class); + doReturn(TestMainApplication.class).when(application).getMainApplicationClass(); + return new ApplicationReadyEvent(application, null, null, + (startupTimeMs != null) ? Duration.ofMillis(startupTimeMs) : null); + } + + private void assertMetricExistsWithValue(String metricName, double expectedValueInMillis) { + assertMetricExistsWithCustomTagsAndValue(metricName, Tags.empty(), expectedValueInMillis); + } + + private void assertMetricExistsWithCustomTagsAndValue(String metricName, Tags expectedCustomTags, + double expectedValueInMillis) { + assertThat(this.registry.find(metricName) + .tags(Tags.concat(expectedCustomTags, "main-application-class", TestMainApplication.class.getName())) + .timeGauge()).isNotNull().extracting((m) -> m.value(TimeUnit.MILLISECONDS)) + .isEqualTo(expectedValueInMillis); + } + + static class TestMainApplication { + + } + +} diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java index a6c2f65053..7f56ede403 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.devtools.restart; +import java.time.Duration; import java.util.List; import org.junit.jupiter.api.AfterEach; @@ -110,7 +111,7 @@ class RestartApplicationListenerTests { listener.onApplicationEvent(new ApplicationFailedEvent(application, ARGS, context, new RuntimeException())); } else { - listener.onApplicationEvent(new ApplicationReadyEvent(application, ARGS, context)); + listener.onApplicationEvent(new ApplicationReadyEvent(application, ARGS, context, Duration.ZERO)); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index ebdd27226d..2e430f050f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -17,6 +17,7 @@ package org.springframework.boot; import java.lang.reflect.Constructor; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -150,6 +151,7 @@ import org.springframework.util.StringUtils; * @author Madhura Bhave * @author Brian Clozel * @author Ethan Rubinson + * @author Chris Bono * @since 1.0.0 * @see #run(Class, String[]) * @see #run(Class[], String[]) @@ -285,7 +287,7 @@ public class SpringApplication { */ public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); - stopWatch.start(); + stopWatch.start("applicationStarted"); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; configureHeadlessProperty(); @@ -302,10 +304,11 @@ public class SpringApplication { refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); + stopWatch.start("applicationReady"); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } - listeners.started(context); + listeners.started(context, Duration.ofMillis(stopWatch.getTotalTimeMillis())); callRunners(context, applicationArguments); } catch (Throwable ex) { @@ -314,7 +317,8 @@ public class SpringApplication { } try { - listeners.running(context); + stopWatch.stop(); + listeners.running(context, Duration.ofMillis(stopWatch.getTotalTimeMillis())); } catch (Throwable ex) { handleRunFailure(context, ex, null); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java index bf16d739b9..f142cd4594 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java @@ -16,6 +16,8 @@ package org.springframework.boot; +import java.time.Duration; + import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; @@ -31,6 +33,7 @@ import org.springframework.core.io.support.SpringFactoriesLoader; * @author Phillip Webb * @author Dave Syer * @author Andy Wilkinson + * @author Chris Bono * @since 1.0.0 */ public interface SpringApplicationRunListener { @@ -75,20 +78,52 @@ public interface SpringApplicationRunListener { * ApplicationRunners} have not been called. * @param context the application context. * @since 2.0.0 + * @deprecated since 2.6.0 for removal in 2.8.0 in favour of + * {@link #started(ConfigurableApplicationContext, Duration)} */ + @Deprecated default void started(ConfigurableApplicationContext context) { } + /** + * The context has been refreshed and the application has started but + * {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner + * ApplicationRunners} have not been called. + * @param context the application context. + * @param startupTime the time taken to start the application or {@code null} if + * unknown + * @since 2.0.0 + */ + default void started(ConfigurableApplicationContext context, Duration startupTime) { + started(context); + } + /** * Called immediately before the run method finishes, when the application context has * been refreshed and all {@link CommandLineRunner CommandLineRunners} and * {@link ApplicationRunner ApplicationRunners} have been called. * @param context the application context. + * @deprecated since 2.6.0 for removal in 2.8.0 in favour of + * {@link #running(ConfigurableApplicationContext, Duration)} * @since 2.0.0 */ + @Deprecated default void running(ConfigurableApplicationContext context) { } + /** + * Called immediately before the run method finishes, when the application context has + * been refreshed and all {@link CommandLineRunner CommandLineRunners} and + * {@link ApplicationRunner ApplicationRunners} have been called. + * @param context the application context. + * @param startupTime the time taken for the application to be ready to service + * requests or {@code null} if unknown + * @since 2.6.0 + */ + default void running(ConfigurableApplicationContext context, Duration startupTime) { + running(context); + } + /** * Called when a failure occurs when running the application. * @param context the application context or {@code null} if a failure occurred before diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java index 21c26ef8fa..982cc0f24c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.boot; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -33,6 +34,8 @@ import org.springframework.util.ReflectionUtils; * A collection of {@link SpringApplicationRunListener}. * * @author Phillip Webb + * @author Andy Wilkinson + * @author Chris Bono */ class SpringApplicationRunListeners { @@ -71,12 +74,12 @@ class SpringApplicationRunListeners { doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context)); } - void started(ConfigurableApplicationContext context) { - doWithListeners("spring.boot.application.started", (listener) -> listener.started(context)); + void started(ConfigurableApplicationContext context, Duration startupTime) { + doWithListeners("spring.boot.application.started", (listener) -> listener.started(context, startupTime)); } - void running(ConfigurableApplicationContext context) { - doWithListeners("spring.boot.application.running", (listener) -> listener.running(context)); + void running(ConfigurableApplicationContext context, Duration startupTime) { + doWithListeners("spring.boot.application.running", (listener) -> listener.running(context, startupTime)); } void failed(ConfigurableApplicationContext context, Throwable exception) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java index b66d20ab3b..d16d72ee78 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.context.event; +import java.time.Duration; + import org.springframework.boot.SpringApplication; import org.springframework.context.ConfigurableApplicationContext; @@ -26,6 +28,7 @@ import org.springframework.context.ConfigurableApplicationContext; * have been completed by then. * * @author Stephane Nicoll + * @author Chris Bono * @since 1.3.0 * @see ApplicationFailedEvent */ @@ -34,15 +37,32 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { private final ConfigurableApplicationContext context; + private final Duration startupTime; + /** * Create a new {@link ApplicationReadyEvent} instance. * @param application the current application * @param args the arguments the application is running with * @param context the context that was being created + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link #ApplicationReadyEvent(SpringApplication, String[], ConfigurableApplicationContext, Duration)} */ public ApplicationReadyEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context) { + this(application, args, context, null); + } + + /** + * Create a new {@link ApplicationReadyEvent} instance. + * @param application the current application + * @param args the arguments the application is running with + * @param context the context that was being created + * @param startupTime the time taken to get the application ready to service requests + */ + public ApplicationReadyEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context, + Duration startupTime) { super(application, args); this.context = context; + this.startupTime = startupTime; } /** @@ -53,4 +73,12 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { return this.context; } + /** + * Return the time taken for the application to be ready to service requests. + * @return the startup time + */ + public Duration getStartupTime() { + return this.startupTime; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java index 2eb99e81f4..7179a5edf2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.event; +import java.time.Duration; + import org.springframework.boot.ApplicationRunner; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; @@ -34,16 +36,34 @@ public class ApplicationStartedEvent extends SpringApplicationEvent { private final ConfigurableApplicationContext context; + private final Duration startupTime; + /** * Create a new {@link ApplicationStartedEvent} instance. * @param application the current application * @param args the arguments the application is running with * @param context the context that was being created + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link #ApplicationStartedEvent(SpringApplication, String[], ConfigurableApplicationContext, Duration)} */ + @Deprecated public ApplicationStartedEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context) { + this(application, args, context, null); + } + + /** + * Create a new {@link ApplicationStartedEvent} instance. + * @param application the current application + * @param args the arguments the application is running with + * @param context the context that was being created + * @param startupTime the time taken to start the application + */ + public ApplicationStartedEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context, + Duration startupTime) { super(application, args); this.context = context; + this.startupTime = startupTime; } /** @@ -54,4 +74,12 @@ public class ApplicationStartedEvent extends SpringApplicationEvent { return this.context; } + /** + * Return the time taken to start the application. + * @return the startup time + */ + public Duration getStartupTime() { + return this.startupTime; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java index fcc94af385..2bcbbc1ce3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.context.event; +import java.time.Duration; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -46,6 +48,7 @@ import org.springframework.util.ErrorHandler; * @author Andy Wilkinson * @author Artsiom Yudovin * @author Brian Clozel + * @author Chris Bono * @since 1.0.0 */ public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { @@ -101,14 +104,26 @@ public class EventPublishingRunListener implements SpringApplicationRunListener, } @Override + @Deprecated public void started(ConfigurableApplicationContext context) { - context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context)); + started(context, null); + } + + @Override + public void started(ConfigurableApplicationContext context, Duration startupTime) { + context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, startupTime)); AvailabilityChangeEvent.publish(context, LivenessState.CORRECT); } @Override + @Deprecated public void running(ConfigurableApplicationContext context) { - context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context)); + running(context, null); + } + + @Override + public void running(ConfigurableApplicationContext context, Duration startupTime) { + context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, startupTime)); AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index d029911cb8..0ba56fa5d5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -149,6 +149,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; * @author Artsiom Yudovin * @author Marten Deinum * @author Nguyen Bao Sach + * @author Chris Bono */ @ExtendWith(OutputCaptureExtension.class) class SpringApplicationTests { @@ -417,6 +418,42 @@ class SpringApplicationTests { inOrder.verifyNoMoreInteractions(); } + @Test + void applicationStartedEventHasStartupTime() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + final AtomicReference reference = new AtomicReference<>(); + class ApplicationStartedEventListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + reference.set(event); + } + + } + application.addListeners(new ApplicationStartedEventListener()); + this.context = application.run(); + assertThat(reference.get()).isNotNull().extracting(ApplicationStartedEvent::getStartupTime).isNotNull(); + } + + @Test + void applicationReadyEventHasStartupTime() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + final AtomicReference reference = new AtomicReference<>(); + class ApplicationReadyEventListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + reference.set(event); + } + + } + application.addListeners(new ApplicationReadyEventListener()); + this.context = application.run(); + assertThat(reference.get()).isNotNull().extracting(ApplicationReadyEvent::getStartupTime).isNotNull(); + } + @Test void defaultApplicationContext() { SpringApplication application = new SpringApplication(ExampleConfig.class); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java index 163cde055b..2a76cb2abe 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.admin; import java.lang.management.ManagementFactory; +import java.time.Duration; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; @@ -88,10 +89,11 @@ class SpringApplicationAdminMXBeanRegistrarTests { SpringApplicationAdminMXBeanRegistrar registrar = new SpringApplicationAdminMXBeanRegistrar(OBJECT_NAME); ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); registrar.setApplicationContext(context); - registrar.onApplicationReadyEvent( - new ApplicationReadyEvent(new SpringApplication(), null, mock(ConfigurableApplicationContext.class))); + registrar.onApplicationReadyEvent(new ApplicationReadyEvent(new SpringApplication(), null, + mock(ConfigurableApplicationContext.class), Duration.ZERO)); assertThat(isApplicationReady(registrar)).isFalse(); - registrar.onApplicationReadyEvent(new ApplicationReadyEvent(new SpringApplication(), null, context)); + registrar.onApplicationReadyEvent( + new ApplicationReadyEvent(new SpringApplication(), null, context, Duration.ZERO)); assertThat(isApplicationReady(registrar)).isTrue(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java index 3dbeec2647..23a66dcf85 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.context; import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; @@ -187,7 +188,7 @@ class ApplicationPidFileWriterTests { ConfigurableEnvironment environment = createEnvironment(propName, propValue); ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); given(context.getEnvironment()).willReturn(environment); - return new ApplicationReadyEvent(new SpringApplication(), new String[] {}, context); + return new ApplicationReadyEvent(new SpringApplication(), new String[] {}, context, Duration.ZERO); } private ConfigurableEnvironment createEnvironment(String propName, String propValue) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java index 5f922fbbd7..c6c5375d1a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.boot.context.event; +import java.time.Duration; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -72,9 +73,9 @@ class EventPublishingRunListenerTests { this.runListener.contextLoaded(context); checkApplicationEvents(ApplicationPreparedEvent.class); context.refresh(); - this.runListener.started(context); + this.runListener.started(context, Duration.ZERO); checkApplicationEvents(ApplicationStartedEvent.class, AvailabilityChangeEvent.class); - this.runListener.running(context); + this.runListener.running(context, Duration.ZERO); checkApplicationEvents(ApplicationReadyEvent.class, AvailabilityChangeEvent.class); } From c62a6819fe55e1126d796baee093ec641b5c0ce0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 16 Sep 2021 10:00:39 +0200 Subject: [PATCH 2/2] Polish "Add startup time metrics" See gh-27878 --- .../StartupTimeMetricsAutoConfiguration.java | 4 +- ...rtupTimeMetricsAutoConfigurationTests.java | 38 ++++----- .../JettyMetricsAutoConfigurationTests.java | 31 ++++---- .../TomcatMetricsAutoConfigurationTests.java | 12 +-- .../metrics/startup/StartupTimeMetrics.java | 54 +++++++++---- .../startup/StartupTimeMetricsTests.java | 51 +++++++----- .../RestartApplicationListenerTests.java | 3 +- .../src/docs/asciidoc/actuator/metrics.adoc | 11 +++ .../boot/SpringApplication.java | 9 ++- .../boot/SpringApplicationRunListener.java | 14 ++-- .../boot/StartupInfoLogger.java | 10 +-- .../context/event/ApplicationReadyEvent.java | 19 +++-- .../event/ApplicationStartedEvent.java | 15 ++-- .../event/EventPublishingRunListener.java | 20 +---- .../boot/SpringApplicationTests.java | 78 ++++++++----------- .../boot/StartupInfoLoggerTests.java | 3 +- ...gApplicationAdminMXBeanRegistrarTests.java | 6 +- .../ApplicationPidFileWriterTests.java | 3 +- .../EventPublishingRunListenerTests.java | 5 +- 19 files changed, 201 insertions(+), 185 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java index d98f3d51de..e30fe37790 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfiguration.java @@ -30,7 +30,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupTimeMetrics}. + * {@link EnableAutoConfiguration Auto-configuration} for startup time metrics. * * @author Chris Bono * @since 2.6.0 @@ -43,7 +43,7 @@ public class StartupTimeMetricsAutoConfiguration { @Bean @ConditionalOnMissingBean - StartupTimeMetrics startupTimeMetrics(MeterRegistry meterRegistry) { + public StartupTimeMetrics startupTimeMetrics(MeterRegistry meterRegistry) { return new StartupTimeMetrics(meterRegistry); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java index bb442f37cb..2dec9ec603 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/startup/StartupTimeMetricsAutoConfigurationTests.java @@ -17,8 +17,9 @@ package org.springframework.boot.actuate.autoconfigure.metrics.startup; import java.time.Duration; +import java.util.concurrent.TimeUnit; -import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; @@ -29,15 +30,15 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link StartupTimeMetricsAutoConfiguration}. * * @author Chris Bono + * @author Stephane Nicoll */ class StartupTimeMetricsAutoConfigurationTests { @@ -47,14 +48,18 @@ class StartupTimeMetricsAutoConfigurationTests { @Test void startupTimeMetricsAreRecorded() { this.contextRunner.run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ofMillis(2500))); - context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ofMillis(3000))); assertThat(context).hasSingleBean(StartupTimeMetrics.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); - assertThat(registry.find("application.started.time").timeGauge()).isNotNull(); - assertThat(registry.find("application.ready.time").timeGauge()).isNotNull(); + context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(1500))); + TimeGauge startedTimeGage = registry.find("application.started.time").timeGauge(); + assertThat(startedTimeGage).isNotNull(); + assertThat(startedTimeGage.value(TimeUnit.MILLISECONDS)).isEqualTo(1500L); + context.publishEvent(new ApplicationReadyEvent(new SpringApplication(), null, + context.getSourceApplicationContext(), Duration.ofMillis(2000))); + TimeGauge readyTimeGage = registry.find("application.ready.time").timeGauge(); + assertThat(readyTimeGage).isNotNull(); + assertThat(readyTimeGage.value(TimeUnit.MILLISECONDS)).isEqualTo(2000L); }); } @@ -74,19 +79,10 @@ class StartupTimeMetricsAutoConfigurationTests { @Test void customStartupTimeMetricsAreRespected() { - this.contextRunner.withUserConfiguration(CustomStartupTimeMetricsConfiguration.class) + this.contextRunner + .withBean("customStartupTimeMetrics", StartupTimeMetrics.class, () -> mock(StartupTimeMetrics.class)) .run((context) -> assertThat(context).hasSingleBean(StartupTimeMetrics.class) - .hasBean("customStartTimeMetrics")); - } - - @Configuration(proxyBeanMethods = false) - static class CustomStartupTimeMetricsConfiguration { - - @Bean - StartupTimeMetrics customStartTimeMetrics() { - return new StartupTimeMetrics(new SimpleMeterRegistry(), Tags.empty(), "myapp.started", "myapp.ready"); - } - + .hasBean("customStartupTimeMetrics")); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java index 7a8f84c2a3..8d377317b2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.jetty; -import java.time.Duration; - import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -37,6 +35,7 @@ import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.HttpHandler; @@ -59,8 +58,7 @@ class JettyMetricsAutoConfigurationTests { ServletWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(JettyServerThreadPoolMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull(); @@ -74,8 +72,7 @@ class JettyMetricsAutoConfigurationTests { ReactiveWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.threads.config.min").meter()).isNotNull(); }); @@ -96,8 +93,7 @@ class JettyMetricsAutoConfigurationTests { ServletWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull(); @@ -111,8 +107,7 @@ class JettyMetricsAutoConfigurationTests { ReactiveWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.connections.messages.in").meter()).isNotNull(); }); @@ -126,8 +121,7 @@ class JettyMetricsAutoConfigurationTests { .withUserConfiguration(ServletWebServerConfiguration.class, CustomJettyConnectionMetricsBinder.class, MeterRegistryConfiguration.class) .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(JettyConnectionMetricsBinder.class) .hasBean("customJettyConnectionMetricsBinder"); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); @@ -145,8 +139,7 @@ class JettyMetricsAutoConfigurationTests { .withPropertyValues("server.ssl.enabled: true", "server.ssl.key-store: src/test/resources/test.jks", "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull(); @@ -162,8 +155,7 @@ class JettyMetricsAutoConfigurationTests { .withPropertyValues("server.ssl.enabled: true", "server.ssl.key-store: src/test/resources/test.jks", "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("jetty.ssl.handshakes").meter()).isNotNull(); }); @@ -179,8 +171,7 @@ class JettyMetricsAutoConfigurationTests { .withPropertyValues("server.ssl.enabled: true", "server.ssl.key-store: src/test/resources/test.jks", "server.ssl.key-store-password: secret", "server.ssl.key-password: password") .run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(JettySslHandshakeMetricsBinder.class) .hasBean("customJettySslHandshakeMetricsBinder"); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); @@ -215,6 +206,10 @@ class JettyMetricsAutoConfigurationTests { .run((context) -> assertThat(context).doesNotHaveBean(JettySslHandshakeMetricsBinder.class)); } + private ApplicationStartedEvent createApplicationStartedEvent(ConfigurableApplicationContext context) { + return new ApplicationStartedEvent(new SpringApplication(), null, context, null); + } + @Configuration(proxyBeanMethods = false) static class MeterRegistryConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java index 017a9cbf18..e95cda900a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/tomcat/TomcatMetricsAutoConfigurationTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat; -import java.time.Duration; import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; @@ -39,6 +38,7 @@ import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactor import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.HttpHandler; @@ -62,8 +62,7 @@ class TomcatMetricsAutoConfigurationTests { ServletWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ServletWebServerConfiguration.class, MeterRegistryConfiguration.class) .withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); assertThat(context).hasSingleBean(TomcatMetricsBinder.class); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull(); @@ -79,8 +78,7 @@ class TomcatMetricsAutoConfigurationTests { ReactiveWebServerFactoryAutoConfiguration.class)) .withUserConfiguration(ReactiveWebServerConfiguration.class, MeterRegistryConfiguration.class) .withPropertyValues("server.tomcat.mbeanregistry.enabled=true").run((context) -> { - context.publishEvent(new ApplicationStartedEvent(new SpringApplication(), null, - context.getSourceApplicationContext(), Duration.ZERO)); + context.publishEvent(createApplicationStartedEvent(context.getSourceApplicationContext())); SimpleMeterRegistry registry = context.getBean(SimpleMeterRegistry.class); assertThat(registry.find("tomcat.sessions.active.max").meter()).isNotNull(); assertThat(registry.find("tomcat.threads.current").meter()).isNotNull(); @@ -110,6 +108,10 @@ class TomcatMetricsAutoConfigurationTests { .hasBean("customTomcatMetrics")); } + private ApplicationStartedEvent createApplicationStartedEvent(ConfigurableApplicationContext context) { + return new ApplicationStartedEvent(new SpringApplication(), null, context, null); + } + private void resetTomcatState() { ReflectionTestUtils.setField(Registry.class, "registry", null); AtomicInteger containerCounter = (AtomicInteger) ReflectionTestUtils.getField(TomcatWebServer.class, diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java index 4356d0c757..7885f075ae 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetrics.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.metrics.startup; +import java.time.Duration; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -39,6 +40,16 @@ import org.springframework.context.event.SmartApplicationListener; */ public class StartupTimeMetrics implements SmartApplicationListener { + /** + * The default name to use for the application started time metric. + */ + public static final String APPLICATION_STARTED_TIME_METRIC_NAME = "application.started.time"; + + /** + * The default name to use for the application ready time metric. + */ + public static final String APPLICATION_READY_TIME_METRIC_NAME = "application.ready.time"; + private final MeterRegistry meterRegistry; private final String applicationStartedTimeMetricName; @@ -47,10 +58,26 @@ public class StartupTimeMetrics implements SmartApplicationListener { private final Iterable tags; + /** + * Create a new instance using default metric names. + * @param meterRegistry the registry to use + * @see #APPLICATION_STARTED_TIME_METRIC_NAME + * @see #APPLICATION_READY_TIME_METRIC_NAME + */ public StartupTimeMetrics(MeterRegistry meterRegistry) { - this(meterRegistry, Collections.emptyList(), "application.started.time", "application.ready.time"); + this(meterRegistry, Collections.emptyList(), APPLICATION_STARTED_TIME_METRIC_NAME, + APPLICATION_READY_TIME_METRIC_NAME); } + /** + * Create a new instance using the specified options. + * @param meterRegistry the registry to use + * @param tags the tags to associate to application startup metrics + * @param applicationStartedTimeMetricName the name to use for the application started + * time metric + * @param applicationReadyTimeMetricName the name to use for the application ready + * time metric + */ public StartupTimeMetrics(MeterRegistry meterRegistry, Iterable tags, String applicationStartedTimeMetricName, String applicationReadyTimeMetricName) { this.meterRegistry = meterRegistry; @@ -76,29 +103,28 @@ public class StartupTimeMetrics implements SmartApplicationListener { } private void onApplicationStarted(ApplicationStartedEvent event) { - if (event.getStartupTime() == null) { + if (event.getStartedTime() == null) { return; } - TimeGauge - .builder(this.applicationStartedTimeMetricName, () -> event.getStartupTime().toMillis(), - TimeUnit.MILLISECONDS) - .tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication())) - .description("Time taken (ms) to start the application").register(this.meterRegistry); + registerGauge(this.applicationStartedTimeMetricName, "Time taken (ms) to start the application", + event.getStartedTime(), createTagsFrom(event.getSpringApplication())); } private void onApplicationReady(ApplicationReadyEvent event) { - if (event.getStartupTime() == null) { + if (event.getReadyTime() == null) { return; } - TimeGauge - .builder(this.applicationReadyTimeMetricName, () -> event.getStartupTime().toMillis(), - TimeUnit.MILLISECONDS) - .tags(maybeDcorateTagsWithApplicationInfo(event.getSpringApplication())) - .description("Time taken (ms) for the application to be ready to serve requests") + registerGauge(this.applicationReadyTimeMetricName, + "Time taken (ms) for the application to be ready to serve requests", event.getReadyTime(), + createTagsFrom(event.getSpringApplication())); + } + + private void registerGauge(String metricName, String description, Duration time, Iterable tags) { + TimeGauge.builder(metricName, time::toMillis, TimeUnit.MILLISECONDS).tags(tags).description(description) .register(this.meterRegistry); } - private Iterable maybeDcorateTagsWithApplicationInfo(SpringApplication springApplication) { + private Iterable createTagsFrom(SpringApplication springApplication) { Class mainClass = springApplication.getMainApplicationClass(); if (mainClass == null) { return this.tags; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java index d6ef5f3855..e17eae1fc3 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/startup/StartupTimeMetricsTests.java @@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,44 +41,52 @@ import static org.mockito.Mockito.mock; */ class StartupTimeMetricsTests { - private static final long APP_STARTED_TIME_MS = 2500; - - private static final long APP_RUNNING_TIME_MS = 2900; - private MeterRegistry registry; private StartupTimeMetrics metrics; @BeforeEach - void prepareUnit() { + void setup() { this.registry = new SimpleMeterRegistry(); this.metrics = new StartupTimeMetrics(this.registry); } @Test void metricsRecordedWithoutCustomTags() { - this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); - this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); - assertMetricExistsWithValue("application.started.time", APP_STARTED_TIME_MS); - assertMetricExistsWithValue("application.ready.time", APP_RUNNING_TIME_MS); + this.metrics.onApplicationEvent(applicationStartedEvent(2000L)); + this.metrics.onApplicationEvent(applicationReadyEvent(2200L)); + assertMetricExistsWithValue("application.started.time", 2000L); + assertMetricExistsWithValue("application.ready.time", 2200L); } @Test void metricsRecordedWithCustomTagsAndMetricNames() { Tags tags = Tags.of("foo", "bar"); this.metrics = new StartupTimeMetrics(this.registry, tags, "m1", "m2"); - this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); - this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); - assertMetricExistsWithCustomTagsAndValue("m1", tags, APP_STARTED_TIME_MS); - assertMetricExistsWithCustomTagsAndValue("m2", tags, APP_RUNNING_TIME_MS); + this.metrics.onApplicationEvent(applicationStartedEvent(1000L)); + this.metrics.onApplicationEvent(applicationReadyEvent(1050L)); + assertMetricExistsWithCustomTagsAndValue("m1", tags, 1000L); + assertMetricExistsWithCustomTagsAndValue("m2", tags, 1050L); + } + + @Test + void metricRecordedWithoutMainAppClassTag() { + SpringApplication application = mock(SpringApplication.class); + this.metrics.onApplicationEvent(new ApplicationStartedEvent(application, null, null, Duration.ofSeconds(2))); + TimeGauge applicationStartedGague = this.registry.find("application.started.time").timeGauge(); + assertThat(applicationStartedGague).isNotNull(); + assertThat(applicationStartedGague.getId().getTags()).isEmpty(); } @Test - void metricsRecordedWithoutMainAppClassTagWhenMainAppClassNotAvailable() { - this.metrics.onApplicationEvent(applicationStartedEvent(APP_STARTED_TIME_MS)); - this.metrics.onApplicationEvent(applicationReadyEvent(APP_RUNNING_TIME_MS)); - assertThat(this.registry.find("application.started.time").timeGauge()).isNotNull(); - assertThat(this.registry.find("application.ready.time").timeGauge()).isNotNull(); + void metricRecordedWithoutMainAppClassTagAndAdditionalTags() { + SpringApplication application = mock(SpringApplication.class); + Tags tags = Tags.of("foo", "bar"); + this.metrics = new StartupTimeMetrics(this.registry, tags, "started", "ready"); + this.metrics.onApplicationEvent(new ApplicationReadyEvent(application, null, null, Duration.ofSeconds(2))); + TimeGauge applicationReadyGague = this.registry.find("ready").timeGauge(); + assertThat(applicationReadyGague).isNotNull(); + assertThat(applicationReadyGague.getId().getTags()).containsExactlyElementsOf(tags); } @Test @@ -102,16 +111,16 @@ class StartupTimeMetricsTests { (startupTimeMs != null) ? Duration.ofMillis(startupTimeMs) : null); } - private void assertMetricExistsWithValue(String metricName, double expectedValueInMillis) { + private void assertMetricExistsWithValue(String metricName, long expectedValueInMillis) { assertMetricExistsWithCustomTagsAndValue(metricName, Tags.empty(), expectedValueInMillis); } private void assertMetricExistsWithCustomTagsAndValue(String metricName, Tags expectedCustomTags, - double expectedValueInMillis) { + Long expectedValueInMillis) { assertThat(this.registry.find(metricName) .tags(Tags.concat(expectedCustomTags, "main-application-class", TestMainApplication.class.getName())) .timeGauge()).isNotNull().extracting((m) -> m.value(TimeUnit.MILLISECONDS)) - .isEqualTo(expectedValueInMillis); + .isEqualTo(expectedValueInMillis.doubleValue()); } static class TestMainApplication { diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java index 7f56ede403..40348ca7f1 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.devtools.restart; -import java.time.Duration; import java.util.List; import org.junit.jupiter.api.AfterEach; @@ -111,7 +110,7 @@ class RestartApplicationListenerTests { listener.onApplicationEvent(new ApplicationFailedEvent(application, ARGS, context, new RuntimeException())); } else { - listener.onApplicationEvent(new ApplicationReadyEvent(application, ARGS, context, Duration.ZERO)); + listener.onApplicationEvent(new ApplicationReadyEvent(application, ARGS, context, null)); } } 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 21d637569d..131aa77017 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 @@ -656,6 +656,17 @@ The following system metrics are provided: +[[actuator.metrics.supported.application-startup]] +==== Application Startup Metrics +Auto-configuration exposes application startup time metrics: + +* `application.started.time`: time taken to start the application. +* `application.ready.time`: time taken for the application to be ready to serve requests. + +Metrics are tagged by the fully qualified name of the application class. + + + [[actuator.metrics.supported.logger]] ==== Logger Metrics Auto-configuration enables the event metrics for both Logback and Log4J2. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 2e430f050f..63be21dac1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -287,7 +287,7 @@ public class SpringApplication { */ public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); - stopWatch.start("applicationStarted"); + stopWatch.start(); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; configureHeadlessProperty(); @@ -304,11 +304,12 @@ public class SpringApplication { refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); - stopWatch.start("applicationReady"); + Duration startedTime = Duration.ofMillis(stopWatch.getTotalTimeMillis()); + stopWatch.start(); if (this.logStartupInfo) { - new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); + new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startedTime); } - listeners.started(context, Duration.ofMillis(stopWatch.getTotalTimeMillis())); + listeners.started(context, startedTime); callRunners(context, applicationArguments); } catch (Throwable ex) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java index f142cd4594..c37659d256 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListener.java @@ -83,6 +83,7 @@ public interface SpringApplicationRunListener { */ @Deprecated default void started(ConfigurableApplicationContext context) { + started(context, null); } /** @@ -90,11 +91,11 @@ public interface SpringApplicationRunListener { * {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner * ApplicationRunners} have not been called. * @param context the application context. - * @param startupTime the time taken to start the application or {@code null} if + * @param startedTime the time taken to start the application or {@code null} if * unknown - * @since 2.0.0 + * @since 2.6.0 */ - default void started(ConfigurableApplicationContext context, Duration startupTime) { + default void started(ConfigurableApplicationContext context, Duration startedTime) { started(context); } @@ -109,6 +110,7 @@ public interface SpringApplicationRunListener { */ @Deprecated default void running(ConfigurableApplicationContext context) { + running(context, null); } /** @@ -116,11 +118,11 @@ public interface SpringApplicationRunListener { * been refreshed and all {@link CommandLineRunner CommandLineRunners} and * {@link ApplicationRunner ApplicationRunners} have been called. * @param context the application context. - * @param startupTime the time taken for the application to be ready to service - * requests or {@code null} if unknown + * @param readyTime the time taken for the application to be ready to service requests + * or {@code null} if unknown * @since 2.6.0 */ - default void running(ConfigurableApplicationContext context, Duration startupTime) { + default void running(ConfigurableApplicationContext context, Duration readyTime) { running(context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java index b6ac1ad9b2..22f9559f98 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java @@ -18,6 +18,7 @@ package org.springframework.boot; import java.lang.management.ManagementFactory; import java.net.InetAddress; +import java.time.Duration; import java.util.concurrent.Callable; import org.apache.commons.logging.Log; @@ -29,7 +30,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.log.LogMessage; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; /** @@ -56,9 +56,9 @@ class StartupInfoLogger { applicationLog.debug(LogMessage.of(this::getRunningMessage)); } - void logStarted(Log applicationLog, StopWatch stopWatch) { + void logStarted(Log applicationLog, Duration startupTime) { if (applicationLog.isInfoEnabled()) { - applicationLog.info(getStartedMessage(stopWatch)); + applicationLog.info(getStartedMessage(startupTime)); } } @@ -83,12 +83,12 @@ class StartupInfoLogger { return message; } - private CharSequence getStartedMessage(StopWatch stopWatch) { + private CharSequence getStartedMessage(Duration startupTime) { StringBuilder message = new StringBuilder(); message.append("Started "); appendApplicationName(message); message.append(" in "); - message.append(stopWatch.getTotalTimeMillis() / 1000.0); + message.append(startupTime.toMillis() / 1000.0); message.append(" seconds"); try { double uptime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java index d16d72ee78..754d167ca4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationReadyEvent.java @@ -37,7 +37,7 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { private final ConfigurableApplicationContext context; - private final Duration startupTime; + private final Duration readyTime; /** * Create a new {@link ApplicationReadyEvent} instance. @@ -47,6 +47,7 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { * @deprecated since 2.6.0 for removal in 2.8.0 in favor of * {@link #ApplicationReadyEvent(SpringApplication, String[], ConfigurableApplicationContext, Duration)} */ + @Deprecated public ApplicationReadyEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context) { this(application, args, context, null); } @@ -56,13 +57,14 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { * @param application the current application * @param args the arguments the application is running with * @param context the context that was being created - * @param startupTime the time taken to get the application ready to service requests + * @param readyTime the time taken to get the application ready to service requests + * @since 2.6.0 */ public ApplicationReadyEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context, - Duration startupTime) { + Duration readyTime) { super(application, args); this.context = context; - this.startupTime = startupTime; + this.readyTime = readyTime; } /** @@ -74,11 +76,12 @@ public class ApplicationReadyEvent extends SpringApplicationEvent { } /** - * Return the time taken for the application to be ready to service requests. - * @return the startup time + * Return the time taken for the application to be ready to service requests, or + * {@code null} if unknown. + * @return the time taken to be ready to service requests */ - public Duration getStartupTime() { - return this.startupTime; + public Duration getReadyTime() { + return this.readyTime; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java index 7179a5edf2..08bf17aab0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/ApplicationStartedEvent.java @@ -36,7 +36,7 @@ public class ApplicationStartedEvent extends SpringApplicationEvent { private final ConfigurableApplicationContext context; - private final Duration startupTime; + private final Duration startedTime; /** * Create a new {@link ApplicationStartedEvent} instance. @@ -57,13 +57,14 @@ public class ApplicationStartedEvent extends SpringApplicationEvent { * @param application the current application * @param args the arguments the application is running with * @param context the context that was being created - * @param startupTime the time taken to start the application + * @param startedTime the time taken to start the application + * @since 2.6.0 */ public ApplicationStartedEvent(SpringApplication application, String[] args, ConfigurableApplicationContext context, - Duration startupTime) { + Duration startedTime) { super(application, args); this.context = context; - this.startupTime = startupTime; + this.startedTime = startedTime; } /** @@ -75,11 +76,11 @@ public class ApplicationStartedEvent extends SpringApplicationEvent { } /** - * Return the time taken to start the application. + * Return the time taken to start the application, or {@code null} if unknown. * @return the startup time */ - public Duration getStartupTime() { - return this.startupTime; + public Duration getStartedTime() { + return this.startedTime; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java index 2bcbbc1ce3..3fc39b628a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/event/EventPublishingRunListener.java @@ -104,26 +104,14 @@ public class EventPublishingRunListener implements SpringApplicationRunListener, } @Override - @Deprecated - public void started(ConfigurableApplicationContext context) { - started(context, null); - } - - @Override - public void started(ConfigurableApplicationContext context, Duration startupTime) { - context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, startupTime)); + public void started(ConfigurableApplicationContext context, Duration startedTime) { + context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, startedTime)); AvailabilityChangeEvent.publish(context, LivenessState.CORRECT); } @Override - @Deprecated - public void running(ConfigurableApplicationContext context) { - running(context, null); - } - - @Override - public void running(ConfigurableApplicationContext context, Duration startupTime) { - context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, startupTime)); + public void running(ConfigurableApplicationContext context, Duration readyTime) { + context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, readyTime)); AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 0ba56fa5d5..a497b70a49 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -88,6 +88,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.context.event.SmartApplicationListener; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; @@ -362,36 +363,18 @@ class SpringApplicationTests { void applicationRunningEventListener() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - final AtomicReference reference = new AtomicReference<>(); - class ApplicationReadyEventListener implements ApplicationListener { - - @Override - public void onApplicationEvent(ApplicationReadyEvent event) { - reference.set(event.getSpringApplication()); - } - - } - application.addListeners(new ApplicationReadyEventListener()); + AtomicReference reference = setupListener(application, ApplicationReadyEvent.class); this.context = application.run("--foo=bar"); - assertThat(application).isSameAs(reference.get()); + assertThat(application).isSameAs(reference.get().getSpringApplication()); } @Test void contextRefreshedEventListener() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - final AtomicReference reference = new AtomicReference<>(); - class InitializerListener implements ApplicationListener { - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - reference.set(event.getApplicationContext()); - } - - } - application.setListeners(Collections.singletonList(new InitializerListener())); + AtomicReference reference = setupListener(application, ContextRefreshedEvent.class); this.context = application.run("--foo=bar"); - assertThat(this.context).isSameAs(reference.get()); + assertThat(this.context).isSameAs(reference.get().getApplicationContext()); // Custom initializers do not switch off the defaults assertThat(getEnvironment().getProperty("foo")).isEqualTo("bar"); } @@ -419,39 +402,21 @@ class SpringApplicationTests { } @Test - void applicationStartedEventHasStartupTime() { + void applicationStartedEventHasStartedTime() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - final AtomicReference reference = new AtomicReference<>(); - class ApplicationStartedEventListener implements ApplicationListener { - - @Override - public void onApplicationEvent(ApplicationStartedEvent event) { - reference.set(event); - } - - } - application.addListeners(new ApplicationStartedEventListener()); + AtomicReference reference = setupListener(application, ApplicationStartedEvent.class); this.context = application.run(); - assertThat(reference.get()).isNotNull().extracting(ApplicationStartedEvent::getStartupTime).isNotNull(); + assertThat(reference.get()).isNotNull().extracting(ApplicationStartedEvent::getStartedTime).isNotNull(); } @Test - void applicationReadyEventHasStartupTime() { + void applicationReadyEventHasReadyTime() { SpringApplication application = new SpringApplication(ExampleConfig.class); application.setWebApplicationType(WebApplicationType.NONE); - final AtomicReference reference = new AtomicReference<>(); - class ApplicationReadyEventListener implements ApplicationListener { - - @Override - public void onApplicationEvent(ApplicationReadyEvent event) { - reference.set(event); - } - - } - application.addListeners(new ApplicationReadyEventListener()); + AtomicReference reference = setupListener(application, ApplicationReadyEvent.class); this.context = application.run(); - assertThat(reference.get()).isNotNull().extracting(ApplicationReadyEvent::getStartupTime).isNotNull(); + assertThat(reference.get()).isNotNull().extracting(ApplicationReadyEvent::getReadyTime).isNotNull(); } @Test @@ -1287,6 +1252,27 @@ class SpringApplicationTests { && ((AvailabilityChangeEvent) argument).getState().equals(state); } + private AtomicReference setupListener(SpringApplication application, + Class targetEventType) { + final AtomicReference reference = new AtomicReference<>(); + class TestEventListener implements SmartApplicationListener { + + @Override + @SuppressWarnings("unchecked") + public void onApplicationEvent(ApplicationEvent event) { + reference.set((T) event); + } + + @Override + public boolean supportsEventType(Class eventType) { + return targetEventType.isAssignableFrom(eventType); + } + + } + application.addListeners(new TestEventListener()); + return reference; + } + private Condition matchingPropertySource(final Class propertySourceClass, final String name) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java index 7649697704..ba41107905 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java @@ -18,6 +18,7 @@ package org.springframework.boot; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Duration; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; @@ -59,7 +60,7 @@ class StartupInfoLoggerTests { stopWatch.start(); given(this.log.isInfoEnabled()).willReturn(true); stopWatch.stop(); - new StartupInfoLogger(getClass()).logStarted(this.log, stopWatch); + new StartupInfoLogger(getClass()).logStarted(this.log, Duration.ofMillis(stopWatch.getTotalTimeMillis())); ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); verify(this.log).info(captor.capture()); assertThat(captor.getValue().toString()).matches("Started " + getClass().getSimpleName() diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java index 2a76cb2abe..cc18ba2561 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/admin/SpringApplicationAdminMXBeanRegistrarTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.admin; import java.lang.management.ManagementFactory; -import java.time.Duration; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; @@ -90,10 +89,9 @@ class SpringApplicationAdminMXBeanRegistrarTests { ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); registrar.setApplicationContext(context); registrar.onApplicationReadyEvent(new ApplicationReadyEvent(new SpringApplication(), null, - mock(ConfigurableApplicationContext.class), Duration.ZERO)); + mock(ConfigurableApplicationContext.class), null)); assertThat(isApplicationReady(registrar)).isFalse(); - registrar.onApplicationReadyEvent( - new ApplicationReadyEvent(new SpringApplication(), null, context, Duration.ZERO)); + registrar.onApplicationReadyEvent(new ApplicationReadyEvent(new SpringApplication(), null, context, null)); assertThat(isApplicationReady(registrar)).isTrue(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java index 23a66dcf85..396e7b1d65 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/ApplicationPidFileWriterTests.java @@ -18,7 +18,6 @@ package org.springframework.boot.context; import java.io.File; import java.io.IOException; -import java.time.Duration; import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; @@ -188,7 +187,7 @@ class ApplicationPidFileWriterTests { ConfigurableEnvironment environment = createEnvironment(propName, propValue); ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); given(context.getEnvironment()).willReturn(environment); - return new ApplicationReadyEvent(new SpringApplication(), new String[] {}, context, Duration.ZERO); + return new ApplicationReadyEvent(new SpringApplication(), new String[] {}, context, null); } private ConfigurableEnvironment createEnvironment(String propName, String propValue) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java index c6c5375d1a..263e17f7e9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/event/EventPublishingRunListenerTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.context.event; -import java.time.Duration; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -73,9 +72,9 @@ class EventPublishingRunListenerTests { this.runListener.contextLoaded(context); checkApplicationEvents(ApplicationPreparedEvent.class); context.refresh(); - this.runListener.started(context, Duration.ZERO); + this.runListener.started(context, null); checkApplicationEvents(ApplicationStartedEvent.class, AvailabilityChangeEvent.class); - this.runListener.running(context, Duration.ZERO); + this.runListener.running(context, null); checkApplicationEvents(ApplicationReadyEvent.class, AvailabilityChangeEvent.class); }