Skip to content

Commit b116e89

Browse files
authored
Jakarta HealthCheckServlet object mapper and status indicator (#3924)
- Allow overriding the `ObjectMapper` instance used in `HealthCheckServlet` with the servlet attribute `io.dropwizard.metrics.servlets.HealthCheckServlet.mapper` - Allow setting the HTTP status code used to indicate health to 200 (OK) with the servlet attribute `io.dropwizard.metrics.servlets.HealthCheckServlet.httpStatusIndicator`, or with the HTTP query parameter `httpStatusIndicator` per request Refs #1821 Refs #1871 Fixes #3918
1 parent 5255717 commit b116e89

File tree

2 files changed

+116
-96
lines changed

2 files changed

+116
-96
lines changed

metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java

+37-3
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,21 @@ protected HealthCheckFilter getHealthCheckFilter() {
4545
return HealthCheckFilter.ALL;
4646
}
4747

48+
/**
49+
* @return the {@link ObjectMapper} that shall be used to render health checks,
50+
* or {@code null} if the default object mapper should be used.
51+
*/
52+
protected ObjectMapper getObjectMapper() {
53+
// don't use an object mapper by default
54+
return null;
55+
}
56+
4857
@Override
4958
public void contextInitialized(ServletContextEvent event) {
5059
final ServletContext context = event.getServletContext();
5160
context.setAttribute(HEALTH_CHECK_REGISTRY, getHealthCheckRegistry());
5261
context.setAttribute(HEALTH_CHECK_EXECUTOR, getExecutorService());
62+
context.setAttribute(HEALTH_CHECK_MAPPER, getObjectMapper());
5363
}
5464

5565
@Override
@@ -61,14 +71,18 @@ public void contextDestroyed(ServletContextEvent event) {
6171
public static final String HEALTH_CHECK_REGISTRY = HealthCheckServlet.class.getCanonicalName() + ".registry";
6272
public static final String HEALTH_CHECK_EXECUTOR = HealthCheckServlet.class.getCanonicalName() + ".executor";
6373
public static final String HEALTH_CHECK_FILTER = HealthCheckServlet.class.getCanonicalName() + ".healthCheckFilter";
74+
public static final String HEALTH_CHECK_MAPPER = HealthCheckServlet.class.getCanonicalName() + ".mapper";
75+
public static final String HEALTH_CHECK_HTTP_STATUS_INDICATOR = HealthCheckServlet.class.getCanonicalName() + ".httpStatusIndicator";
6476

6577
private static final long serialVersionUID = -8432996484889177321L;
6678
private static final String CONTENT_TYPE = "application/json";
79+
private static final String HTTP_STATUS_INDICATOR_PARAM = "httpStatusIndicator";
6780

6881
private transient HealthCheckRegistry registry;
6982
private transient ExecutorService executorService;
7083
private transient HealthCheckFilter filter;
7184
private transient ObjectMapper mapper;
85+
private transient boolean httpStatusIndicator;
7286

7387
public HealthCheckServlet() {
7488
}
@@ -96,7 +110,6 @@ public void init(ServletConfig config) throws ServletException {
96110
this.executorService = (ExecutorService) executorAttr;
97111
}
98112

99-
100113
final Object filterAttr = context.getAttribute(HEALTH_CHECK_FILTER);
101114
if (filterAttr instanceof HealthCheckFilter) {
102115
filter = (HealthCheckFilter) filterAttr;
@@ -105,7 +118,20 @@ public void init(ServletConfig config) throws ServletException {
105118
filter = HealthCheckFilter.ALL;
106119
}
107120

108-
this.mapper = new ObjectMapper().registerModule(new HealthCheckModule());
121+
final Object mapperAttr = context.getAttribute(HEALTH_CHECK_MAPPER);
122+
if (mapperAttr instanceof ObjectMapper) {
123+
this.mapper = (ObjectMapper) mapperAttr;
124+
} else {
125+
this.mapper = new ObjectMapper();
126+
}
127+
this.mapper.registerModule(new HealthCheckModule());
128+
129+
final Object httpStatusIndicatorAttr = context.getAttribute(HEALTH_CHECK_HTTP_STATUS_INDICATOR);
130+
if (httpStatusIndicatorAttr instanceof Boolean) {
131+
this.httpStatusIndicator = (Boolean) httpStatusIndicatorAttr;
132+
} else {
133+
this.httpStatusIndicator = true;
134+
}
109135
}
110136

111137
@Override
@@ -123,7 +149,10 @@ protected void doGet(HttpServletRequest req,
123149
if (results.isEmpty()) {
124150
resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
125151
} else {
126-
if (isAllHealthy(results)) {
152+
final String reqParameter = req.getParameter(HTTP_STATUS_INDICATOR_PARAM);
153+
final boolean httpStatusIndicatorParam = Boolean.parseBoolean(reqParameter);
154+
final boolean useHttpStatusForHealthCheck = reqParameter == null ? httpStatusIndicator : httpStatusIndicatorParam;
155+
if (!useHttpStatusForHealthCheck || isAllHealthy(results)) {
127156
resp.setStatus(HttpServletResponse.SC_OK);
128157
} else {
129158
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
@@ -158,4 +187,9 @@ private static boolean isAllHealthy(Map<String, HealthCheck.Result> results) {
158187
}
159188
return true;
160189
}
190+
191+
// visible for testing
192+
ObjectMapper getMapper() {
193+
return mapper;
194+
}
161195
}

metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java

+79-93
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.codahale.metrics.health.HealthCheck;
55
import com.codahale.metrics.health.HealthCheckFilter;
66
import com.codahale.metrics.health.HealthCheckRegistry;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
78
import jakarta.servlet.ServletConfig;
89
import jakarta.servlet.ServletContext;
910
import jakarta.servlet.ServletException;
@@ -15,6 +16,7 @@
1516

1617
import java.time.ZonedDateTime;
1718
import java.time.format.DateTimeFormatter;
19+
import java.util.concurrent.Callable;
1820
import java.util.concurrent.ExecutorService;
1921
import java.util.concurrent.Executors;
2022

@@ -75,136 +77,79 @@ public void tearDown() {
7577
public void returns501IfNoHealthChecksAreRegistered() throws Exception {
7678
processRequest();
7779

78-
assertThat(response.getStatus())
79-
.isEqualTo(501);
80-
assertThat(response.getContent())
81-
.isEqualTo("{}");
82-
assertThat(response.get(HttpHeader.CONTENT_TYPE))
83-
.isEqualTo("application/json");
80+
assertThat(response.getStatus()).isEqualTo(501);
81+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
82+
assertThat(response.getContent()).isEqualTo("{}");
8483
}
8584

8685
@Test
8786
public void returnsA200IfAllHealthChecksAreHealthy() throws Exception {
88-
registry.register("fun", new HealthCheck() {
89-
@Override
90-
protected Result check() {
91-
return healthyResultUsingFixedClockWithMessage("whee");
92-
}
93-
94-
@Override
95-
protected Clock clock() {
96-
return FIXED_CLOCK;
97-
}
98-
});
87+
registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee")));
9988

10089
processRequest();
10190

102-
assertThat(response.getStatus())
103-
.isEqualTo(200);
91+
assertThat(response.getStatus()).isEqualTo(200);
92+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
10493
assertThat(response.getContent())
10594
.isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" +
10695
EXPECTED_TIMESTAMP +
10796
"\"}}");
108-
assertThat(response.get(HttpHeader.CONTENT_TYPE))
109-
.isEqualTo("application/json");
11097
}
11198

11299
@Test
113100
public void returnsASubsetOfHealthChecksIfFiltered() throws Exception {
114-
registry.register("fun", new HealthCheck() {
115-
@Override
116-
protected Result check() {
117-
return healthyResultUsingFixedClockWithMessage("whee");
118-
}
119-
120-
@Override
121-
protected Clock clock() {
122-
return FIXED_CLOCK;
123-
}
124-
});
125-
126-
registry.register("filtered", new HealthCheck() {
127-
@Override
128-
protected Result check() {
129-
return Result.unhealthy("whee");
130-
}
131-
132-
@Override
133-
protected Clock clock() {
134-
return FIXED_CLOCK;
135-
}
136-
});
101+
registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee")));
102+
registry.register("filtered", new TestHealthCheck(() -> unhealthyResultWithMessage("whee")));
137103

138104
processRequest();
139105

140-
assertThat(response.getStatus())
141-
.isEqualTo(200);
106+
assertThat(response.getStatus()).isEqualTo(200);
107+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
142108
assertThat(response.getContent())
143109
.isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" +
144110
EXPECTED_TIMESTAMP +
145111
"\"}}");
146-
assertThat(response.get(HttpHeader.CONTENT_TYPE))
147-
.isEqualTo("application/json");
148112
}
149113

150114
@Test
151115
public void returnsA500IfAnyHealthChecksAreUnhealthy() throws Exception {
152-
registry.register("fun", new HealthCheck() {
153-
@Override
154-
protected Result check() {
155-
return healthyResultUsingFixedClockWithMessage("whee");
156-
}
157-
158-
@Override
159-
protected Clock clock() {
160-
return FIXED_CLOCK;
161-
}
162-
});
163-
164-
registry.register("notFun", new HealthCheck() {
165-
@Override
166-
protected Result check() {
167-
return Result.builder().usingClock(FIXED_CLOCK).unhealthy().withMessage("whee").build();
168-
}
169-
170-
@Override
171-
protected Clock clock() {
172-
return FIXED_CLOCK;
173-
}
174-
});
116+
registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee")));
117+
registry.register("notFun", new TestHealthCheck(() -> unhealthyResultWithMessage("whee")));
175118

176119
processRequest();
177120

178-
assertThat(response.getStatus())
179-
.isEqualTo(500);
180-
assertThat(response.getContent())
181-
.contains(
121+
assertThat(response.getStatus()).isEqualTo(500);
122+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
123+
assertThat(response.getContent()).contains(
182124
"{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}",
183125
",\"notFun\":{\"healthy\":false,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}");
184-
assertThat(response.get(HttpHeader.CONTENT_TYPE))
185-
.isEqualTo("application/json");
126+
}
127+
128+
@Test
129+
public void returnsA200IfAnyHealthChecksAreUnhealthyAndHttpStatusIndicatorIsDisabled() throws Exception {
130+
registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("whee")));
131+
registry.register("notFun", new TestHealthCheck(() -> unhealthyResultWithMessage("whee")));
132+
request.setURI("/healthchecks?httpStatusIndicator=false");
133+
134+
processRequest();
135+
136+
assertThat(response.getStatus()).isEqualTo(200);
137+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
138+
assertThat(response.getContent()).contains(
139+
"{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}",
140+
",\"notFun\":{\"healthy\":false,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}");
186141
}
187142

188143
@Test
189144
public void optionallyPrettyPrintsTheJson() throws Exception {
190-
registry.register("fun", new HealthCheck() {
191-
@Override
192-
protected Result check() {
193-
return healthyResultUsingFixedClockWithMessage("foo bar 123");
194-
}
195-
196-
@Override
197-
protected Clock clock() {
198-
return FIXED_CLOCK;
199-
}
200-
});
145+
registry.register("fun", new TestHealthCheck(() -> healthyResultWithMessage("foo bar 123")));
201146

202147
request.setURI("/healthchecks?pretty=true");
203148

204149
processRequest();
205150

206-
assertThat(response.getStatus())
207-
.isEqualTo(200);
151+
assertThat(response.getStatus()).isEqualTo(200);
152+
assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualTo("application/json");
208153
assertThat(response.getContent())
209154
.isEqualTo(String.format("{%n" +
210155
" \"fun\" : {%n" +
@@ -213,18 +158,24 @@ protected Clock clock() {
213158
" \"duration\" : 0,%n" +
214159
" \"timestamp\" : \"" + EXPECTED_TIMESTAMP + "\"" +
215160
"%n }%n}"));
216-
assertThat(response.get(HttpHeader.CONTENT_TYPE))
217-
.isEqualTo("application/json");
218161
}
219162

220-
private static HealthCheck.Result healthyResultUsingFixedClockWithMessage(String message) {
163+
private static HealthCheck.Result healthyResultWithMessage(String message) {
221164
return HealthCheck.Result.builder()
222165
.healthy()
223166
.withMessage(message)
224167
.usingClock(FIXED_CLOCK)
225168
.build();
226169
}
227170

171+
private static HealthCheck.Result unhealthyResultWithMessage(String message) {
172+
return HealthCheck.Result.builder()
173+
.unhealthy()
174+
.withMessage(message)
175+
.usingClock(FIXED_CLOCK)
176+
.build();
177+
}
178+
228179
@Test
229180
public void constructorWithRegistryAsArgumentIsUsedInPreferenceOverServletConfig() throws Exception {
230181
final HealthCheckRegistry healthCheckRegistry = mock(HealthCheckRegistry.class);
@@ -266,4 +217,39 @@ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNullButWrongTy
266217
final io.dropwizard.metrics.servlets.HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null);
267218
healthCheckServlet.init(servletConfig);
268219
}
220+
221+
@Test
222+
public void constructorWithObjectMapperAsArgumentUsesServletConfigWhenNullButWrongTypeInContext() throws Exception {
223+
final ServletContext servletContext = mock(ServletContext.class);
224+
final ServletConfig servletConfig = mock(ServletConfig.class);
225+
when(servletConfig.getServletContext()).thenReturn(servletContext);
226+
when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY)).thenReturn(registry);
227+
when(servletContext.getAttribute(HealthCheckServlet.HEALTH_CHECK_MAPPER)).thenReturn("IRELLEVANT_STRING");
228+
229+
final HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null);
230+
healthCheckServlet.init(servletConfig);
231+
232+
assertThat(healthCheckServlet.getMapper())
233+
.isNotNull()
234+
.isInstanceOf(ObjectMapper.class);
235+
}
236+
237+
static class TestHealthCheck extends HealthCheck {
238+
private final Callable<Result> check;
239+
240+
public TestHealthCheck(Callable<Result> check) {
241+
this.check = check;
242+
}
243+
244+
@Override
245+
protected Result check() throws Exception {
246+
return check.call();
247+
}
248+
249+
@Override
250+
protected Clock clock() {
251+
return FIXED_CLOCK;
252+
}
253+
}
254+
269255
}

0 commit comments

Comments
 (0)