diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java index d31f02faff..2af0d91fab 100644 --- a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java +++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java @@ -45,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 @@ -61,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() { } @@ -96,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; @@ -105,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 @@ -123,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); @@ -158,4 +187,9 @@ private static boolean isAllHealthy(Map results) { } return true; } + + // visible for testing + ObjectMapper getMapper() { + return mapper; + } } diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java index c34f7da438..ec5b080841 100644 --- a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java +++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java @@ -4,6 +4,7 @@ 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 jakarta.servlet.ServletConfig; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; @@ -15,6 +16,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -75,136 +77,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" + @@ -213,11 +158,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) @@ -225,6 +168,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); @@ -266,4 +217,39 @@ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNullButWrongTy final io.dropwizard.metrics.servlets.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; + } + } + }