Skip to content

Commit 9f55818

Browse files
committed
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
1 parent 5cc5baa commit 9f55818

File tree

6 files changed

+96
-8
lines changed

6 files changed

+96
-8
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java

+22-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
import org.springframework.core.io.DefaultResourceLoader;
7676
import org.springframework.core.io.ResourceLoader;
7777
import org.springframework.core.io.support.SpringFactoriesLoader;
78+
import org.springframework.core.metrics.ApplicationStartup;
7879
import org.springframework.util.Assert;
7980
import org.springframework.util.ClassUtils;
8081
import org.springframework.util.CollectionUtils;
@@ -246,6 +247,8 @@ public class SpringApplication {
246247

247248
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;
248249

250+
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
251+
249252
/**
250253
* Create a new {@link SpringApplication} instance. The application context will load
251254
* beans from the specified primary sources (see {@link SpringApplication class-level}
@@ -316,6 +319,7 @@ public ConfigurableApplicationContext run(String... args) {
316319
configureIgnoreBeanInfo(environment);
317320
Banner printedBanner = printBanner(environment);
318321
context = createApplicationContext();
322+
context.setApplicationStartup(this.applicationStartup);
319323
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
320324
new Class<?>[] { ConfigurableApplicationContext.class }, context);
321325
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
@@ -422,7 +426,8 @@ private void configureHeadlessProperty() {
422426
private SpringApplicationRunListeners getRunListeners(String[] args) {
423427
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
424428
return new SpringApplicationRunListeners(logger,
425-
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
429+
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
430+
this.applicationStartup);
426431
}
427432

428433
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
@@ -1237,6 +1242,22 @@ public Set<ApplicationListener<?>> getListeners() {
12371242
return asUnmodifiableOrderedSet(this.listeners);
12381243
}
12391244

1245+
/**
1246+
* Set the {@link ApplicationStartup} to use for collecting startup metrics.
1247+
* @param applicationStartup the application startup to use
1248+
*/
1249+
public void setApplicationStartup(ApplicationStartup applicationStartup) {
1250+
this.applicationStartup = (applicationStartup != null) ? applicationStartup : ApplicationStartup.DEFAULT;
1251+
}
1252+
1253+
/**
1254+
* Returns the {@link ApplicationStartup} used for collecting startup metrics.
1255+
* @return the application startup
1256+
*/
1257+
public ApplicationStartup getApplicationStartup() {
1258+
return this.applicationStartup;
1259+
}
1260+
12401261
/**
12411262
* Static helper that can be used to run a {@link SpringApplication} from the
12421263
* specified source using default settings.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplicationRunListeners.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,8 @@
2424

2525
import org.springframework.context.ConfigurableApplicationContext;
2626
import org.springframework.core.env.ConfigurableEnvironment;
27+
import org.springframework.core.metrics.ApplicationStartup;
28+
import org.springframework.core.metrics.StartupStep;
2729
import org.springframework.util.ReflectionUtils;
2830

2931
/**
@@ -37,51 +39,69 @@ class SpringApplicationRunListeners {
3739

3840
private final List<SpringApplicationRunListener> listeners;
3941

40-
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
42+
private final ApplicationStartup applicationStartup;
43+
44+
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners,
45+
ApplicationStartup applicationStartup) {
4146
this.log = log;
4247
this.listeners = new ArrayList<>(listeners);
48+
this.applicationStartup = applicationStartup;
4349
}
4450

4551
void starting() {
52+
StartupStep starting = this.applicationStartup.start("spring.boot.application.starting");
4653
for (SpringApplicationRunListener listener : this.listeners) {
4754
listener.starting();
4855
}
56+
starting.end();
4957
}
5058

5159
void environmentPrepared(ConfigurableEnvironment environment) {
60+
StartupStep environmentPrepared = this.applicationStartup.start("spring.boot.application.environment-prepared");
5261
for (SpringApplicationRunListener listener : this.listeners) {
5362
listener.environmentPrepared(environment);
5463
}
64+
environmentPrepared.end();
5565
}
5666

5767
void contextPrepared(ConfigurableApplicationContext context) {
68+
StartupStep contextPrepared = this.applicationStartup.start("spring.boot.application.context-prepared");
5869
for (SpringApplicationRunListener listener : this.listeners) {
5970
listener.contextPrepared(context);
6071
}
72+
contextPrepared.end();
6173
}
6274

6375
void contextLoaded(ConfigurableApplicationContext context) {
76+
StartupStep contextLoaded = this.applicationStartup.start("spring.boot.application.context-loaded");
6477
for (SpringApplicationRunListener listener : this.listeners) {
6578
listener.contextLoaded(context);
6679
}
80+
contextLoaded.end();
6781
}
6882

6983
void started(ConfigurableApplicationContext context) {
84+
StartupStep started = this.applicationStartup.start("spring.boot.application.started");
7085
for (SpringApplicationRunListener listener : this.listeners) {
7186
listener.started(context);
7287
}
88+
started.end();
7389
}
7490

7591
void running(ConfigurableApplicationContext context) {
92+
StartupStep running = this.applicationStartup.start("spring.boot.application.running");
7693
for (SpringApplicationRunListener listener : this.listeners) {
7794
listener.running(context);
7895
}
96+
running.end();
7997
}
8098

8199
void failed(ConfigurableApplicationContext context, Throwable exception) {
100+
StartupStep failed = this.applicationStartup.start("spring.boot.application.failed");
82101
for (SpringApplicationRunListener listener : this.listeners) {
83102
callFailedListener(listener, context, exception);
84103
}
104+
failed.tag("exception", exception.getClass().toString()).tag("message", exception.getMessage()).end();
85105
}
86106

87107
private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java

+13
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.core.env.ConfigurableEnvironment;
4141
import org.springframework.core.env.Environment;
4242
import org.springframework.core.io.ResourceLoader;
43+
import org.springframework.core.metrics.ApplicationStartup;
4344
import org.springframework.util.StringUtils;
4445

4546
/**
@@ -548,4 +549,16 @@ public SpringApplicationBuilder listeners(ApplicationListener<?>... listeners) {
548549
return this;
549550
}
550551

552+
/**
553+
* Configure the {@link ApplicationStartup} to be used with the
554+
* {@link ApplicationContext} for collecting startup metrics.
555+
* @param applicationStartup the application startup to use
556+
* @return the current builder
557+
* @since 2.4.0
558+
*/
559+
public SpringApplicationBuilder applicationStartup(ApplicationStartup applicationStartup) {
560+
this.application.setApplicationStartup(applicationStartup);
561+
return this;
562+
}
563+
551564
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
2525
import org.springframework.boot.web.server.WebServer;
2626
import org.springframework.context.ApplicationContextException;
27+
import org.springframework.core.metrics.StartupStep;
2728
import org.springframework.http.server.reactive.HttpHandler;
2829
import org.springframework.util.StringUtils;
2930

@@ -84,14 +85,17 @@ protected void onRefresh() {
8485
private void createWebServer() {
8586
WebServerManager serverManager = this.serverManager;
8687
if (serverManager == null) {
88+
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
8789
String webServerFactoryBeanName = getWebServerFactoryBeanName();
8890
ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
91+
createWebServer.tag("factory", webServerFactory.getClass().toString());
8992
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
9093
this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
9194
getBeanFactory().registerSingleton("webServerGracefulShutdown",
9295
new WebServerGracefulShutdownLifecycle(this.serverManager));
9396
getBeanFactory().registerSingleton("webServerStartStop",
9497
new WebServerStartStopLifecycle(this.serverManager));
98+
createWebServer.end();
9599
}
96100
initPropertySources();
97101
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.context.ApplicationContext;
5050
import org.springframework.context.ApplicationContextException;
5151
import org.springframework.core.io.Resource;
52+
import org.springframework.core.metrics.StartupStep;
5253
import org.springframework.util.StringUtils;
5354
import org.springframework.web.context.ContextLoaderListener;
5455
import org.springframework.web.context.ServletContextAware;
@@ -174,8 +175,11 @@ private void createWebServer() {
174175
WebServer webServer = this.webServer;
175176
ServletContext servletContext = getServletContext();
176177
if (webServer == null && servletContext == null) {
178+
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
177179
ServletWebServerFactory factory = getWebServerFactory();
180+
createWebServer.tag("factory", factory.getClass().toString());
178181
this.webServer = factory.getWebServer(getSelfInitializer());
182+
createWebServer.end();
179183
getBeanFactory().registerSingleton("webServerGracefulShutdown",
180184
new WebServerGracefulShutdownLifecycle(this.webServer));
181185
getBeanFactory().registerSingleton("webServerStartStop",

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java

+31-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Set;
2525
import java.util.concurrent.atomic.AtomicInteger;
2626
import java.util.concurrent.atomic.AtomicReference;
27+
import java.util.function.Supplier;
2728

2829
import javax.annotation.PostConstruct;
2930

@@ -100,6 +101,8 @@
100101
import org.springframework.core.io.DefaultResourceLoader;
101102
import org.springframework.core.io.Resource;
102103
import org.springframework.core.io.ResourceLoader;
104+
import org.springframework.core.metrics.ApplicationStartup;
105+
import org.springframework.core.metrics.StartupStep;
103106
import org.springframework.http.server.reactive.HttpHandler;
104107
import org.springframework.test.context.support.TestPropertySourceUtils;
105108
import org.springframework.util.StringUtils;
@@ -111,8 +114,11 @@
111114
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
112115
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
113116
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
117+
import static org.mockito.ArgumentMatchers.any;
118+
import static org.mockito.ArgumentMatchers.anyString;
114119
import static org.mockito.ArgumentMatchers.argThat;
115120
import static org.mockito.ArgumentMatchers.isA;
121+
import static org.mockito.BDDMockito.given;
116122
import static org.mockito.BDDMockito.willThrow;
117123
import static org.mockito.Mockito.atLeastOnce;
118124
import static org.mockito.Mockito.mock;
@@ -679,13 +685,11 @@ void runnersAreCalledAfterStartedIsLoggedAndBeforeApplicationReadyEventIsPublish
679685
application.addListeners(eventListener);
680686
this.context = application.run();
681687
InOrder applicationRunnerOrder = Mockito.inOrder(eventListener, applicationRunner);
682-
applicationRunnerOrder.verify(applicationRunner).run(ArgumentMatchers.any(ApplicationArguments.class));
683-
applicationRunnerOrder.verify(eventListener)
684-
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
688+
applicationRunnerOrder.verify(applicationRunner).run(any(ApplicationArguments.class));
689+
applicationRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class));
685690
InOrder commandLineRunnerOrder = Mockito.inOrder(eventListener, commandLineRunner);
686691
commandLineRunnerOrder.verify(commandLineRunner).run();
687-
commandLineRunnerOrder.verify(eventListener)
688-
.onApplicationEvent(ArgumentMatchers.any(ApplicationReadyEvent.class));
692+
commandLineRunnerOrder.verify(eventListener).onApplicationEvent(any(ApplicationReadyEvent.class));
689693
}
690694

691695
@Test
@@ -1149,6 +1153,28 @@ void lazyInitializationIgnoresLazyInitializationExcludeFilteredBeans() {
11491153
.getBean(AtomicInteger.class)).hasValue(1);
11501154
}
11511155

1156+
@Test
1157+
void customApplicationStartupPublishStartupSteps() {
1158+
ApplicationStartup applicationStartup = mock(ApplicationStartup.class);
1159+
StartupStep startupStep = mock(StartupStep.class);
1160+
given(applicationStartup.start(anyString())).willReturn(startupStep);
1161+
given(startupStep.tag(anyString(), anyString())).willReturn(startupStep);
1162+
given(startupStep.tag(anyString(), ArgumentMatchers.<Supplier<String>>any())).willReturn(startupStep);
1163+
1164+
SpringApplication application = new SpringApplication(ExampleConfig.class);
1165+
application.setWebApplicationType(WebApplicationType.NONE);
1166+
application.setApplicationStartup(applicationStartup);
1167+
this.context = application.run();
1168+
1169+
assertThat(this.context.getBean(ApplicationStartup.class)).isEqualTo(applicationStartup);
1170+
verify(applicationStartup).start("spring.boot.application.starting");
1171+
verify(applicationStartup).start("spring.boot.application.environment-prepared");
1172+
verify(applicationStartup).start("spring.boot.application.context-prepared");
1173+
verify(applicationStartup).start("spring.boot.application.context-loaded");
1174+
verify(applicationStartup).start("spring.boot.application.started");
1175+
verify(applicationStartup).start("spring.boot.application.running");
1176+
}
1177+
11521178
private <S extends AvailabilityState> ArgumentMatcher<ApplicationEvent> isAvailabilityChangeEventWithState(
11531179
S state) {
11541180
return (argument) -> (argument instanceof AvailabilityChangeEvent<?>)

0 commit comments

Comments
 (0)