Commit 04a02f0a authored by Stephane Nicoll's avatar Stephane Nicoll

Merge pull request #24717 from bono007

* pr/24717:
  Polish "Add support for GET requests for /actuator/startup"
  Add support for GET requests for /actuator/startup

Closes gh-24717
parents 8a6e79dc 632c1239
...@@ -4,24 +4,38 @@ ...@@ -4,24 +4,38 @@
The `startup` endpoint provides information about the application's startup sequence. The `startup` endpoint provides information about the application's startup sequence.
[[startup-retrieving]] [[startup-retrieving]]
== Retrieving the Application Startup steps == Retrieving the Application Startup steps
To retrieve the steps recorded so far during the application startup phase , make a `POST` request to `/actuator/startup`, as shown in the following curl-based example: The application startup steps can either be retrieved as a snapshot (`GET`) or drained from the buffer (`POST`).
include::{snippets}/startup/curl-request.adoc[] [[startup-retrieving-snapshot]]
=== Retrieving a snapshot of 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-snapshot/curl-request.adoc[]
The resulting response is similar to the following: The resulting response is similar to the following:
include::{snippets}/startup/http-response.adoc[] include::{snippets}/startup-snapshot/http-response.adoc[]
[[startup-retrieving-drain]]
== Draining the Application Startup steps
To drain and return the steps recorded so far during the application startup phase , make a `POST` 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]] [[startup-retrieving-response-structure]]
=== Response Structure === Response Structure
The response contains details of the application startup steps recorded so far by the application. The response contains details of the application startup steps.
The following table describes the structure of the response: The following table describes the structure of the response:
[cols="2,1,3"] [cols="2,1,3"]
......
...@@ -26,12 +26,13 @@ import org.springframework.context.annotation.Bean; ...@@ -26,12 +26,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.core.metrics.StartupStep; import org.springframework.core.metrics.StartupStep;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.payload.PayloadDocumentation;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 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.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
...@@ -39,6 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. ...@@ -39,6 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Tests for generating documentation describing {@link StartupEndpoint}. * Tests for generating documentation describing {@link StartupEndpoint}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Stephane Nicoll
*/ */
class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
...@@ -53,9 +55,20 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest ...@@ -53,9 +55,20 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
instantiate.end(); instantiate.end();
} }
@Test
void startupSnapshot() throws Exception {
this.mockMvc.perform(get("/actuator/startup")).andExpect(status().isOk())
.andDo(document("startup-snapshot", PayloadDocumentation.responseFields(responseFields())));
}
@Test @Test
void startup() throws Exception { void startup() throws Exception {
ResponseFieldsSnippet responseFields = responseFields( this.mockMvc.perform(post("/actuator/startup")).andExpect(status().isOk())
.andDo(document("startup", PayloadDocumentation.responseFields(responseFields())));
}
private FieldDescriptor[] responseFields() {
return new FieldDescriptor[] {
fieldWithPath("springBootVersion").type(JsonFieldType.STRING) fieldWithPath("springBootVersion").type(JsonFieldType.STRING)
.description("Spring Boot version for this application.").optional(), .description("Spring Boot version for this application.").optional(),
fieldWithPath("timeline.startTime").description("Start time of the application."), fieldWithPath("timeline.startTime").description("Start time of the application."),
...@@ -73,10 +86,7 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest ...@@ -73,10 +86,7 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest
fieldWithPath("timeline.events.[].startupStep.tags[].key") fieldWithPath("timeline.events.[].startupStep.tags[].key")
.description("The key of the StartupStep Tag."), .description("The key of the StartupStep Tag."),
fieldWithPath("timeline.events.[].startupStep.tags[].value") fieldWithPath("timeline.events.[].startupStep.tags[].value")
.description("The value of the StartupStep Tag.")); .description("The value of the StartupStep Tag.") };
this.mockMvc.perform(post("/actuator/startup")).andExpect(status().isOk())
.andDo(document("startup", responseFields));
} }
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.startup; ...@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.startup;
import org.springframework.boot.SpringBootVersion; import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.metrics.buffering.StartupTimeline; import org.springframework.boot.context.metrics.buffering.StartupTimeline;
...@@ -28,6 +29,7 @@ import org.springframework.boot.context.metrics.buffering.StartupTimeline; ...@@ -28,6 +29,7 @@ import org.springframework.boot.context.metrics.buffering.StartupTimeline;
* application startup}. * application startup}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Chris Bono
* @since 2.4.0 * @since 2.4.0
*/ */
@Endpoint(id = "startup") @Endpoint(id = "startup")
...@@ -44,6 +46,12 @@ public class StartupEndpoint { ...@@ -44,6 +46,12 @@ public class StartupEndpoint {
this.applicationStartup = applicationStartup; this.applicationStartup = applicationStartup;
} }
@ReadOperation
public StartupResponse startupSnapshot() {
StartupTimeline startupTimeline = this.applicationStartup.getBufferedTimeline();
return new StartupResponse(startupTimeline);
}
@WriteOperation @WriteOperation
public StartupResponse startup() { public StartupResponse startup() {
StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline(); StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline();
......
/* /*
* Copyright 2012-2020 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,13 +16,17 @@ ...@@ -16,13 +16,17 @@
package org.springframework.boot.actuate.startup; package org.springframework.boot.actuate.startup;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootVersion; import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.actuate.startup.StartupEndpoint.StartupResponse;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.metrics.ApplicationStartup;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
...@@ -30,17 +34,15 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -30,17 +34,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link StartupEndpoint}. * Tests for {@link StartupEndpoint}.
* *
* @author Brian Clozel * @author Brian Clozel
* @author Chris Bono
*/ */
class StartupEndpointTests { class StartupEndpointTests {
@Test @Test
void startupEventsAreFound() { void startupEventsAreFound() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256); BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256);
ApplicationContextRunner contextRunner = new ApplicationContextRunner() testStartupEndpoint(applicationStartup, (startupEndpoint) -> {
.withInitializer((context) -> context.setApplicationStartup(applicationStartup)) StartupResponse startup = startupEndpoint.startup();
.withUserConfiguration(EndpointConfiguration.class);
contextRunner.run((context) -> {
StartupEndpoint.StartupResponse startup = context.getBean(StartupEndpoint.class).startup();
assertThat(startup.getSpringBootVersion()).isEqualTo(SpringBootVersion.getVersion()); assertThat(startup.getSpringBootVersion()).isEqualTo(SpringBootVersion.getVersion());
assertThat(startup.getTimeline().getStartTime()) assertThat(startup.getTimeline().getStartTime())
.isEqualTo(applicationStartup.getBufferedTimeline().getStartTime()); .isEqualTo(applicationStartup.getBufferedTimeline().getStartTime());
...@@ -48,15 +50,32 @@ class StartupEndpointTests { ...@@ -48,15 +50,32 @@ class StartupEndpointTests {
} }
@Test @Test
void bufferIsDrained() { void bufferWithGetIsNotDrained() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256);
testStartupEndpoint(applicationStartup, (startupEndpoint) -> {
StartupResponse startup = startupEndpoint.startupSnapshot();
assertThat(startup.getTimeline().getEvents()).isNotEmpty();
assertThat(applicationStartup.getBufferedTimeline().getEvents()).isNotEmpty();
});
}
@Test
void bufferWithPostIsDrained() {
BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256); BufferingApplicationStartup applicationStartup = new BufferingApplicationStartup(256);
testStartupEndpoint(applicationStartup, (startupEndpoint) -> {
StartupResponse startup = startupEndpoint.startup();
assertThat(startup.getTimeline().getEvents()).isNotEmpty();
assertThat(applicationStartup.getBufferedTimeline().getEvents()).isEmpty();
});
}
private void testStartupEndpoint(ApplicationStartup applicationStartup, Consumer<StartupEndpoint> startupEndpoint) {
ApplicationContextRunner contextRunner = new ApplicationContextRunner() ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withInitializer((context) -> context.setApplicationStartup(applicationStartup)) .withInitializer((context) -> context.setApplicationStartup(applicationStartup))
.withUserConfiguration(EndpointConfiguration.class); .withUserConfiguration(EndpointConfiguration.class);
contextRunner.run((context) -> { contextRunner.run((context) -> {
StartupEndpoint.StartupResponse startup = context.getBean(StartupEndpoint.class).startup(); assertThat(context).hasSingleBean(StartupEndpoint.class);
assertThat(startup.getTimeline().getEvents()).isNotEmpty(); startupEndpoint.accept(context.getBean(StartupEndpoint.class));
assertThat(applicationStartup.getBufferedTimeline().getEvents()).isEmpty();
}); });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment