Add startup time metrics

See gh-27878
This commit is contained in:
bono007 2021-09-05 21:05:50 -05:00 committed by Stephane Nicoll
parent 32cfde074f
commit 2e67963bfe
20 changed files with 601 additions and 30 deletions

View File

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

View File

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

View File

@ -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,\

View File

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

View File

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

View File

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

View File

@ -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<Tag> tags;
public StartupTimeMetrics(MeterRegistry meterRegistry) {
this(meterRegistry, Collections.emptyList(), "application.started.time", "application.ready.time");
}
public StartupTimeMetrics(MeterRegistry meterRegistry, Iterable<Tag> 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<? extends ApplicationEvent> 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<Tag> maybeDcorateTagsWithApplicationInfo(SpringApplication springApplication) {
Class<?> mainClass = springApplication.getMainApplicationClass();
if (mainClass == null) {
return this.tags;
}
return Tags.concat(this.tags, "main-application-class", mainClass.getName());
}
}

View File

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

View File

@ -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 {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ApplicationStartedEvent> reference = new AtomicReference<>();
class ApplicationStartedEventListener implements ApplicationListener<ApplicationStartedEvent> {
@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<ApplicationReadyEvent> reference = new AtomicReference<>();
class ApplicationReadyEventListener implements ApplicationListener<ApplicationReadyEvent> {
@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);

View File

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

View File

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

View File

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