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-23213pull/23215/head
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[]
|
@ -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;
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue