You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2024/09/12 07:12:58 UTC

(james-project) branch master updated: JAMES-4070 Implement query param to return 503 code if degraded (#2404)

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new fdf1d5fbaf JAMES-4070 Implement query param to return 503 code if degraded (#2404)
fdf1d5fbaf is described below

commit fdf1d5fbaf4c0aaef8ebdf7f3496a8840501edc2
Author: hungphan227 <45...@users.noreply.github.com>
AuthorDate: Thu Sep 12 14:12:52 2024 +0700

    JAMES-4070 Implement query param to return 503 code if degraded (#2404)
---
 .../modules/servers/partials/operate/webadmin.adoc |  24 ++++
 .../james/webadmin/routes/HealthCheckRoutes.java   |  62 +++++++---
 .../webadmin/routes/HealthCheckRoutesTest.java     | 126 ++++++++++++++++++++-
 src/site/markdown/server/manage-webadmin.md        |  65 +++++++++++
 4 files changed, 256 insertions(+), 21 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc b/docs/modules/servers/partials/operate/webadmin.adoc
index 8682405a30..7015e9af04 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -94,6 +94,14 @@ Response codes:
 services can still be used.
 * 503: At least one check have answered with a Unhealthy status
 
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+....
+curl -XGET http://ip:port/healthcheck?strict
+....
+
 === Check specific components
 
 Performs health checks for the given components. Components are
@@ -141,6 +149,14 @@ Response codes:
 services can still be used.
 * 503: At least one check have answered with a Unhealthy status
 
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+....
+curl -XGET http://ip:port/healthcheck?strict&check=HealthCheck1&check=HealthCheck%20two
+....
+
 === Check single component
 
 Performs a health check for the given component. The component is
@@ -168,6 +184,14 @@ Response codes:
 * 404: A component with the given name was not found.
 * 503: The check has answered with an Unhealthy status.
 
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+....
+curl -XGET http://ip:port/healthcheck/checks/{backend-name}%20backend?strict
+....
+
 === List all health checks
 
 This endpoint lists all the available health checks.
diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
index 28fcfe0cf7..275402bacf 100644
--- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
+++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/routes/HealthCheckRoutes.java
@@ -25,6 +25,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -57,6 +58,38 @@ import spark.Response;
 import spark.Service;
 
 public class HealthCheckRoutes implements PublicRoutes {
+    public enum StatusCodeConvertMode {
+        STRICT(resultStatus -> {
+            switch (resultStatus) {
+                case HEALTHY:
+                    return HttpStatus.OK_200;
+                case DEGRADED, UNHEALTHY:
+                    return HttpStatus.SERVICE_UNAVAILABLE_503;
+                default:
+                    throw new NotImplementedException(resultStatus + " is not supported");
+            }
+        }),
+        RELAXED(resultStatus -> {
+            switch (resultStatus) {
+                case HEALTHY, DEGRADED:
+                    return HttpStatus.OK_200;
+                case UNHEALTHY:
+                    return HttpStatus.SERVICE_UNAVAILABLE_503;
+                default:
+                    throw new NotImplementedException(resultStatus + " is not supported");
+            }
+        });
+
+        private Function<ResultStatus, Integer> converter;
+
+        StatusCodeConvertMode(Function<ResultStatus, Integer> converter) {
+            this.converter = converter;
+        }
+
+        public int getCorrespondingStatusCode(ResultStatus result) {
+            return converter.apply(result);
+        }
+    }
 
     private static final Logger LOGGER = LoggerFactory.getLogger(HealthCheckRoutes.class);
 
@@ -65,6 +98,7 @@ public class HealthCheckRoutes implements PublicRoutes {
 
     private static final String PARAM_COMPONENT_NAME = "componentName";
     private static final String QUERY_PARAM_COMPONENT_NAMES = "check";
+    private static final String QUERY_PARAM_STRICT = "strict";
 
     private final JsonTransformer jsonTransformer;
     private final Set<HealthCheck> healthChecks;
@@ -89,15 +123,17 @@ public class HealthCheckRoutes implements PublicRoutes {
 
     public Object validateHealthChecks(Request request, Response response) {
         Set<ComponentName> selectedComponentNames = getComponentNames(request);
+        StatusCodeConvertMode convertMode = getStatusCodeConvertMode(request);
         Collection<HealthCheck> selectedHealthChecks = selectHealthChecks(selectedComponentNames);
         List<Result> results = executeHealthChecks(selectedHealthChecks).collectList().block();
         ResultStatus status = retrieveAggregationStatus(results);
-        response.status(getCorrespondingStatusCode(status));
+        response.status(convertMode.getCorrespondingStatusCode(status));
         return new HeathCheckAggregationExecutionResultDto(status, mapResultToDto(results));
     }
 
     public Object performHealthCheckForComponent(Request request, Response response) {
         String componentName = request.params(PARAM_COMPONENT_NAME);
+        StatusCodeConvertMode convertMode = getStatusCodeConvertMode(request);
         HealthCheck healthCheck = healthChecks.stream()
             .filter(c -> c.componentName().getName().equals(componentName))
             .findFirst()
@@ -105,7 +141,7 @@ public class HealthCheckRoutes implements PublicRoutes {
 
         Result result = Mono.from(healthCheck.check()).block();
         logFailedCheck(result);
-        response.status(getCorrespondingStatusCode(result.getStatus()));
+        response.status(convertMode.getCorrespondingStatusCode(result.getStatus()));
         return new HealthCheckExecutionResultDto(result);
     }
 
@@ -131,6 +167,12 @@ public class HealthCheckRoutes implements PublicRoutes {
             .collect(ImmutableSet.toImmutableSet());
     }
 
+    private StatusCodeConvertMode getStatusCodeConvertMode(Request request) {
+        return Optional.ofNullable(request.queryParamsValues(QUERY_PARAM_STRICT))
+            .map(s -> StatusCodeConvertMode.STRICT)
+            .orElse(StatusCodeConvertMode.RELAXED);
+    }
+
     private Collection<HealthCheck> getHealthChecks(Set<ComponentName> selectedComponentNames) {
         Set<ComponentName> componentNames = healthChecks.stream().map(HealthCheck::componentName).collect(ImmutableSet.toImmutableSet());
         List<ComponentName> nonExistedComponentNames = selectedComponentNames.stream()
@@ -145,18 +187,6 @@ public class HealthCheckRoutes implements PublicRoutes {
             .toList();
     }
 
-    private int getCorrespondingStatusCode(ResultStatus resultStatus) {
-        switch (resultStatus) {
-            case HEALTHY:
-            case DEGRADED:
-                return HttpStatus.OK_200;
-            case UNHEALTHY:
-                return HttpStatus.SERVICE_UNAVAILABLE_503;
-            default:
-                throw new NotImplementedException(resultStatus + " is not supported");
-        }
-    }
-
     private void logFailedCheck(Result result) {
         switch (result.getStatus()) {
             case UNHEALTHY:
@@ -189,10 +219,6 @@ public class HealthCheckRoutes implements PublicRoutes {
         }
     }
 
-    private Flux<Result> executeHealthChecks() {
-        return executeHealthChecks(healthChecks);
-    }
-
     private Flux<Result> executeHealthChecks(Collection<HealthCheck> healthChecks) {
         return Flux.fromIterable(healthChecks)
             .flatMap(HealthCheck::check, DEFAULT_CONCURRENCY)
diff --git a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
index e54850f98d..819118addd 100644
--- a/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-core/src/test/java/org/apache/james/webadmin/routes/HealthCheckRoutesTest.java
@@ -208,7 +208,7 @@ class HealthCheckRoutesTest {
     }
 
     @Test
-    void validateHealthChecksShouldReturnInternalErrorWhenOneHealthCheckIsDegraded() {
+    void validateHealthChecksShouldReturnOkWhenOneHealthCheckIsDegraded() {
         healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "cause")));
         healthChecks.add(healthCheck(Result.healthy(COMPONENT_NAME_2)));
 
@@ -241,7 +241,7 @@ class HealthCheckRoutesTest {
     }
 
     @Test
-    void validateHealthChecksShouldReturnInternalErrorWhenAllHealthCheckAreDegraded() {
+    void validateHealthChecksShouldReturnOkWhenAllHealthCheckAreDegraded() {
         healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "cause")));
         healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_2, "cause")));
 
@@ -304,6 +304,78 @@ class HealthCheckRoutesTest {
             .isEqualTo(healthCheckBody);
     }
 
+    @Test
+    void validateHealthChecksShouldReturn503WhenOneHealthCheckIsDegradedAndStrictModeIsEnabled() {
+        healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "cause")));
+        healthChecks.add(healthCheck(Result.healthy(COMPONENT_NAME_2)));
+
+        String healthCheckBody =
+            "{\"status\": \"degraded\"," +
+                " \"checks\": [" +
+                "  {" +
+                "    \"componentName\": \"component-1\"," +
+                "    \"escapedComponentName\": \"component-1\"," +
+                "    \"status\": \"degraded\"," +
+                "    \"cause\": \"cause\"" +
+                "  }," +
+                "  {" +
+                "    \"componentName\": \"component-2\"," +
+                "    \"escapedComponentName\": \"component-2\"," +
+                "    \"status\": \"healthy\"," +
+                "    \"cause\": null" +
+                "}]}";
+
+        String retrieveBody =
+            given()
+                .queryParam("strict")
+            .when()
+                .get()
+            .then()
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
+                .extract()
+                .body().asString();
+
+        assertThatJson(retrieveBody)
+            .when(Option.IGNORING_ARRAY_ORDER)
+            .isEqualTo(healthCheckBody);
+    }
+
+    @Test
+    void validateHealthChecksShouldReturn503WhenAllHealthCheckAreDegradedAndStrictModeIsEnabled() {
+        healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "cause")));
+        healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_2, "cause")));
+
+        String healthCheckBody =
+            "{\"status\": \"degraded\"," +
+                " \"checks\": [" +
+                "  {" +
+                "    \"componentName\": \"component-1\"," +
+                "    \"escapedComponentName\": \"component-1\"," +
+                "    \"status\": \"degraded\"," +
+                "    \"cause\": \"cause\"" +
+                "  }," +
+                "  {" +
+                "    \"componentName\": \"component-2\"," +
+                "    \"escapedComponentName\": \"component-2\"," +
+                "    \"status\": \"degraded\"," +
+                "    \"cause\": \"cause\"" +
+                "}]}";
+
+        String retrieveBody =
+            given()
+                .queryParam("strict")
+            .when()
+                .get()
+            .then()
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
+                .extract()
+                .body().asString();
+
+        assertThatJson(retrieveBody)
+            .when(Option.IGNORING_ARRAY_ORDER)
+            .isEqualTo(healthCheckBody);
+    }
+
     @Test
     void validateHealthChecksShouldReturnOkWhenSelectedHealthCheckIsHealthy() {
         healthChecks.add(healthCheck(Result.healthy(COMPONENT_NAME_1)));
@@ -371,6 +443,37 @@ class HealthCheckRoutesTest {
             .isEqualTo(healthCheckBody);
     }
 
+    @Test
+    void validateHealthChecksShouldReturn503WhenSelectedHealthCheckIsDegradedAndStrictModeIsEnabled() {
+        healthChecks.add(healthCheck(Result.healthy(COMPONENT_NAME_1)));
+        healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_2, "cause")));
+
+        String healthCheckBody =
+            "{\"status\": \"degraded\"," +
+                " \"checks\": [" +
+                "  {" +
+                "    \"componentName\": \"component-2\"," +
+                "    \"escapedComponentName\": \"component-2\"," +
+                "    \"status\": \"degraded\"," +
+                "    \"cause\": \"cause\"" +
+                "}]}";
+
+        String retrieveBody =
+            given()
+                .queryParam("strict")
+                .queryParam("check", "component-2")
+            .when()
+                .get()
+            .then()
+                .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
+                .extract()
+                .body().asString();
+
+        assertThatJson(retrieveBody)
+            .when(Option.IGNORING_ARRAY_ORDER)
+            .isEqualTo(healthCheckBody);
+    }
+
     @Test
     void validateHealthChecksShouldReturnStatusUnHealthyWhenOneOfSelectedHealthChecksIsUnhealthy() {
         healthChecks.add(healthCheck(Result.healthy(COMPONENT_NAME_1)));
@@ -469,7 +572,7 @@ class HealthCheckRoutesTest {
     }
     
     @Test
-    void performHealthCheckShouldReturnInternalErrorWhenHealthCheckIsDegraded() {
+    void performHealthCheckShouldReturnOkWhenHealthCheckIsDegraded() {
         healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "the cause")));
 
         given()
