Skip to content

Commit 5d7e61e

Browse files
authored
Forward-port to 3.x of addition of HEAD support to health endpoints (helidon-io#3936)
* Forward-port of addition of HEAD support to health endpoints
1 parent 889af59 commit 5d7e61e

5 files changed

Lines changed: 237 additions & 23 deletions

File tree

docs/mp/health/01_introduction.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///////////////////////////////////////////////////////////////////////////////
22

3-
Copyright (c) 2020, 2021 Oracle and/or its affiliates.
3+
Copyright (c) 2020, 2022 Oracle and/or its affiliates.
44

55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -88,6 +88,10 @@ A MicroProfile-compliant service reports its health via known REST endpoints. He
8888
provides these endpoints automatically as part of every MP microservice.
8989
9090
External management tools (or `curl` or browsers) retrieve health checks using the REST endpoints in the following table which summarizes the types of health checks in MicroProfile Health.
91+
Responses from the health endpoints report `200` (OK), `204` (no content), or `503` (service unavailable) depending on the outcome of running the health checks.
92+
HTTP `GET` responses include JSON content showing the detailed results of all the health checks which the server executed after receiving the request.
93+
HTTP `HEAD` requests return only the status with no payload.
94+
9195
9296
.Types of Health Checks
9397
|===

docs/se/health/01_health.adoc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///////////////////////////////////////////////////////////////////////////////
22

3-
Copyright (c) 2019, 2021 Oracle and/or its affiliates.
3+
Copyright (c) 2019, 2022 Oracle and/or its affiliates.
44

55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -121,13 +121,14 @@ HealthCheck hc = this::exampleHealthCheck;
121121
[cols="1,5",role="flex, sm7"]
122122
.Health status codes
123123
|=======
124-
| `200` | The application is healthy.
124+
| `200` | The application is healthy (with health check details in the response).
125+
| `204` | The application is healthy (with _no_ health check details in the response).
125126
| `503` | The application is not healthy.
126127
| `500` | An error occurred while reporting the health.
127128
|=======
128129
129-
The HTTP response also contains a JSON payload that describes the statuses for
130-
all health checks.
130+
HTTP `GET` responses include JSON content showing the detailed results of all the health checks which the server executed after receiving the request.
131+
HTTP `HEAD` requests return only the status with no payload.
131132
132133
[source,java]
133134
.Create the health support service:

health/health/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
3-
Copyright (c) 2018, 2021 Oracle and/or its affiliates.
3+
Copyright (c) 2018, 2022 Oracle and/or its affiliates.
44
55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -87,5 +87,10 @@
8787
<artifactId>helidon-config-yaml</artifactId>
8888
<scope>test</scope>
8989
</dependency>
90+
<dependency>
91+
<groupId>io.helidon.webclient</groupId>
92+
<artifactId>helidon-webclient</artifactId>
93+
<scope>test</scope>
94+
</dependency>
9095
</dependencies>
9196
</project>

health/health/src/main/java/io/helidon/health/HealthSupport.java

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2021 Oracle and/or its affiliates.
2+
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -117,10 +117,14 @@ public void update(Routing.Rules rules) {
117117
protected void postConfigureEndpoint(Routing.Rules defaultRules, Routing.Rules serviceEndpointRoutingRules) {
118118
if (enabled) {
119119
serviceEndpointRoutingRules
120-
.get(context(), this::callAll)
121-
.get(context() + "/live", this::callLiveness)
122-
.get(context() + "/ready", this::callReadiness)
123-
.get(context() + "/started", this::callStartup);
120+
.get(context(), this::getAll)
121+
.get(context() + "/live", this::getLiveness)
122+
.get(context() + "/ready", this::getReadiness)
123+
.get(context() + "/started", this::getStartup)
124+
.head(context(), this::headAll)
125+
.head(context() + "/live", this::headLiveness)
126+
.head(context() + "/ready", this::headReadiness)
127+
.head(context() + "/started", this::headStartup);
124128
}
125129
}
126130

@@ -130,23 +134,47 @@ private static void collectNonexcludedChecks(Builder builder, List<HealthCheck>
130134
.forEach(adder);
131135
}
132136

133-
private void callAll(ServerRequest req, ServerResponse res) {
134-
invoke(res, allChecks);
137+
private void getAll(ServerRequest req, ServerResponse res) {
138+
get(res, allChecks);
135139
}
136140

137-
private void callLiveness(ServerRequest req, ServerResponse res) {
138-
invoke(res, livenessChecks);
141+
private void getLiveness(ServerRequest req, ServerResponse res) {
142+
get(res, livenessChecks);
139143
}
140144

141-
private void callReadiness(ServerRequest req, ServerResponse res) {
142-
invoke(res, readinessChecks);
145+
private void getReadiness(ServerRequest req, ServerResponse res) {
146+
get(res, readinessChecks);
143147
}
144148

145-
private void callStartup(ServerRequest req, ServerResponse res) {
146-
invoke(res, startupChecks);
149+
private void getStartup(ServerRequest req, ServerResponse res) {
150+
get(res, startupChecks);
147151
}
148152

149-
void invoke(ServerResponse res, List<HealthCheck> healthChecks) {
153+
private void headAll(ServerRequest req, ServerResponse res) {
154+
head(res, allChecks);
155+
}
156+
157+
private void headLiveness(ServerRequest req, ServerResponse res) {
158+
head(res, livenessChecks);
159+
}
160+
161+
private void headReadiness(ServerRequest req, ServerResponse res) {
162+
head(res, readinessChecks);
163+
}
164+
165+
private void headStartup(ServerRequest req, ServerResponse res) {
166+
head(res, startupChecks);
167+
}
168+
169+
private void get(ServerResponse res, List<HealthCheck> healthChecks) {
170+
invoke(res, healthChecks, true);
171+
}
172+
173+
private void head(ServerResponse res, List<HealthCheck> healthChecks) {
174+
invoke(res, healthChecks, false);
175+
}
176+
177+
void invoke(ServerResponse res, List<HealthCheck> healthChecks, boolean sendDetails) {
150178
// timeout on the asynchronous execution
151179
Single<HealthResponse> result = timeout.invoke(() -> async.invoke(() -> callHealthChecks(healthChecks)));
152180

@@ -158,10 +186,17 @@ void invoke(ServerResponse res, List<HealthCheck> healthChecks) {
158186
});
159187

160188
result.thenAccept(hres -> {
161-
res.status(hres.status());
162-
res.send(jsonpWriter.marshall(hres.json));
189+
int status = hres.status().code();
190+
if (status == Http.Status.OK_200.code() && !sendDetails) {
191+
status = Http.Status.NO_CONTENT_204.code();
192+
}
193+
res.status(status);
194+
if (sendDetails) {
195+
res.send(jsonpWriter.marshall(hres.json));
196+
} else {
197+
res.send();
198+
}
163199
});
164-
165200
}
166201

167202
HealthResponse callHealthChecks(List<HealthCheck> healthChecks) {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package io.helidon.health;
18+
19+
import java.io.StringReader;
20+
import java.util.concurrent.ExecutionException;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.concurrent.TimeoutException;
23+
import java.util.function.Supplier;
24+
import java.util.logging.Level;
25+
import java.util.logging.Logger;
26+
27+
import jakarta.json.Json;
28+
import jakarta.json.JsonObject;
29+
30+
import io.helidon.common.http.Http;
31+
import io.helidon.common.http.MediaType;
32+
import io.helidon.media.common.MessageBodyReadableContent;
33+
import io.helidon.media.jsonp.JsonpSupport;
34+
import io.helidon.webclient.WebClient;
35+
import io.helidon.webclient.WebClientRequestBuilder;
36+
import io.helidon.webclient.WebClientResponse;
37+
import io.helidon.webserver.Routing;
38+
import io.helidon.webserver.Service;
39+
import io.helidon.webserver.WebServer;
40+
41+
import org.junit.jupiter.api.AfterAll;
42+
import org.junit.jupiter.api.BeforeAll;
43+
import org.junit.jupiter.api.Test;
44+
45+
import static org.hamcrest.MatcherAssert.assertThat;
46+
import static org.hamcrest.Matchers.equalTo;
47+
import static org.hamcrest.Matchers.greaterThan;
48+
import static org.hamcrest.Matchers.is;
49+
50+
class HealthServerTest {
51+
52+
private static final Logger LOGGER = Logger.getLogger(HealthServerTest.class.getName());
53+
54+
private static WebServer webServer;
55+
56+
private static WebClient webClient;
57+
58+
@BeforeAll
59+
static void startup() throws InterruptedException, ExecutionException, TimeoutException {
60+
HealthSupport healthSupport = HealthSupport.builder().build();
61+
webServer = startServer(healthSupport);
62+
webClient = WebClient.builder()
63+
.baseUri("http://localhost:" + webServer.port() + "/")
64+
.addMediaSupport(JsonpSupport.create())
65+
.build();
66+
}
67+
68+
@AfterAll
69+
static void shutdown() {
70+
shutdownServer(webServer);
71+
}
72+
73+
@Test
74+
void testGetAll() {
75+
checkResponse(webClient::get, "health", true);
76+
}
77+
78+
@Test
79+
void testHeadAll() {
80+
checkResponse(webClient::head, "health", false);
81+
}
82+
83+
@Test
84+
void testGetLive() {
85+
checkResponse(webClient::get, "health/live", true);
86+
}
87+
88+
@Test
89+
void testHeadLive() {
90+
checkResponse(webClient::head, "health/live", false);
91+
}
92+
93+
@Test
94+
void testGetReady() {
95+
checkResponse(webClient::get, "health/ready", true);
96+
}
97+
98+
@Test
99+
void testHeadReady() {
100+
checkResponse(webClient::head, "health/ready", false);
101+
}
102+
103+
@Test
104+
void testGetStarted() {
105+
checkResponse(webClient::get, "health/started", true);
106+
}
107+
108+
@Test
109+
void testHeadStarted() {
110+
checkResponse(webClient::head, "health/started", false);
111+
}
112+
113+
private static void checkResponse(Supplier<WebClientRequestBuilder> requestFactory,
114+
String requestPath,
115+
boolean expectContent) {
116+
117+
WebClientResponse response = null;
118+
119+
try {
120+
response = requestFactory.get()
121+
.path(requestPath)
122+
.accept(MediaType.APPLICATION_JSON)
123+
.request()
124+
.await();
125+
126+
int expectedStatus = expectContent ? Http.Status.OK_200.code() : Http.Status.NO_CONTENT_204.code();
127+
assertThat("health response status", response.status().code(), is(expectedStatus));
128+
MessageBodyReadableContent content = response.content();
129+
String textContent = null;
130+
try {
131+
textContent = content.as(String.class).get();
132+
} catch (InterruptedException | ExecutionException e) {
133+
throw new RuntimeException(e);
134+
}
135+
136+
assertThat("content length", textContent.length(), expectContent ? greaterThan(0) : equalTo(0));
137+
138+
if (expectContent) {
139+
JsonObject health = Json.createReader(new StringReader(textContent)).readObject();
140+
assertThat("health status", health.getString("status"), is("UP"));
141+
}
142+
} finally {
143+
if (response != null) {
144+
response.close();
145+
}
146+
}
147+
}
148+
149+
static WebServer startServer(
150+
Service... services) throws
151+
InterruptedException, ExecutionException, TimeoutException {
152+
WebServer result = WebServer.builder(
153+
Routing.builder()
154+
.register(services)
155+
.build())
156+
.port(0)
157+
.build()
158+
.start()
159+
.toCompletableFuture()
160+
.get(10, TimeUnit.SECONDS);
161+
LOGGER.log(Level.INFO, "Started server at: https://localhost:{0}", result.port());
162+
return result;
163+
}
164+
165+
static void shutdownServer(WebServer server) {
166+
server.shutdown();
167+
}
168+
169+
}

0 commit comments

Comments
 (0)