diff --git a/metrics-servlets/src/main/java/com/codahale/metrics/servlets/HealthCheckServlet.java b/metrics-servlets/src/main/java/com/codahale/metrics/servlets/HealthCheckServlet.java index 60af83f27f..3865a2d62c 100644 --- a/metrics-servlets/src/main/java/com/codahale/metrics/servlets/HealthCheckServlet.java +++ b/metrics-servlets/src/main/java/com/codahale/metrics/servlets/HealthCheckServlet.java @@ -1,10 +1,11 @@ package com.codahale.metrics.servlets; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; -import java.util.SortedMap; -import java.util.concurrent.ExecutorService; +import com.codahale.metrics.health.HealthCheck; +import com.codahale.metrics.health.HealthCheckFilter; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.json.HealthCheckModule; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -14,13 +15,11 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; - -import com.codahale.metrics.health.HealthCheck; -import com.codahale.metrics.health.HealthCheckFilter; -import com.codahale.metrics.health.HealthCheckRegistry; -import com.codahale.metrics.json.HealthCheckModule; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; +import java.util.SortedMap; +import java.util.concurrent.ExecutorService; public class HealthCheckServlet extends HttpServlet { public static abstract class ContextListener implements ServletContextListener { @@ -46,11 +45,21 @@ protected HealthCheckFilter getHealthCheckFilter() { return HealthCheckFilter.ALL; } + /** + * @return the {@link ObjectMapper} that shall be used to render health checks, + * or {@code null} if the default object mapper should be used. + */ + protected ObjectMapper getObjectMapper() { + // don't use an object mapper by default + return null; + } + @Override public void contextInitialized(ServletContextEvent event) { final ServletContext context = event.getServletContext(); context.setAttribute(HEALTH_CHECK_REGISTRY, getHealthCheckRegistry()); context.setAttribute(HEALTH_CHECK_EXECUTOR, getExecutorService()); + context.setAttribute(HEALTH_CHECK_MAPPER, getObjectMapper()); } @Override @@ -62,14 +71,18 @@ public void contextDestroyed(ServletContextEvent event) { public static final String HEALTH_CHECK_REGISTRY = HealthCheckServlet.class.getCanonicalName() + ".registry"; public static final String HEALTH_CHECK_EXECUTOR = HealthCheckServlet.class.getCanonicalName() + ".executor"; public static final String HEALTH_CHECK_FILTER = HealthCheckServlet.class.getCanonicalName() + ".healthCheckFilter"; + public static final String HEALTH_CHECK_MAPPER = HealthCheckServlet.class.getCanonicalName() + ".mapper"; + public static final String HEALTH_CHECK_HTTP_STATUS_INDICATOR = HealthCheckServlet.class.getCanonicalName() + ".httpStatusIndicator"; private static final long serialVersionUID = -8432996484889177321L; private static final String CONTENT_TYPE = "application/json"; + private static final String HTTP_STATUS_INDICATOR_PARAM = "httpStatusIndicator"; private transient HealthCheckRegistry registry; private transient ExecutorService executorService; private transient HealthCheckFilter filter; private transient ObjectMapper mapper; + private transient boolean httpStatusIndicator; public HealthCheckServlet() { } @@ -97,7 +110,6 @@ public void init(ServletConfig config) throws ServletException { this.executorService = (ExecutorService) executorAttr; } - final Object filterAttr = context.getAttribute(HEALTH_CHECK_FILTER); if (filterAttr instanceof HealthCheckFilter) { filter = (HealthCheckFilter) filterAttr; @@ -106,7 +118,20 @@ public void init(ServletConfig config) throws ServletException { filter = HealthCheckFilter.ALL; } - this.mapper = new ObjectMapper().registerModule(new HealthCheckModule()); + final Object mapperAttr = context.getAttribute(HEALTH_CHECK_MAPPER); + if (mapperAttr instanceof ObjectMapper) { + this.mapper = (ObjectMapper) mapperAttr; + } else { + this.mapper = new ObjectMapper(); + } + this.mapper.registerModule(new HealthCheckModule()); + + final Object httpStatusIndicatorAttr = context.getAttribute(HEALTH_CHECK_HTTP_STATUS_INDICATOR); + if (httpStatusIndicatorAttr instanceof Boolean) { + this.httpStatusIndicator = (Boolean) httpStatusIndicatorAttr; + } else { + this.httpStatusIndicator = true; + } } @Override @@ -124,7 +149,10 @@ protected void doGet(HttpServletRequest req, if (results.isEmpty()) { resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); } else { - if (isAllHealthy(results)) { + final String reqParameter = req.getParameter(HTTP_STATUS_INDICATOR_PARAM); + final boolean httpStatusIndicatorParam = Boolean.parseBoolean(reqParameter); + final boolean useHttpStatusForHealthCheck = reqParameter == null ? httpStatusIndicator : httpStatusIndicatorParam; + if (!useHttpStatusForHealthCheck || isAllHealthy(results)) { resp.setStatus(HttpServletResponse.SC_OK); } else { resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); @@ -159,4 +187,9 @@ private static boolean isAllHealthy(Map results) { } return true; } + + // visible for testing + ObjectMapper getMapper() { + return mapper; + } } diff --git a/metrics-servlets/src/test/java/com/codahale/metrics/servlets/HealthCheckServletTest.java b/metrics-servlets/src/test/java/com/codahale/metrics/servlets/HealthCheckServletTest.java index 8b0a1573be..b3ccc9bae0 100644 --- a/metrics-servlets/src/test/java/com/codahale/metrics/servlets/HealthCheckServletTest.java +++ b/metrics-servlets/src/test/java/com/codahale/metrics/servlets/HealthCheckServletTest.java @@ -1,32 +1,32 @@ package com.codahale.metrics.servlets; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; - import com.codahale.metrics.Clock; +import com.codahale.metrics.health.HealthCheck; +import com.codahale.metrics.health.HealthCheckFilter; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.servlet.ServletTester; import org.junit.After; import org.junit.Before; import org.junit.Test; -import com.codahale.metrics.health.HealthCheck; -import com.codahale.metrics.health.HealthCheckFilter; -import com.codahale.metrics.health.HealthCheckRegistry; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class HealthCheckServletTest extends AbstractServletTest { @@ -51,12 +51,14 @@ public long getTime() { private final HealthCheckRegistry registry = new HealthCheckRegistry(); private final ExecutorService threadPool = Executors.newCachedThreadPool(); + private final ObjectMapper mapper = new ObjectMapper(); @Override protected void setUp(ServletTester tester) { tester.addServlet(HealthCheckServlet.class, "/healthchecks"); tester.setAttribute("com.codahale.metrics.servlets.HealthCheckServlet.registry", registry); tester.setAttribute("com.codahale.metrics.servlets.HealthCheckServlet.executor", threadPool); + tester.setAttribute("com.codahale.metrics.servlets.HealthCheckServlet.mapper", mapper); tester.setAttribute("com.codahale.metrics.servlets.HealthCheckServlet.healthCheckFilter", (HealthCheckFilter) (name, healthCheck) -> !"filtered".equals(name)); } @@ -77,136 +79,79 @@ public void tearDown() { public void returns501IfNoHealthChecksAreRegistered() throws Exception { processRequest(); - assertThat(response.getStatus()) - .isEqualTo(501); - assertThat(response.getContent()) - .isEqualTo("{}"); - assertThat(response.get(HttpHeader.CONTENT_TYPE)) - .isEqualTo("application/json"); + assertThat(response.getStatus()).isEqualTo(501); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); + assertThat(response.getContent()).isEqualTo("{}"); } @Test public void returnsA200IfAllHealthChecksAreHealthy() throws Exception { - registry.register("fun", new HealthCheck() { - @Override - protected Result check() { - return healthyResultUsingFixedClockWithMessage("whee"); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); + registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee"))); processRequest(); - assertThat(response.getStatus()) - .isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); assertThat(response.getContent()) .isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}"); - assertThat(response.get(HttpHeader.CONTENT_TYPE)) - .isEqualTo("application/json"); } @Test public void returnsASubsetOfHealthChecksIfFiltered() throws Exception { - registry.register("fun", new HealthCheck() { - @Override - protected Result check() { - return healthyResultUsingFixedClockWithMessage("whee"); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); - - registry.register("filtered", new HealthCheck() { - @Override - protected Result check() { - return Result.unhealthy("whee"); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); + registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee"))); + registry.register("filtered", new TestHealthCheck(() -> unhealthyResultWithMessage("whee"))); processRequest(); - assertThat(response.getStatus()) - .isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); assertThat(response.getContent()) .isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}"); - assertThat(response.get(HttpHeader.CONTENT_TYPE)) - .isEqualTo("application/json"); } @Test public void returnsA500IfAnyHealthChecksAreUnhealthy() throws Exception { - registry.register("fun", new HealthCheck() { - @Override - protected Result check() { - return healthyResultUsingFixedClockWithMessage("whee"); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); - - registry.register("notFun", new HealthCheck() { - @Override - protected Result check() { - return Result.builder().usingClock(FIXED_CLOCK).unhealthy().withMessage("whee").build(); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); + registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee"))); + registry.register("notFun", new TestHealthCheck(() -> unhealthyResultWithMessage("whee"))); processRequest(); - assertThat(response.getStatus()) - .isEqualTo(500); - assertThat(response.getContent()) - .contains( + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); + assertThat(response.getContent()).contains( "{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}", ",\"notFun\":{\"healthy\":false,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}"); - assertThat(response.get(HttpHeader.CONTENT_TYPE)) - .isEqualTo("application/json"); + } + + @Test + public void returnsA200IfAnyHealthChecksAreUnhealthyAndHttpStatusIndicatorIsDisabled() throws Exception { + registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee"))); + registry.register("notFun", new TestHealthCheck(() -> unhealthyResultWithMessage("whee"))); + request.setURI("/healthchecks?httpStatusIndicator=false"); + + processRequest(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); + assertThat(response.getContent()).contains( + "{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}", + ",\"notFun\":{\"healthy\":false,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}"); } @Test public void optionallyPrettyPrintsTheJson() throws Exception { - registry.register("fun", new HealthCheck() { - @Override - protected Result check() { - return healthyResultUsingFixedClockWithMessage("foo bar 123"); - } - - @Override - protected Clock clock() { - return FIXED_CLOCK; - } - }); + registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("foo bar 123"))); request.setURI("/healthchecks?pretty=true"); processRequest(); - assertThat(response.getStatus()) - .isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json"); assertThat(response.getContent()) .isEqualTo(String.format("{%n" + " \"fun\" : {%n" + @@ -215,11 +160,9 @@ protected Clock clock() { " \"duration\" : 0,%n" + " \"timestamp\" : \"" + EXPECTED_TIMESTAMP + "\"" + "%n }%n}")); - assertThat(response.get(HttpHeader.CONTENT_TYPE)) - .isEqualTo("application/json"); } - private static HealthCheck.Result healthyResultUsingFixedClockWithMessage(String message) { + private static HealthCheck.Result healthyResultWithMessage(String message) { return HealthCheck.Result.builder() .healthy() .withMessage(message) @@ -227,6 +170,14 @@ private static HealthCheck.Result healthyResultUsingFixedClockWithMessage(String .build(); } + private static HealthCheck.Result unhealthyResultWithMessage(String message) { + return HealthCheck.Result.builder() + .unhealthy() + .withMessage(message) + .usingClock(FIXED_CLOCK) + .build(); + } + @Test public void constructorWithRegistryAsArgumentIsUsedInPreferenceOverServletConfig() throws Exception { final HealthCheckRegistry healthCheckRegistry = mock(HealthCheckRegistry.class); @@ -238,7 +189,7 @@ public void constructorWithRegistryAsArgumentIsUsedInPreferenceOverServletConfig healthCheckServlet.init(servletConfig); verify(servletConfig, times(1)).getServletContext(); - verify(servletContext, never()).getAttribute(eq(HealthCheckServlet.HEALTH_CHECK_REGISTRY)); + verify(servletContext, never()).getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY); } @Test @@ -247,14 +198,14 @@ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNull() throws final ServletContext servletContext = mock(ServletContext.class); final ServletConfig servletConfig = mock(ServletConfig.class); when(servletConfig.getServletContext()).thenReturn(servletContext); - when(servletContext.getAttribute(eq(HealthCheckServlet.HEALTH_CHECK_REGISTRY))) + when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY)) .thenReturn(healthCheckRegistry); final HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null); healthCheckServlet.init(servletConfig); verify(servletConfig, times(1)).getServletContext(); - verify(servletContext, times(1)).getAttribute(eq(HealthCheckServlet.HEALTH_CHECK_REGISTRY)); + verify(servletContext, times(1)).getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY); } @Test(expected = ServletException.class) @@ -262,10 +213,44 @@ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNullButWrongTy final ServletContext servletContext = mock(ServletContext.class); final ServletConfig servletConfig = mock(ServletConfig.class); when(servletConfig.getServletContext()).thenReturn(servletContext); - when(servletContext.getAttribute(eq(HealthCheckServlet.HEALTH_CHECK_REGISTRY))) + when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY)) .thenReturn("IRELLEVANT_STRING"); final HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null); healthCheckServlet.init(servletConfig); } + + @Test + public void constructorWithObjectMapperAsArgumentUsesServletConfigWhenNullButWrongTypeInContext() throws Exception { + final ServletContext servletContext = mock(ServletContext.class); + final ServletConfig servletConfig = mock(ServletConfig.class); + when(servletConfig.getServletContext()).thenReturn(servletContext); + when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY)).thenReturn(registry); + when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_MAPPER)).thenReturn("IRELLEVANT_STRING"); + + final HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null); + healthCheckServlet.init(servletConfig); + + assertThat(healthCheckServlet.getMapper()) + .isNotNull() + .isInstanceOf(ObjectMapper.class); + } + + static class TestHealthCheck extends HealthCheck { + private final Callable check; + + public TestHealthCheck(Callable check) { + this.check = check; + } + + @Override + protected Result check() throws Exception { + return check.call(); + } + + @Override + protected Clock clock() { + return FIXED_CLOCK; + } + } }