Add startup Actuator endpoint

This commit builds on top of gh-22603 and exposes data collected by the
`BufferingApplicationStartup` on a dedicated `"/startup"` Actuator
endpoint.

Closes gh-23213
pull/23215/head
Brian Clozel 4 years ago
parent 6be4409fde
commit 676e1809fb

@ -0,0 +1,28 @@
[[startup]]
= Application Startup (`startup`)
The `startup` endpoint provides information about the application's startup sequence.
[[startup-retrieving]]
== Retrieving the Application Startup steps
To retrieve the steps recorded so far during the application startup phase , make a `GET` request to `/actuator/startup`, as shown in the following curl-based example:
include::{snippets}/startup/curl-request.adoc[]
The resulting response is similar to the following:
include::{snippets}/startup/http-response.adoc[]
[[startup-retrieving-response-structure]]
=== Response Structure
The response contains details of the application startup steps recorded so far by the application.
The following table describes the structure of the response:
[cols="2,1,3"]
include::{snippets}/startup/response-fields.adoc[]

@ -70,4 +70,5 @@ include::endpoints/prometheus.adoc[leveloffset=+1]
include::endpoints/scheduledtasks.adoc[leveloffset=+1] include::endpoints/scheduledtasks.adoc[leveloffset=+1]
include::endpoints/sessions.adoc[leveloffset=+1] include::endpoints/sessions.adoc[leveloffset=+1]
include::endpoints/shutdown.adoc[leveloffset=+1] include::endpoints/shutdown.adoc[leveloffset=+1]
include::endpoints/startup.adoc[leveloffset=+1]
include::endpoints/threaddump.adoc[leveloffset=+1] include::endpoints/threaddump.adoc[leveloffset=+1]

@ -0,0 +1,74 @@
/*
* Copyright 2012-2019 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.startup;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.startup.StartupEndpoint;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link EnableAutoConfiguration Auto-configuration} for the {@link StartupEndpoint}.
*
* @author Brian Clozel
* @since 2.4.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnAvailableEndpoint(endpoint = StartupEndpoint.class)
@Conditional(StartupEndpointAutoConfiguration.ApplicationStartupCondition.class)
public class StartupEndpointAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public StartupEndpoint startupEndpoint(BufferingApplicationStartup applicationStartup) {
return new StartupEndpoint(applicationStartup);
}
/**
* {@link SpringBootCondition} checking the configured
* {@link org.springframework.core.metrics.ApplicationStartup}.
* <p>
* Endpoint is enabled only if the configured implementation is
* {@link BufferingApplicationStartup}.
*/
static class ApplicationStartupCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ApplicationStartup");
ApplicationStartup applicationStartup = context.getBeanFactory().getApplicationStartup();
if (applicationStartup instanceof BufferingApplicationStartup) {
return ConditionOutcome.match(
message.because("configured applicationStartup is of type BufferingApplicationStartup."));
}
return ConditionOutcome.noMatch(message.because("configured applicationStartup is of type "
+ applicationStartup.getClass() + ", expected BufferingApplicationStartup."));
}
}
}

@ -0,0 +1,20 @@
/*
* 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.
* 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 ApplicationStartup concerns.
*/
package org.springframework.boot.actuate.autoconfigure.startup;

@ -87,6 +87,7 @@ org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagem
org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.solr.SolrHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.solr.SolrHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration,\

@ -0,0 +1,98 @@
/*
* 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.
* 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.endpoint.web.documentation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.startup.StartupEndpoint;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.metrics.StartupStep;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.ResponseFieldsSnippet;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for generating documentation describing {@link StartupEndpoint}.
*
* @author Brian Clozel
*/
class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
@BeforeEach
void appendSampleStartupSteps(@Autowired BufferingApplicationStartup applicationStartup) {
StartupStep starting = applicationStartup.start("spring.boot.application.starting");
starting.tag("mainApplicationClass", "com.example.startup.StartupApplication");
starting.end();
StartupStep instantiate = applicationStartup.start("spring.beans.instantiate");
instantiate.tag("beanName", "homeController");
instantiate.end();
}
@Test
void startup() throws Exception {
ResponseFieldsSnippet responseFields = responseFields(
fieldWithPath("springBootVersion").type(JsonFieldType.STRING)
.description("Spring Boot version for this application.").optional(),
fieldWithPath("timeline.startTime").description("Start time of the application."),
fieldWithPath("timeline.events")
.description("An array of steps collected during application startup so far."),
fieldWithPath("timeline.events.[].startTime").description("The timestamp of the start of this event."),
fieldWithPath("timeline.events.[].endTime").description("The timestamp of the end of this event."),
fieldWithPath("timeline.events.[].duration").description("The precise duration of this event."),
fieldWithPath("timeline.events.[].startupStep.name").description("The name of the StartupStep."),
fieldWithPath("timeline.events.[].startupStep.id").description("The id of this StartupStep."),
fieldWithPath("timeline.events.[].startupStep.parentId")
.description("The parent id for this StartupStep."),
fieldWithPath("timeline.events.[].startupStep.tags")
.description("An array of key/value pairs with additional step info."),
fieldWithPath("timeline.events.[].startupStep.tags[].key")
.description("The key of the StartupStep Tag."),
fieldWithPath("timeline.events.[].startupStep.tags[].value")
.description("The value of the StartupStep Tag."));
this.mockMvc.perform(get("/actuator/startup")).andExpect(status().isOk())
.andDo(document("startup", responseFields));
}
@Configuration(proxyBeanMethods = false)
@Import(BaseDocumentationConfiguration.class)
static class TestConfiguration {
@Bean
StartupEndpoint startupEndpoint(BufferingApplicationStartup startup) {
return new StartupEndpoint(startup);
}
@Bean
BufferingApplicationStartup bufferingApplicationStartup() {
return new BufferingApplicationStartup(16);
}
}
}

@ -0,0 +1,61 @@
/*
* 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.
* 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.startup;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.startup.StartupEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StartupEndpointAutoConfiguration}
*
* @author Brian Clozel
*/
class StartupEndpointAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(StartupEndpointAutoConfiguration.class));
@Test
void runShouldNotHaveStartupEndpoint() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(StartupEndpoint.class));
}
@Test
void runWhenMissingAppStartupShouldNotHaveStartupEndpoint() {
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=startup")
.run((context) -> assertThat(context).doesNotHaveBean(StartupEndpoint.class));
}
@Test
void runShouldHaveStartupEndpoint() {
new ApplicationContextRunner(() -> {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.setApplicationStartup(new BufferingApplicationStartup(1));
return context;
}).withConfiguration(AutoConfigurations.of(StartupEndpointAutoConfiguration.class))
.withPropertyValues("management.endpoints.web.exposure.include=startup")
.run((context) -> assertThat(context).hasSingleBean(StartupEndpoint.class));
}
}

@ -0,0 +1,78 @@
/*
* 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.
* 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.startup;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline;
/**
* {@link Endpoint @Endpoint} to expose the timeline of the
* {@link org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup
* application startup}.
*
* @author Brian Clozel
* @since 2.4.0
*/
@Endpoint(id = "startup")
public class StartupEndpoint {
private final BufferingApplicationStartup applicationStartup;
/**
* Creates a new {@code StartupEndpoint} that will describe the timeline of buffered
* application startup events.
* @param applicationStartup the application startup
*/
public StartupEndpoint(BufferingApplicationStartup applicationStartup) {
this.applicationStartup = applicationStartup;
}
@ReadOperation
public StartupResponse startup() {
StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline();
return new StartupResponse(startupTimeline);
}
/**
* A description of an application startup, primarily intended for serialization to
* JSON.
*/
public static final class StartupResponse {
private final String springBootVersion;
private final StartupTimeline timeline;
private StartupResponse(StartupTimeline timeline) {
this.timeline = timeline;
this.springBootVersion = SpringBootVersion.getVersion();
}
public String getSpringBootVersion() {
return this.springBootVersion;
}
public StartupTimeline getTimeline() {
return this.timeline;
}
}
}

@ -0,0 +1,20 @@
/*
* 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.
* 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 {@link org.springframework.core.metrics.ApplicationStartup}.
*/
package org.springframework.boot.actuate.startup;

@ -0,0 +1,73 @@
/*
* 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.
* 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.startup;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
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 StartupEndpoint}.
*
* @author Brian Clozel
*/
class StartupEndpointTests {
@Test
void startupEventsAreFound() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256);
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer((context) -> context.setApplicationStartup(applicationStartup))
.withUserConfiguration(EndpointConfiguration.class);
contextRunner.run((context) -> {
StartupEndpoint.StartupResponse startup = context.getBean(StartupEndpoint.class).startup();
assertThat(startup.getSpringBootVersion()).isEqualTo(SpringBootVersion.getVersion());
assertThat(startup.getTimeline().getStartTime())
.isEqualTo(applicationStartup.getBufferedTimeline().getStartTime());
});
}
@Test
void bufferIsDrained() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256);
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer((context) -> context.setApplicationStartup(applicationStartup))
.withUserConfiguration(EndpointConfiguration.class);
contextRunner.run((context) -> {
StartupEndpoint.StartupResponse startup = context.getBean(StartupEndpoint.class).startup();
assertThat(startup.getTimeline().getEvents()).isNotEmpty();
assertThat(applicationStartup.getBufferedTimeline().getEvents()).isEmpty();
});
}
@Configuration(proxyBeanMethods = false)
static class EndpointConfiguration {
@Bean
StartupEndpoint endpoint(BufferingApplicationStartup applicationStartup) {
return new StartupEndpoint(applicationStartup);
}
}
}

@ -121,6 +121,10 @@ The following technology-agnostic endpoints are available:
| Lets the application be gracefully shutdown. | Lets the application be gracefully shutdown.
Disabled by default. Disabled by default.
| `startup`
| Shows the startup steps data collected by the `ApplicationStartup`.
Requires the `SpringApplication` to be configured with a `BufferingApplicationStartup`.
| `threaddump` | `threaddump`
| Performs a thread dump. | Performs a thread dump.
|=== |===
@ -273,6 +277,10 @@ The following table shows the default exposure for the built-in endpoints:
| Yes | Yes
| No | No
| `startup`
| Yes
| No
| `threaddump` | `threaddump`
| Yes | Yes
| No | No

Loading…
Cancel
Save