Browse Source
This commit builds on top of gh-22603 and exposes data collected by the `BufferingApplicationStartup` on a dedicated `"/startup"` Actuator endpoint. Closes gh-23213pull/23215/head
11 changed files with 462 additions and 0 deletions
@ -0,0 +1,28 @@
@@ -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[] |
||||
@ -0,0 +1,74 @@
@@ -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 @@
@@ -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; |
||||
@ -0,0 +1,98 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue