diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc new file mode 100644 index 0000000000..3c21018341 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/startup.adoc @@ -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[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc index 8bd3212295..58c9f5105c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/index.adoc @@ -70,4 +70,5 @@ include::endpoints/prometheus.adoc[leveloffset=+1] include::endpoints/scheduledtasks.adoc[leveloffset=+1] include::endpoints/sessions.adoc[leveloffset=+1] include::endpoints/shutdown.adoc[leveloffset=+1] +include::endpoints/startup.adoc[leveloffset=+1] include::endpoints/threaddump.adoc[leveloffset=+1] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java new file mode 100644 index 0000000000..55f8b8005f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfiguration.java @@ -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}. + *

+ * 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.")); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java new file mode 100644 index 0000000000..d2a84b9bdb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/startup/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index be21096fd5..fd5b433b26 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -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.session.SessionsEndpointAutoConfiguration,\ 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.trace.http.HttpTraceAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.trace.http.HttpTraceEndpointAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java new file mode 100644 index 0000000000..270088d6aa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/StartupEndpointDocumentationTests.java @@ -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); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..163731311d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/startup/StartupEndpointAutoConfigurationTests.java @@ -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)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java new file mode 100644 index 0000000000..e04fe0b1f7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/StartupEndpoint.java @@ -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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java new file mode 100644 index 0000000000..ca8f82edbb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/startup/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java new file mode 100644 index 0000000000..5f9dcf6eb3 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/startup/StartupEndpointTests.java @@ -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); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 7ce142f006..f343afacb5 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -121,6 +121,10 @@ The following technology-agnostic endpoints are available: | Lets the application be gracefully shutdown. Disabled by default. +| `startup` +| Shows the startup steps data collected by the `ApplicationStartup`. + Requires the `SpringApplication` to be configured with a `BufferingApplicationStartup`. + | `threaddump` | Performs a thread dump. |=== @@ -273,6 +277,10 @@ The following table shows the default exposure for the built-in endpoints: | Yes | No +| `startup` +| Yes +| No + | `threaddump` | Yes | No