From 4b8d6efc12fb718b0bea460f7c3e63cb3acfe388 Mon Sep 17 00:00:00 2001 From: bono007 Date: Sat, 9 Jan 2021 09:58:02 -0600 Subject: [PATCH 1/2] Add support for GET requests for /actuator/startup See gh-24717 --- .../src/docs/asciidoc/endpoints/startup.adoc | 4 +++- .../boot/actuate/startup/StartupEndpoint.java | 8 ++++++++ .../boot/actuate/startup/StartupEndpointTests.java | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) 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 index 76ac18e236..a2d806b9e4 100644 --- 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 @@ -16,7 +16,9 @@ The resulting response is similar to the following: include::{snippets}/startup/http-response.adoc[] - +NOTE: The above call resets the application startup steps buffer - subsequent calls to the endpoint will +not include the returned steps. To retrieve a snapshot of the steps recorded so far without removing them +from the startup buffer, make a `GET` request to `/actuator/startup`. [[startup-retrieving-response-structure]] === Response Structure 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 index 145d1d7317..9e8c802e40 100644 --- 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 @@ -18,6 +18,7 @@ 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.actuate.endpoint.annotation.WriteOperation; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.StartupTimeline; @@ -28,6 +29,7 @@ import org.springframework.boot.context.metrics.buffering.StartupTimeline; * application startup}. * * @author Brian Clozel + * @author Chris Bono * @since 2.4.0 */ @Endpoint(id = "startup") @@ -50,6 +52,12 @@ public class StartupEndpoint { return new StartupResponse(startupTimeline); } + @ReadOperation + public StartupResponse startupSnapshot() { + StartupTimeline startupTimeline = this.applicationStartup.getBufferedTimeline(); + return new StartupResponse(startupTimeline); + } + /** * A description of an application startup, primarily intended for serialization to * JSON. 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 index 5f9dcf6eb3..c33ab1e0af 100644 --- 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 @@ -30,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link StartupEndpoint}. * * @author Brian Clozel + * @author Chris Bono */ class StartupEndpointTests { @@ -60,6 +61,19 @@ class StartupEndpointTests { }); } + @Test + void bufferIsNotDrained() { + 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).startupSnapshot(); + assertThat(startup.getTimeline().getEvents()).isNotEmpty(); + assertThat(applicationStartup.getBufferedTimeline().getEvents()).isNotEmpty(); + }); + } + @Configuration(proxyBeanMethods = false) static class EndpointConfiguration { From 632c1239e69e8c2383f710ec759fddea0bcde52b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Jan 2021 09:12:53 +0100 Subject: [PATCH 2/2] Polish "Add support for GET requests for /actuator/startup" See gh-24717 --- .../src/docs/asciidoc/endpoints/startup.adoc | 26 +++++++++---- .../StartupEndpointDocumentationTests.java | 24 ++++++++---- .../boot/actuate/startup/StartupEndpoint.java | 14 +++---- .../actuate/startup/StartupEndpointTests.java | 39 +++++++++++-------- 4 files changed, 65 insertions(+), 38 deletions(-) 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 index a2d806b9e4..681d07834f 100644 --- 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 @@ -4,11 +4,27 @@ 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 `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`). + +[[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: + +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[] @@ -16,14 +32,10 @@ The resulting response is similar to the following: include::{snippets}/startup/http-response.adoc[] -NOTE: The above call resets the application startup steps buffer - subsequent calls to the endpoint will -not include the returned steps. To retrieve a snapshot of the steps recorded so far without removing them -from the startup buffer, make a `GET` request to `/actuator/startup`. - [[startup-retrieving-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: [cols="2,1,3"] 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 index 5fe538a4d4..935ee973f7 100644 --- 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 @@ -26,12 +26,13 @@ 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.FieldDescriptor; 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.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.result.MockMvcResultMatchers.status; @@ -39,6 +40,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Tests for generating documentation describing {@link StartupEndpoint}. * * @author Brian Clozel + * @author Stephane Nicoll */ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { @@ -53,9 +55,20 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest instantiate.end(); } + @Test + void startupSnapshot() throws Exception { + this.mockMvc.perform(get("/actuator/startup")).andExpect(status().isOk()) + .andDo(document("startup-snapshot", PayloadDocumentation.responseFields(responseFields()))); + } + @Test 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) .description("Spring Boot version for this application.").optional(), fieldWithPath("timeline.startTime").description("Start time of the application."), @@ -73,10 +86,7 @@ class StartupEndpointDocumentationTests extends MockMvcEndpointDocumentationTest 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(post("/actuator/startup")).andExpect(status().isOk()) - .andDo(document("startup", responseFields)); + .description("The value of the StartupStep Tag.") }; } @Configuration(proxyBeanMethods = false) 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 index 9e8c802e40..51b0412a65 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -46,18 +46,18 @@ public class StartupEndpoint { this.applicationStartup = applicationStartup; } - @WriteOperation - public StartupResponse startup() { - StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline(); - return new StartupResponse(startupTimeline); - } - @ReadOperation public StartupResponse startupSnapshot() { StartupTimeline startupTimeline = this.applicationStartup.getBufferedTimeline(); return new StartupResponse(startupTimeline); } + @WriteOperation + public StartupResponse startup() { + StartupTimeline startupTimeline = this.applicationStartup.drainBufferedTimeline(); + return new StartupResponse(startupTimeline); + } + /** * A description of an application startup, primarily intended for serialization to * JSON. 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 index c33ab1e0af..6e9ae9e5b2 100644 --- 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 @@ -1,5 +1,5 @@ /* - * 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"); * you may not use this file except in compliance with the License. @@ -16,13 +16,17 @@ package org.springframework.boot.actuate.startup; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; 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.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.metrics.ApplicationStartup; import static org.assertj.core.api.Assertions.assertThat; @@ -37,11 +41,8 @@ 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(); + testStartupEndpoint(applicationStartup, (startupEndpoint) -> { + StartupResponse startup = startupEndpoint.startup(); assertThat(startup.getSpringBootVersion()).isEqualTo(SpringBootVersion.getVersion()); assertThat(startup.getTimeline().getStartTime()) .isEqualTo(applicationStartup.getBufferedTimeline().getStartTime()); @@ -49,28 +50,32 @@ class StartupEndpointTests { } @Test - void bufferIsDrained() { + void bufferWithGetIsNotDrained() { 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(); + testStartupEndpoint(applicationStartup, (startupEndpoint) -> { + StartupResponse startup = startupEndpoint.startupSnapshot(); assertThat(startup.getTimeline().getEvents()).isNotEmpty(); - assertThat(applicationStartup.getBufferedTimeline().getEvents()).isEmpty(); + assertThat(applicationStartup.getBufferedTimeline().getEvents()).isNotEmpty(); }); } @Test - void bufferIsNotDrained() { + void bufferWithPostIsDrained() { 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) { ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withInitializer((context) -> context.setApplicationStartup(applicationStartup)) .withUserConfiguration(EndpointConfiguration.class); contextRunner.run((context) -> { - StartupEndpoint.StartupResponse startup = context.getBean(StartupEndpoint.class).startupSnapshot(); - assertThat(startup.getTimeline().getEvents()).isNotEmpty(); - assertThat(applicationStartup.getBufferedTimeline().getEvents()).isNotEmpty(); + assertThat(context).hasSingleBean(StartupEndpoint.class); + startupEndpoint.accept(context.getBean(StartupEndpoint.class)); }); }