@@ -499,6 +602,23 @@ class HealthCheckRoutesTest {
             .body("status", equalTo(ResultStatus.UNHEALTHY.getValue()))
             .body("cause", is(SAMPLE_CAUSE));
     }
+
+    @Test
+    void performHealthCheckShouldReturn503WhenHealthCheckIsDegradedAndStrictModeIsEnabled() {
+        healthChecks.add(healthCheck(Result.degraded(COMPONENT_NAME_1, "the cause")));
+
+        given()
+            .pathParam("componentName", COMPONENT_NAME_1.getName())
+            .queryParam("strict")
+        .when()
+            .get("/checks/{componentName}")
+        .then()
+            .statusCode(HttpStatus.SERVICE_UNAVAILABLE_503)
+            .body("componentName", equalTo(NAME_1))
+            .body("escapedComponentName", equalTo(NAME_1))
+            .body("status", equalTo(ResultStatus.DEGRADED.getValue()))
+            .body("cause", equalTo("the cause"));
+    }
     
     @Test
     void performHealthCheckShouldReturnProperlyEscapedComponentName() {
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 95b3fb6345..2fb22542b7 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -122,6 +122,63 @@ Response codes:
  - 200: All checks have answered with a Healthy or Degraded status. James services can still be used.
  - 503: At least one check have answered with a Unhealthy status
 
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+```
+curl -XGET http://ip:port/healthcheck?strict
+```
+
+### Check specific components
+
+Performs health checks for the given components. Components are referenced by their URL encoded names.
+
+```
+curl -XGET http://ip:port/healthcheck?check=HealthCheck1&check=HealthCheck%20two
+```
+
+Will return a list of healthChecks execution result, with an aggregated result:
+
+```
+{
+  "status": "healthy",
+  "checks": [
+    {
+      "componentName": "HealthCheck1",
+      "escapedComponentName": "HealthCheck1",
+      "status": "healthy"
+      "cause": null
+    },
+    {
+      "componentName": "HealthCheck two",
+      "escapedComponentName": "HealthCheck%20two",
+      "status": "healthy"
+      "cause": null
+    }
+  ]
+}
+```
+
+*status* field can be:
+
+* *healthy*: Component works normally
+* *degraded*: Component works in degraded mode. Some non-critical services may not be working, or latencies are high, for example. Cause contains explanations.
+* *unhealthy*: The component is currently not working. Cause contains explanations.
+
+Response codes:
+
+* 200: All checks have answered with a Healthy or Degraded status. James services can still be used.
+* 503: At least one check have answered with a Unhealthy status
+
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+```
+curl -XGET http://ip:port/healthcheck?strict&check=HealthCheck1&check=HealthCheck%20two
+```
+
 ### Check single component
 
 Performs a health check for the given component. The component is referenced by its URL encoded name.
@@ -146,6 +203,14 @@ Response codes:
  - 200: The check has answered with a Healthy or Degraded status.
  - 404: A component with the given name was not found.
  - 503: The check has anwered with a Unhealthy status.
+
+Additional query parameters are supported:
+
+- `strict` allows you enable the strict mode. In this mode, if any checks have the result of Degraded or Unhealthy status, the response code will be 500. If omitted, degraded checks would be reported with status code 200.
+
+```
+curl -XGET http://ip:port/healthcheck/checks/{backend-name}%20backend?strict
+```
  
 ### List all health checks
  


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org