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
This commit is contained in:
Brian Clozel
2020-08-06 12:04:42 +02:00
parent 5cc5baa303
commit 9f558181d5
6 changed files with 96 additions and 8 deletions

View File

@@ -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 <T> Collection<T> getSpringFactoriesInstances(Class<T> 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.

View File

@@ -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<SpringApplicationRunListener> listeners;
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
private final ApplicationStartup applicationStartup;
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> 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,

View File

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

View File

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

View File

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

View File

@@ -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.<Supplier<String>>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 <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
S state) {
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)