diff --git a/pom.xml b/pom.xml index 09f21e5f5..c986b6a8d 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ vpro-shared-hibernate-search vpro-shared-jackson2 vpro-shared-logging + vpro-shared-monitoring vpro-shared-persistence vpro-shared-resteasy vpro-shared-rs @@ -394,6 +395,12 @@ ${assertj.version} test + + com.jayway.jsonpath + json-path-assert + 2.4.0 + test + org.springframework spring-web @@ -736,7 +743,6 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.0 attach-javadocs diff --git a/vpro-shared-bom/pom.xml b/vpro-shared-bom/pom.xml index 985391562..0d058102f 100644 --- a/vpro-shared-bom/pom.xml +++ b/vpro-shared-bom/pom.xml @@ -112,6 +112,11 @@ vpro-shared-logging ${project.version} + + nl.vpro.shared + vpro-shared-monitoring + ${project.version} + nl.vpro.shared vpro-shared-persistence diff --git a/vpro-shared-monitoring/pom.xml b/vpro-shared-monitoring/pom.xml new file mode 100644 index 000000000..7657a2373 --- /dev/null +++ b/vpro-shared-monitoring/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + + + nl.vpro.shared + vpro-shared-parent + 2.16-SNAPSHOT + + vpro-shared-monitoring + This modules contains utilities and endpoints for application deployments and monitoring. + + + com.jayway.jsonpath + json-path-assert + 2.4.0 + + + nl.vpro.shared + vpro-shared-test + + + javax.servlet + javax.servlet-api + 3.1.0 + test + + + org.hamcrest + hamcrest-all + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core + jackson-core + compile + + + org.projectlombok + lombok + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + test + + + org.springframework + spring-test + + + diff --git a/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/domain/Health.java b/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/domain/Health.java new file mode 100644 index 000000000..893354074 --- /dev/null +++ b/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/domain/Health.java @@ -0,0 +1,11 @@ +package nl.vpro.monitoring.domain; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class Health { + private int status; + private String message; +} diff --git a/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/web/HealthController.java b/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/web/HealthController.java new file mode 100644 index 000000000..494a30e01 --- /dev/null +++ b/vpro-shared-monitoring/src/main/java/nl/vpro/monitoring/web/HealthController.java @@ -0,0 +1,46 @@ +package nl.vpro.monitoring.web; + +import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.*; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import nl.vpro.monitoring.domain.Health; + +@Lazy(false) +@RestController +@RequestMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE) +public class HealthController { + + private Status status = Status.STARTING; + + @EventListener + public void onApplicationEvent(ContextRefreshedEvent refreshedEvent) { + status = Status.READY; + } + + @EventListener + public void onApplicationEvent(ContextStoppedEvent stoppedEvent) { + status = Status.STOPPING; + } + + @GetMapping + public ResponseEntity health() { + return ResponseEntity.status(status.code).body(Health.builder().status(status.code).message(status.message).build()); + } + + private enum Status { + STARTING(503, "Application starting"), + READY(200, "Application ready"), + STOPPING(503, "Application shutdown"); + + int code; + String message; + + Status(int code, String message) { + this.code = code; + this.message = message; + } + } +} diff --git a/vpro-shared-monitoring/src/test/java/nl/vpro/monitoring/web/HealthControllerTest.java b/vpro-shared-monitoring/src/test/java/nl/vpro/monitoring/web/HealthControllerTest.java new file mode 100644 index 000000000..a89316846 --- /dev/null +++ b/vpro-shared-monitoring/src/test/java/nl/vpro/monitoring/web/HealthControllerTest.java @@ -0,0 +1,76 @@ +package nl.vpro.monitoring.web; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import nl.vpro.monitoring.domain.Health; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration("/manage-servlet.xml") +class HealthControllerTest { + @Autowired + private HealthController healthController; + + @Autowired + private WebApplicationContext wac; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + } + + @Test + void initialValues() { + // Pragmatic testing of initial values. Using #wac and #mockMvc is more or less impossible. + ResponseEntity response = new HealthController().health(); + assertThat(response.getStatusCodeValue()).isEqualTo(503); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getStatus()).isEqualTo(503); + assertThat(response.getBody().getMessage()).isEqualTo("Application starting"); + } + + @Test + void statusReady() throws Exception { + healthController.onApplicationEvent((ContextRefreshedEvent) null); + + mockMvc.perform( + get("/health") + .accept(APPLICATION_JSON_VALUE) + ).andExpect(status().is(200)) + .andExpect(jsonPath("$.status", is(200))) + .andExpect(jsonPath("$.message", is("Application ready"))); + } + + @Test + void statusStopping() throws Exception { + healthController.onApplicationEvent((ContextStoppedEvent) null); + + mockMvc.perform( + get("/health") + .accept(APPLICATION_JSON_VALUE) + ).andExpect(status().is(503)) + .andExpect(jsonPath("$.status", is(503))) + .andExpect(jsonPath("$.message", is("Application shutdown"))); + } +} diff --git a/vpro-shared-monitoring/src/test/resources/manage-servlet.xml b/vpro-shared-monitoring/src/test/resources/manage-servlet.xml new file mode 100644 index 000000000..769bc79c7 --- /dev/null +++ b/vpro-shared-monitoring/src/test/resources/manage-servlet.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + +