From 9f558181d5072aaf9b98860835923abfad1d4f13 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 6 Aug 2020 12:04:42 +0200 Subject: [PATCH] Instrument SpringBootApplication for ApplicationStartup This commit allows the configuration of a custom `ApplicationStartup` implementation on the `SpringApplication` and `SpringApplicationBuilder` for collecting `StartupStep` metrics. This also instruments Spring Boot run listeners and server-specific application context implementations for collecting Spring Boot application events during startup. Closes gh-22600 --- .../boot/SpringApplication.java | 23 +++++++++++- .../boot/SpringApplicationRunListeners.java | 24 +++++++++++-- .../builder/SpringApplicationBuilder.java | 13 +++++++ .../ReactiveWebServerApplicationContext.java | 4 +++ .../ServletWebServerApplicationContext.java | 4 +++ .../boot/SpringApplicationTests.java | 36 ++++++++++++++++--- 6 files changed, 96 insertions(+), 8 deletions(-) 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 a5210f0961..84b5eaeb79 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 @@ -75,6 +75,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.metrics.ApplicationStartup; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -246,6 +247,8 @@ public class SpringApplication { private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT; + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} @@ -316,6 +319,7 @@ public class SpringApplication { configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); + context.setApplicationStartup(this.applicationStartup); exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); prepareContext(context, environment, listeners, applicationArguments, printedBanner); @@ -422,7 +426,8 @@ public class SpringApplication { private SpringApplicationRunListeners getRunListeners(String[] args) { Class[] types = new Class[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, - getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); + getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), + this.applicationStartup); } private Collection getSpringFactoriesInstances(Class type) { @@ -1237,6 +1242,22 @@ public class SpringApplication { return asUnmodifiableOrderedSet(this.listeners); } + /** + * Set the {@link ApplicationStartup} to use for collecting startup metrics. + * @param applicationStartup the application startup to use + */ + public void setApplicationStartup(ApplicationStartup applicationStartup) { + this.applicationStartup = (applicationStartup != null) ? applicationStartup : ApplicationStartup.DEFAULT; + } + + /** + * Returns the {@link ApplicationStartup} used for collecting startup metrics. + * @return the application startup + */ + public ApplicationStartup getApplicationStartup() { + return this.applicationStartup; + } + /** * Static helper that can be used to run a {@link SpringApplication} from the * specified source using default settings. 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 55f520dac8..7dc6ebebfd 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-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -24,6 +24,8 @@ import org.apache.commons.logging.Log; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.util.ReflectionUtils; /** @@ -37,51 +39,69 @@ class SpringApplicationRunListeners { private final List listeners; - SpringApplicationRunListeners(Log log, Collection listeners) { + private final ApplicationStartup applicationStartup; + + SpringApplicationRunListeners(Log log, Collection listeners, + ApplicationStartup applicationStartup) { this.log = log; this.listeners = new ArrayList<>(listeners); + this.applicationStartup = applicationStartup; } void starting() { + StartupStep starting = this.applicationStartup.start("spring.boot.application.starting"); for (SpringApplicationRunListener listener : this.listeners) { listener.starting(); } + starting.end(); } void environmentPrepared(ConfigurableEnvironment environment) { + StartupStep environmentPrepared = this.applicationStartup.start("spring.boot.application.environment-prepared"); for (SpringApplicationRunListener listener : this.listeners) { listener.environmentPrepared(environment); } + environmentPrepared.end(); } void contextPrepared(ConfigurableApplicationContext context) { + StartupStep contextPrepared = this.applicationStartup.start("spring.boot.application.context-prepared"); for (SpringApplicationRunListener listener : this.listeners) { listener.contextPrepared(context); } + contextPrepared.end(); } void contextLoaded(ConfigurableApplicationContext context) { + StartupStep contextLoaded = this.applicationStartup.start("spring.boot.application.context-loaded"); for (SpringApplicationRunListener listener : this.listeners) { listener.contextLoaded(context); } + contextLoaded.end(); } void started(ConfigurableApplicationContext context) { + StartupStep started = this.applicationStartup.start("spring.boot.application.started"); for (SpringApplicationRunListener listener : this.listeners) { listener.started(context); } + started.end(); } void running(ConfigurableApplicationContext context) { + StartupStep running = this.applicationStartup.start("spring.boot.application.running"); for (SpringApplicationRunListener listener : this.listeners) { listener.running(context); } + running.end(); } void failed(ConfigurableApplicationContext context, Throwable exception) { + StartupStep failed = this.applicationStartup.start("spring.boot.application.failed"); for (SpringApplicationRunListener listener : this.listeners) { callFailedListener(listener, context, exception); } + failed.tag("exception", exception.getClass().toString()).tag("message", exception.getMessage()).end(); } private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context, diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 26bbac7fd8..947d33ea17 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -40,6 +40,7 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.metrics.ApplicationStartup; import org.springframework.util.StringUtils; /** @@ -548,4 +549,16 @@ public class SpringApplicationBuilder { return this; } + /** + * Configure the {@link ApplicationStartup} to be used with the + * {@link ApplicationContext} for collecting startup metrics. + * @param applicationStartup the application startup to use + * @return the current builder + * @since 2.4.0 + */ + public SpringApplicationBuilder applicationStartup(ApplicationStartup applicationStartup) { + this.application.setApplicationStartup(applicationStartup); + return this; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java index 993c825656..942c42bbdb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java @@ -24,6 +24,7 @@ import org.springframework.boot.web.context.ConfigurableWebServerApplicationCont import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; import org.springframework.boot.web.server.WebServer; import org.springframework.context.ApplicationContextException; +import org.springframework.core.metrics.StartupStep; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.util.StringUtils; @@ -84,14 +85,17 @@ public class ReactiveWebServerApplicationContext extends GenericReactiveWebAppli private void createWebServer() { WebServerManager serverManager = this.serverManager; if (serverManager == null) { + StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create"); String webServerFactoryBeanName = getWebServerFactoryBeanName(); ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName); + createWebServer.tag("factory", webServerFactory.getClass().toString()); boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit(); this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit); getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.serverManager)); getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.serverManager)); + createWebServer.end(); } initPropertySources(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java index 8bbe5b0c91..5292ff7fd8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java @@ -49,6 +49,7 @@ import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; import org.springframework.core.io.Resource; +import org.springframework.core.metrics.StartupStep; import org.springframework.util.StringUtils; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.ServletContextAware; @@ -174,8 +175,11 @@ public class ServletWebServerApplicationContext extends GenericWebApplicationCon WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { + StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create"); ServletWebServerFactory factory = getWebServerFactory(); + createWebServer.tag("factory", factory.getClass().toString()); this.webServer = factory.getWebServer(getSelfInitializer()); + createWebServer.end(); getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer)); getBeanFactory().registerSingleton("webServerStartStop", 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 7caa863e2b..e48661124d 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 @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import javax.annotation.PostConstruct; @@ -100,6 +101,8 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.util.StringUtils; @@ -111,8 +114,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -679,13 +685,11 @@ class SpringApplicationTests { application.addListeners(eventListener); this.context = application.run(); InOrder applicationRunnerOrder = Mockito.inOrder(eventListener, applicationRunner); - applicationRunnerOrder.verify(applicationRunner).run(ArgumentMatchers.any(ApplicationArguments.class)); - applicationRunnerOrder.verify(eventListener) - .onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class)); + applicationRunnerOrder.verify(applicationRunner).run(any(ApplicationArguments.class)); + applicationRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class)); InOrder commandLineRunnerOrder = Mockito.inOrder(eventListener, commandLineRunner); commandLineRunnerOrder.verify(commandLineRunner).run(); - commandLineRunnerOrder.verify(eventListener) - .onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class)); + commandLineRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class)); } @Test @@ -1149,6 +1153,28 @@ class SpringApplicationTests { .getBean(AtomicInteger.class)).hasValue(1); } + @Test + void customApplicationStartupPublishStartupSteps() { + ApplicationStartup applicationStartup = mock(ApplicationStartup.class); + StartupStep startupStep = mock(StartupStep.class); + given(applicationStartup.start(anyString())).willReturn(startupStep); + given(startupStep.tag(anyString(), anyString())).willReturn(startupStep); + given(startupStep.tag(anyString(), ArgumentMatchers.>any())).willReturn(startupStep); + + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setApplicationStartup(applicationStartup); + this.context = application.run(); + + assertThat(this.context.getBean(ApplicationStartup.class)).isEqualTo(applicationStartup); + verify(applicationStartup).start("spring.boot.application.starting"); + verify(applicationStartup).start("spring.boot.application.environment-prepared"); + verify(applicationStartup).start("spring.boot.application.context-prepared"); + verify(applicationStartup).start("spring.boot.application.context-loaded"); + verify(applicationStartup).start("spring.boot.application.started"); + verify(applicationStartup).start("spring.boot.application.running"); + } + private ArgumentMatcher isAvailabilityChangeEventWithState( S state) { return (argument) -> (argument instanceof AvailabilityChangeEvent)