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

jerryshao pushed a commit to branch 1.2.0-hotfix
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/1.2.0-hotfix by this push:
     new fd738b9946 [#11101] feat(iceberg-rest-server): Add health check 
endpoints to the Iceberg REST Catalog server (#11102)
fd738b9946 is described below

commit fd738b99468c2a92e011f8dfa7ce275a58795d50
Author: Jerry Shao <[email protected]>
AuthorDate: Fri May 15 16:55:53 2026 +0800

    [#11101] feat(iceberg-rest-server): Add health check endpoints to the 
Iceberg REST Catalog server (#11102)
    
    Adds MicroProfile-style health check endpoints to the Iceberg REST
    Catalog (IRC) server, mirroring the existing endpoints on the main
    Gravitino server:
    
    - `GET /iceberg/health/live` — liveness probe; always 200 while the HTTP
    thread is responsive
    - `GET /iceberg/health/ready` — readiness probe; 200 when
    `IcebergCatalogWrapperManager` is initialized, 503 otherwise
    - `GET /iceberg/health` — aggregate; 200 only when both pass
    
    Root-level GTM aliases (`/health/*`, `/health.html`) are registered
    unconditionally (standalone and auxiliary mode) and forward to
    `/iceberg/health*`.
    
    All health paths bypass `IcebergAuthenticationFilter` so infrastructure
    probes work without credentials.
    
    `HealthAliasServlet` is moved from `:server` to `:server-common` so it
    can be shared by both modules.
    `AuthenticationFilter.isHealthCheckRequest` is widened from `private` to
    `protected` to allow the override in `IcebergAuthenticationFilter`.
    
    The IRC server runs on its own Jetty instance and has no health
    endpoints, making it impossible for Kubernetes probes, load balancers,
    and enterprise GTMs to monitor its availability.
    
    Fix: #11101
    
    Yes — three new REST endpoints are available on the IRC server: `GET
    /iceberg/health`, `GET /iceberg/health/live`, `GET
    /iceberg/health/ready`, plus root-level GTM aliases `/health`,
    `/health/live`, `/health/ready`, `/health.html`.
    
    - `TestIcebergHealthOperations` — 5 unit tests covering all UP/DOWN
    combinations for liveness, readiness, and aggregate endpoints
    - `TestIcebergAuthenticationFilter` — 2 new parametrized tests verifying
    `/iceberg/health*` paths bypass auth and non-health paths do not
    - `TestHealthAliasServlet` — extended with custom-prefix cases covering
    `/iceberg` forwarding
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 .../org/apache/gravitino/iceberg/RESTService.java  |   6 +
 .../service/rest/IcebergHealthOperations.java      | 138 +++++++++++++++++++++
 .../service/rest/TestIcebergHealthOperations.java  |  94 ++++++++++++++
 .../authentication/AuthenticationFilter.java       |  15 ++-
 .../gravitino/server/web/HealthAliasServlet.java   |  31 +++--
 .../org/apache/gravitino/server/web/Utils.java     |   7 ++
 .../authentication/TestAuthenticationFilter.java   |  12 +-
 .../server/web/TestHealthAliasServlet.java         |  20 +++
 .../server/web/rest/HealthOperations.java          |  24 +---
 .../server/web/rest/VersionOperations.java         |   3 +-
 10 files changed, 318 insertions(+), 32 deletions(-)

diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
index 23f1fcf042..2ce3e6fde8 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
@@ -49,6 +49,7 @@ import 
org.apache.gravitino.iceberg.service.provider.IcebergConfigProviderFactor
 import org.apache.gravitino.listener.EventBus;
 import org.apache.gravitino.metrics.MetricsSystem;
 import org.apache.gravitino.metrics.source.MetricsSource;
+import org.apache.gravitino.server.web.HealthAliasServlet;
 import org.apache.gravitino.server.web.HttpServerMetricsSource;
 import org.apache.gravitino.server.web.JettyServer;
 import org.apache.gravitino.server.web.JettyServerConfig;
@@ -154,6 +155,11 @@ public class RESTService implements 
GravitinoAuxiliaryService {
     server.addServlet(servlet, ICEBERG_SPEC);
     server.addCustomFilters(ICEBERG_SPEC);
     server.addSystemFilters(ICEBERG_SPEC);
+
+    // Root-level aliases for health checks to improve compatibility with 
various monitoring
+    // systems that expect a /health endpoint.
+    server.addServlet(new HealthAliasServlet("/iceberg"), "/health/*");
+    server.addServlet(new HealthAliasServlet("/iceberg"), "/health.html");
   }
 
   @Override
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergHealthOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergHealthOperations.java
new file mode 100644
index 0000000000..e91fe5e0a8
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergHealthOperations.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.iceberg.service.rest;
+
+import com.codahale.metrics.annotation.ResponseMetered;
+import com.codahale.metrics.annotation.Timed;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.HealthCheckDTO;
+import org.apache.gravitino.dto.responses.HealthResponse;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+
+/**
+ * Health check endpoints for the Iceberg REST server. Follows the same 
MicroProfile Health
+ * semantics as the main Gravitino server.
+ *
+ * <ul>
+ *   <li>{@code GET /iceberg/health/live} — liveness, 200 as long as the HTTP 
thread can respond
+ *   <li>{@code GET /iceberg/health/ready} — readiness, 200 when the catalog 
wrapper manager is
+ *       initialized
+ *   <li>{@code GET /iceberg/health} — aggregate, 200 when both pass
+ * </ul>
+ *
+ * All endpoints return 503 with a JSON body describing the failed check(s) 
when unhealthy.
+ */
+@Path("/health")
+@Produces(MediaType.APPLICATION_JSON)
+public class IcebergHealthOperations {
+
+  private static final String CHECK_HTTP_SERVER = "httpServer";
+  private static final String CHECK_CATALOG_WRAPPER_MANAGER = 
"catalogWrapperManager";
+
+  @Inject private IcebergCatalogWrapperManager catalogWrapperManager;
+
+  /** Default constructor for Jersey auto-discovery. */
+  public IcebergHealthOperations() {}
+
+  /**
+   * Liveness probe. Returns 200 as long as the HTTP thread can respond.
+   *
+   * @return 200 OK with an UP {@link HealthResponse}
+   */
+  @GET
+  @Path("/live")
+  @Timed(name = "iceberg.health.live." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "iceberg.health.live", absolute = true)
+  public Response live() {
+    HealthCheckDTO check = up(CHECK_HTTP_SERVER, Collections.emptyMap());
+    return Utils.ok(new HealthResponse(HealthCheckDTO.Status.UP, 
Collections.singletonList(check)));
+  }
+
+  /**
+   * Readiness probe. Returns 200 when the {@link 
IcebergCatalogWrapperManager} is initialized, 503
+   * otherwise.
+   *
+   * @return 200 OK when ready, 503 Service Unavailable with a DOWN {@link 
HealthResponse} otherwise
+   */
+  @GET
+  @Path("/ready")
+  @Timed(name = "iceberg.health.ready." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "iceberg.health.ready", absolute = true)
+  public Response ready() {
+    HealthCheckDTO managerCheck = checkCatalogWrapperManager();
+    HealthCheckDTO.Status overall = managerCheck.getStatus();
+    HealthResponse body = new HealthResponse(overall, 
Collections.singletonList(managerCheck));
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
+  }
+
+  /**
+   * Aggregate health check. Returns 200 when both liveness and readiness 
pass, 503 otherwise.
+   *
+   * @return 200 OK when healthy, 503 Service Unavailable with failing checks 
described in the body
+   */
+  @GET
+  @Timed(name = "iceberg.health." + MetricNames.HTTP_PROCESS_DURATION, 
absolute = true)
+  @ResponseMetered(name = "iceberg.health", absolute = true)
+  public Response health() {
+    List<HealthCheckDTO> checks = new ArrayList<>(2);
+    checks.add(up(CHECK_HTTP_SERVER, Collections.emptyMap()));
+    checks.add(checkCatalogWrapperManager());
+
+    HealthCheckDTO.Status overall =
+        checks.stream().anyMatch(c -> c.getStatus() == 
HealthCheckDTO.Status.DOWN)
+            ? HealthCheckDTO.Status.DOWN
+            : HealthCheckDTO.Status.UP;
+
+    HealthResponse body = new HealthResponse(overall, checks);
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
+  }
+
+  private HealthCheckDTO checkCatalogWrapperManager() {
+    if (getCatalogWrapperManager() == null) {
+      return down(
+          CHECK_CATALOG_WRAPPER_MANAGER, "reason", "catalog wrapper manager 
not initialized");
+    }
+    return up(CHECK_CATALOG_WRAPPER_MANAGER, Collections.emptyMap());
+  }
+
+  /** Visible for testing — subclasses override to inject a different manager 
instance. */
+  IcebergCatalogWrapperManager getCatalogWrapperManager() {
+    return catalogWrapperManager;
+  }
+
+  private static HealthCheckDTO up(String name, Map<String, String> details) {
+    return new HealthCheckDTO(name, HealthCheckDTO.Status.UP, details);
+  }
+
+  private static HealthCheckDTO down(String name, String detailKey, String 
detailValue) {
+    return new HealthCheckDTO(
+        name, HealthCheckDTO.Status.DOWN, Collections.singletonMap(detailKey, 
detailValue));
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergHealthOperations.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergHealthOperations.java
new file mode 100644
index 0000000000..376479140c
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergHealthOperations.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.iceberg.service.rest;
+
+import static org.mockito.Mockito.mock;
+
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.HealthCheckDTO;
+import org.apache.gravitino.dto.responses.HealthResponse;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergHealthOperations {
+
+  private static IcebergHealthOperations operationsWithManager(
+      IcebergCatalogWrapperManager manager) {
+    return new IcebergHealthOperations() {
+      @Override
+      IcebergCatalogWrapperManager getCatalogWrapperManager() {
+        return manager;
+      }
+    };
+  }
+
+  @Test
+  public void testLiveReturns200() {
+    IcebergHealthOperations ops = operationsWithManager(null);
+    Response resp = ops.live();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+  }
+
+  @Test
+  public void testReadyReturns200WhenManagerInitialized() {
+    IcebergCatalogWrapperManager manager = 
mock(IcebergCatalogWrapperManager.class);
+    IcebergHealthOperations ops = operationsWithManager(manager);
+    Response resp = ops.ready();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+  }
+
+  @Test
+  public void testReadyReturns503WhenManagerNotInitialized() {
+    IcebergHealthOperations ops = operationsWithManager(null);
+    Response resp = ops.ready();
+    
Assertions.assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.DOWN, body.getStatus());
+    Assertions.assertFalse(body.getChecks().isEmpty());
+    Assertions.assertEquals("catalogWrapperManager", 
body.getChecks().get(0).getName());
+  }
+
+  @Test
+  public void testHealthReturns200WhenManagerInitialized() {
+    IcebergCatalogWrapperManager manager = 
mock(IcebergCatalogWrapperManager.class);
+    IcebergHealthOperations ops = operationsWithManager(manager);
+    Response resp = ops.health();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.UP, body.getStatus());
+    Assertions.assertEquals(2, body.getChecks().size());
+  }
+
+  @Test
+  public void testHealthReturns503WhenManagerNotInitialized() {
+    IcebergHealthOperations ops = operationsWithManager(null);
+    Response resp = ops.health();
+    
Assertions.assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), 
resp.getStatus());
+    HealthResponse body = (HealthResponse) resp.getEntity();
+    Assertions.assertEquals(HealthCheckDTO.Status.DOWN, body.getStatus());
+    boolean hasCatalogCheck =
+        body.getChecks().stream().anyMatch(c -> 
"catalogWrapperManager".equals(c.getName()));
+    Assertions.assertTrue(hasCatalogCheck);
+  }
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
index 37bb5af9b4..2a91858081 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authentication/AuthenticationFilter.java
@@ -132,7 +132,14 @@ public class AuthenticationFilter implements Filter {
     response.sendError(status, exception.getMessage());
   }
 
-  private static boolean isHealthCheckRequest(ServletRequest request) {
+  /**
+   * Returns {@code true} if the request targets a health check endpoint that 
should bypass
+   * authentication. Subclasses may override this to add additional bypass 
paths.
+   *
+   * @param request the incoming servlet request
+   * @return {@code true} if the request should skip authentication
+   */
+  protected boolean isHealthCheckRequest(ServletRequest request) {
     if (!(request instanceof HttpServletRequest)) {
       return false;
     }
@@ -142,11 +149,15 @@ public class AuthenticationFilter implements Filter {
     }
     // Also match /health, /health/*, and /health.html — root-level aliases 
that forward to
     // /api/health/*. During a forward, getRequestURI() returns the original 
URI, not the target.
+    // Also match /iceberg/health and /iceberg/health/* for the Iceberg REST 
server health
+    // endpoints.
     return path.equals("/health")
         || path.startsWith("/health/")
         || path.equals("/health.html")
         || path.equals("/api/health")
-        || path.startsWith("/api/health/");
+        || path.startsWith("/api/health/")
+        || path.equals("/iceberg/health")
+        || path.startsWith("/iceberg/health/");
   }
 
   @Override
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java 
b/server-common/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java
similarity index 62%
rename from 
server/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java
rename to 
server-common/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java
index 532bc44e4a..c9d2c75722 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/web/HealthAliasServlet.java
@@ -26,22 +26,39 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 /**
- * Serves root-level health paths ({@code /health}, {@code /health/live}, 
{@code /health/ready},
- * {@code /health.html}) by forwarding to the canonical {@code /api/health/*} 
endpoints.
+ * Forwards root-level health probe paths to canonical health endpoints.
+ *
+ * <p>The no-arg constructor targets {@code /api/health} (for the main 
Gravitino server). Pass a
+ * custom {@code targetPrefix} (e.g. {@code "/iceberg"}) to forward to a 
different base path.
  *
  * <p>This alias exists for compatibility with enterprise global traffic 
managers that require
- * probes at well-known root paths. The canonical implementation remains at 
{@code /api/health}.
+ * probes at well-known root paths such as {@code /health}, {@code 
/health/live}, {@code
+ * /health/ready}, and {@code /health.html}.
  */
 public class HealthAliasServlet extends HttpServlet {
 
+  private final String targetPrefix;
+
+  /** Forwards to {@code /api/health*} (main Gravitino server default). */
+  public HealthAliasServlet() {
+    this.targetPrefix = "/api";
+  }
+
+  /**
+   * Forwards to {@code <targetPrefix>/health*}.
+   *
+   * @param targetPrefix the path prefix of the canonical health endpoint, 
e.g. {@code "/iceberg"}
+   */
+  public HealthAliasServlet(String targetPrefix) {
+    this.targetPrefix = targetPrefix;
+  }
+
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp)
       throws ServletException, IOException {
-    // Map root-level health paths to their canonical /api/health counterparts.
-    // /health and /health.html both target the aggregate /api/health; legacy 
GTM
-    // standards sometimes hardcode the .html extension.
+    // /health.html maps to the aggregate endpoint; other paths keep their 
sub-path.
     String uri = req.getRequestURI();
-    String targetPath = "/health.html".equals(uri) ? "/api/health" : "/api" + 
uri;
+    String targetPath = "/health.html".equals(uri) ? targetPrefix + "/health" 
: targetPrefix + uri;
     RequestDispatcher dispatcher = req.getRequestDispatcher(targetPath);
     if (dispatcher == null) {
       resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "health 
dispatcher unavailable");
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java 
b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
index 6cf48f6e95..4ef45b6a50 100644
--- a/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
+++ b/server-common/src/main/java/org/apache/gravitino/server/web/Utils.java
@@ -191,6 +191,13 @@ public class Utils {
         .build();
   }
 
+  public static <T> Response serviceUnavailable(T t) {
+    return Response.status(Response.Status.SERVICE_UNAVAILABLE)
+        .entity(t)
+        .type(MediaType.APPLICATION_JSON)
+        .build();
+  }
+
   public static Response doAs(
       HttpServletRequest httpRequest, PrivilegedExceptionAction<Response> 
action) throws Exception {
     UserPrincipal principal =
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestAuthenticationFilter.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestAuthenticationFilter.java
index 8c227727b4..9878841533 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestAuthenticationFilter.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestAuthenticationFilter.java
@@ -136,7 +136,10 @@ public class TestAuthenticationFilter {
       "/api/health",
       "/api/health/",
       "/api/health/live",
-      "/api/health/ready"
+      "/api/health/ready",
+      "/iceberg/health",
+      "/iceberg/health/live",
+      "/iceberg/health/ready"
     };
     for (String path : healthPaths) {
       Authenticator authenticator = mock(Authenticator.class);
@@ -161,7 +164,12 @@ public class TestAuthenticationFilter {
     // Regression guard against an overly broad exemption. Paths that merely 
contain
     // "health" or share a prefix with "/api/health" must still be 
authenticated.
     String[] nonHealthPaths = {
-      "/api/metalakes/health_metalake", "/api/healthcheck", "/api/version", 
"/api/metalakes"
+      "/api/metalakes/health_metalake",
+      "/api/healthcheck",
+      "/api/version",
+      "/api/metalakes",
+      "/iceberg/healthcheck",
+      "/iceberg/v1/namespaces"
     };
     for (String path : nonHealthPaths) {
       Authenticator authenticator = mock(Authenticator.class);
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
 
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
similarity index 76%
rename from 
server/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
rename to 
server-common/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
index 1d0e6e3e8a..b753de0ebe 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHealthAliasServlet.java
@@ -51,6 +51,26 @@ public class TestHealthAliasServlet {
     verify(dispatcher).forward(req, resp);
   }
 
+  @ParameterizedTest
+  @CsvSource({
+    "/health,        /iceberg/health",
+    "/health.html,   /iceberg/health",
+    "/health/live,   /iceberg/health/live",
+    "/health/ready,  /iceberg/health/ready"
+  })
+  public void testDoGetForwardsWithCustomPrefix(String incoming, String 
expected) throws Exception {
+    HealthAliasServlet servlet = new HealthAliasServlet("/iceberg");
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+    when(req.getRequestURI()).thenReturn(incoming.strip());
+    when(req.getRequestDispatcher(expected.strip())).thenReturn(dispatcher);
+
+    servlet.doGet(req, resp);
+
+    verify(dispatcher).forward(req, resp);
+  }
+
   @Test
   public void testDoGetReturns503WhenDispatcherIsNull() throws Exception {
     HealthAliasServlet servlet = new HealthAliasServlet();
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/HealthOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/HealthOperations.java
index 60c44aeb58..ea2aa6ffc0 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/HealthOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/HealthOperations.java
@@ -34,7 +34,6 @@ import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
-import javax.servlet.http.HttpServlet;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -48,6 +47,7 @@ import org.apache.gravitino.dto.HealthCheckDTO;
 import org.apache.gravitino.dto.responses.HealthResponse;
 import org.apache.gravitino.metrics.MetricNames;
 import org.apache.gravitino.server.ServerConfig;
+import org.apache.gravitino.server.web.Utils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -66,7 +66,7 @@ import org.slf4j.LoggerFactory;
  */
 @Path("/health")
 @Produces(MediaType.APPLICATION_JSON)
-public class HealthOperations extends HttpServlet {
+public class HealthOperations {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(HealthOperations.class);
 
@@ -110,7 +110,7 @@ public class HealthOperations extends HttpServlet {
   @ResponseMetered(name = "health.live", absolute = true)
   public Response live() {
     HealthCheckDTO check = up(CHECK_HTTP_SERVER, Collections.emptyMap());
-    return ok(new HealthResponse(HealthCheckDTO.Status.UP, 
Collections.singletonList(check)));
+    return Utils.ok(new HealthResponse(HealthCheckDTO.Status.UP, 
Collections.singletonList(check)));
   }
 
   @GET
@@ -122,7 +122,7 @@ public class HealthOperations extends HttpServlet {
     HealthCheckDTO entityStoreCheck = checkEntityStore();
     HealthCheckDTO.Status overall = entityStoreCheck.getStatus();
     HealthResponse body = new HealthResponse(overall, 
Collections.singletonList(entityStoreCheck));
-    return overall == HealthCheckDTO.Status.UP ? ok(body) : 
serviceUnavailable(body);
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
   }
 
   @GET
@@ -140,7 +140,7 @@ public class HealthOperations extends HttpServlet {
             : HealthCheckDTO.Status.UP;
 
     HealthResponse body = new HealthResponse(overall, checks);
-    return overall == HealthCheckDTO.Status.UP ? ok(body) : 
serviceUnavailable(body);
+    return overall == HealthCheckDTO.Status.UP ? Utils.ok(body) : 
Utils.serviceUnavailable(body);
   }
 
   private HealthCheckDTO checkEntityStore() {
@@ -227,18 +227,4 @@ public class HealthOperations extends HttpServlet {
     return new HealthCheckDTO(
         name, HealthCheckDTO.Status.DOWN, Collections.singletonMap(detailKey, 
detailValue));
   }
-
-  private static Response ok(HealthResponse body) {
-    return Response.status(Response.Status.OK)
-        .entity(body)
-        .type(MediaType.APPLICATION_JSON)
-        .build();
-  }
-
-  private static Response serviceUnavailable(HealthResponse body) {
-    return Response.status(Response.Status.SERVICE_UNAVAILABLE)
-        .entity(body)
-        .type(MediaType.APPLICATION_JSON)
-        .build();
-  }
 }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/VersionOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/VersionOperations.java
index 0491e87873..8da2767f4e 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/VersionOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/VersionOperations.java
@@ -20,7 +20,6 @@ package org.apache.gravitino.server.web.rest;
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
-import javax.servlet.http.HttpServlet;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -35,7 +34,7 @@ import org.apache.gravitino.server.web.Utils;
 @Path("/version")
 @Consumes(MediaType.APPLICATION_JSON)
 @Produces(MediaType.APPLICATION_JSON)
-public class VersionOperations extends HttpServlet {
+public class VersionOperations {
   @GET
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "version." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)

Reply via email to