This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 4207d3fca8 [#11553] feat(lance): add health check endpoints for Lance
REST servers (#11558)
4207d3fca8 is described below
commit 4207d3fca8d09ef9ee85ed1dd49121a3b864d753
Author: tian bao <[email protected]>
AuthorDate: Tue Jun 30 17:52:18 2026 +0800
[#11553] feat(lance): add health check endpoints for Lance REST servers
(#11558)
### What changes were proposed in this pull request?
This PR adds health check endpoints and a custom authentication filter
for the Lance REST server, aligning it with the Iceberg REST server
implementation.
1、LanceAuthenticationFilter — A new subclass of AuthenticationFilter
that whitelists /lance/health and /lance/health/* paths to bypass
authentication, in addition to the default health paths (/health,
/api/health). This allows Kubernetes probes and monitoring systems to
reach the Lance health endpoints without credentials.
2、LanceHealthOperations — A new JAX-RS resource at /health/live that
returns a MicroProfile Health-compatible liveness response (200 OK with
UP status).
3、LanceRESTService — Updated to inject LanceAuthenticationFilter into
the Jetty server via createAuthenticationFilter() override.
4、build.gradle.kts — Added testImplementation(libs.junit.jupiter.params)
dependency for @ParameterizedTest support.
### Why are the changes needed?
- Health check endpoints are required for Kubernetes liveness probes and
monitoring integration.
- Without LanceAuthenticationFilter, all Lance REST endpoints (including
health checks) require authentication, making health probes fail.
#11553
### Does this PR introduce _any_ user-facing change?
Yes. The Lance REST server now exposes:
- GET /lance/health/live — Liveness probe endpoint (no authentication
required).
### How was this patch tested?
Added unit tests:
- TestLanceAuthenticationFilter — Verifies health paths bypass auth and
non-health paths require auth.
- TestLanceHealthOperations — Verifies the liveness endpoint returns 200
with UP status.
---
.../lance/common/ops/NamespaceWrapper.java | 4 +
lance/lance-rest-server/build.gradle.kts | 1 +
.../apache/gravitino/lance/LanceJettyServer.java | 35 +++++
.../apache/gravitino/lance/LanceRESTService.java | 13 +-
.../lance/service/LanceAuthenticationFilter.java | 86 +++++++++++
.../lance/service/LanceHealthCheckPathMatcher.java | 41 ++++++
.../lance/service/rest/LanceHealthOperations.java | 142 +++++++++++++++++++
.../service/TestLanceAuthenticationFilter.java | 157 +++++++++++++++++++++
.../service/rest/TestLanceHealthOperations.java | 96 +++++++++++++
9 files changed, 573 insertions(+), 2 deletions(-)
diff --git
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
index 936a5a7069..5973fa4f52 100644
---
a/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
+++
b/lance/lance-common/src/main/java/org/apache/gravitino/lance/common/ops/NamespaceWrapper.java
@@ -53,6 +53,10 @@ public abstract class NamespaceWrapper {
return tableOps;
}
+ public boolean isInitialized() {
+ return initialized;
+ }
+
public LanceConfig config() {
return config;
}
diff --git a/lance/lance-rest-server/build.gradle.kts
b/lance/lance-rest-server/build.gradle.kts
index 127eac64d5..3235b0dc17 100644
--- a/lance/lance-rest-server/build.gradle.kts
+++ b/lance/lance-rest-server/build.gradle.kts
@@ -133,6 +133,7 @@ dependencies {
}
testImplementation(libs.junit.jupiter.api)
+ testImplementation(libs.junit.jupiter.params)
testImplementation(libs.mockito.inline)
testImplementation(libs.mysql.driver)
testImplementation(libs.postgresql.driver)
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
new file mode 100644
index 0000000000..be8b8dbff5
--- /dev/null
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceJettyServer.java
@@ -0,0 +1,35 @@
+/*
+ * 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.lance;
+
+import javax.servlet.Filter;
+import org.apache.gravitino.lance.service.LanceAuthenticationFilter;
+import org.apache.gravitino.server.web.JettyServer;
+
+/**
+ * A {@link JettyServer} subclass that creates a {@link
LanceAuthenticationFilter} for the Lance
+ * REST service authentication layer.
+ */
+class LanceJettyServer extends JettyServer {
+
+ @Override
+ protected Filter createAuthenticationFilter() {
+ return new LanceAuthenticationFilter();
+ }
+}
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
index 407350c000..3375dd9060 100644
---
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/LanceRESTService.java
@@ -28,10 +28,12 @@ import
org.apache.gravitino.auxiliary.GravitinoAuxiliaryService;
import org.apache.gravitino.lance.common.config.LanceConfig;
import org.apache.gravitino.lance.common.ops.LanceNamespaceBackend;
import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.apache.gravitino.lance.service.LanceHealthCheckPathMatcher;
import org.apache.gravitino.listener.EventBus;
import org.apache.gravitino.listener.api.event.EventSource;
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.HttpAuditFilter;
import org.apache.gravitino.server.web.HttpServerMetricsSource;
import org.apache.gravitino.server.web.JettyServer;
@@ -66,7 +68,7 @@ public class LanceRESTService implements
GravitinoAuxiliaryService {
LanceConfig lanceConfig = new LanceConfig(properties);
JettyServerConfig serverConfig = JettyServerConfig.fromConfig(lanceConfig);
- server = new JettyServer();
+ server = new LanceJettyServer();
// Get MetricsSystem and EventBus from GravitinoEnv once at init time.
MetricsSystem metricsSystem = GravitinoEnv.getInstance().metricsSystem();
EventBus eventBus = GravitinoEnv.getInstance().eventBus();
@@ -94,10 +96,17 @@ public class LanceRESTService implements
GravitinoAuxiliaryService {
Servlet container = new ServletContainer(resourceConfig);
server.addServlet(container, LANCE_SPEC);
server.addFilter(
- new HttpAuditFilter(eventBus,
EventSource.GRAVITINO_LANCE_REST_SERVER), LANCE_SPEC);
+ new HttpAuditFilter(
+ eventBus, EventSource.GRAVITINO_LANCE_REST_SERVER, new
LanceHealthCheckPathMatcher()),
+ LANCE_SPEC);
server.addCustomFilters(LANCE_SPEC);
server.addSystemFilters(LANCE_SPEC);
+ // Root-level aliases for health checks to improve compatibility with
various monitoring
+ // systems that expect a /health endpoint.
+ server.addServlet(new HealthAliasServlet("/lance"), "/health/*");
+ server.addServlet(new HealthAliasServlet("/lance"), "/health.html");
+
LOG.info(
"Initialized Lance REST service for backend {} in {} mode",
lanceConfig.getNamespaceBackend(),
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.java
new file mode 100644
index 0000000000..51aea85524
--- /dev/null
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceAuthenticationFilter.java
@@ -0,0 +1,86 @@
+/*
+ * 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.lance.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.apache.gravitino.server.authentication.AuthenticationFilter;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.lance.namespace.model.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An {@link AuthenticationFilter} subclass for the Lance REST server that:
+ *
+ * <ul>
+ * <li>allows health check endpoints to bypass authentication via {@link
+ * LanceHealthCheckPathMatcher};
+ * <li>returns Lance-compatible JSON error responses on authentication
failure instead of the
+ * default HTML error pages.
+ * </ul>
+ */
+public class LanceAuthenticationFilter extends AuthenticationFilter {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(LanceAuthenticationFilter.class);
+ private static final ObjectMapper MAPPER =
ObjectMapperProvider.objectMapper();
+
+ public LanceAuthenticationFilter() {
+ healthCheckMatcher = new LanceHealthCheckPathMatcher();
+ }
+
+ @Override
+ protected void sendAuthErrorResponse(HttpServletResponse response, Exception
exception)
+ throws IOException {
+ int status;
+ String message;
+ if (exception instanceof UnauthorizedException) {
+ status = HttpServletResponse.SC_UNAUTHORIZED;
+ message = exception.getMessage();
+ if (message == null || message.isEmpty()) {
+ message = "Authentication failed";
+ }
+ } else if (exception instanceof ForbiddenException) {
+ status = HttpServletResponse.SC_FORBIDDEN;
+ message = exception.getMessage();
+ if (message == null || message.isEmpty()) {
+ message = "Access denied";
+ }
+ } else {
+ status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+ LOG.error("Authentication failure", exception);
+ message = "Authentication failed";
+ }
+
+ ErrorResponse errorResponse = new ErrorResponse();
+ errorResponse.setCode(status);
+ errorResponse.setError(message);
+ errorResponse.setDetail("");
+ errorResponse.setInstance("");
+
+ response.setStatus(status);
+ response.setContentType("application/json");
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ MAPPER.writeValue(response.getWriter(), errorResponse);
+ }
+}
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
new file mode 100644
index 0000000000..e18745fce6
--- /dev/null
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/LanceHealthCheckPathMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * 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.lance.service;
+
+import org.apache.gravitino.server.web.HealthCheckPathMatcher;
+
+/**
+ * A {@link HealthCheckPathMatcher} for the Lance REST server that
additionally recognises {@code
+ * /lance/health} and {@code /lance/health/*} as health check endpoints.
+ *
+ * <p>Pass an instance of this class to both {@link
+ * org.apache.gravitino.server.authentication.AuthenticationFilter} (via {@code
+ * LanceAuthenticationFilter}) and {@link
org.apache.gravitino.server.web.HttpAuditFilter} when
+ * constructing the Lance REST server so that both filters agree on which
paths are probe traffic.
+ */
+public class LanceHealthCheckPathMatcher extends HealthCheckPathMatcher {
+
+ @Override
+ public boolean isHealthCheckPath(String path) {
+ if (super.isHealthCheckPath(path)) {
+ return true;
+ }
+ return path.equals("/lance/health") || path.startsWith("/lance/health/");
+ }
+}
diff --git
a/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
new file mode 100644
index 0000000000..4c280d9f8b
--- /dev/null
+++
b/lance/lance-rest-server/src/main/java/org/apache/gravitino/lance/service/rest/LanceHealthOperations.java
@@ -0,0 +1,142 @@
+/*
+ * 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.lance.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.lance.common.ops.NamespaceWrapper;
+import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.web.Utils;
+
+/**
+ * Health check endpoints for the Lance REST server. Follows the same
MicroProfile Health semantics
+ * as the main Gravitino server.
+ *
+ * <ul>
+ * <li>{@code GET /lance/health/live} — liveness, 200 as long as the HTTP
thread can respond
+ * <li>{@code GET /lance/health/ready} — readiness, 200 when the namespace
wrapper is initialized
+ * <li>{@code GET /lance/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 LanceHealthOperations {
+
+ private static final String CHECK_HTTP_SERVER = "httpServer";
+ private static final String CHECK_NAMESPACE_WRAPPER = "namespaceWrapper";
+
+ @Inject private NamespaceWrapper namespaceWrapper;
+
+ /** Default constructor for Jersey auto-discovery. */
+ public LanceHealthOperations() {}
+
+ /**
+ * 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 = "lance.health.live." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "lance.health.live", absolute = true)
+ public Response live() {
+ HealthCheckDTO check = up(CHECK_HTTP_SERVER, Collections.emptyMap());
+ HealthResponse healthResponse =
+ new HealthResponse(HealthCheckDTO.Status.UP,
Collections.singletonList(check));
+ return Utils.ok(healthResponse);
+ }
+
+ /**
+ * Readiness probe. Returns 200 when the {@link NamespaceWrapper} is
initialized, 503 otherwise.
+ *
+ * @return 200 OK when ready, 503 Service Unavailable with a DOWN {@link
HealthResponse} otherwise
+ */
+ @GET
+ @Path("/ready")
+ @Timed(name = "lance.health.ready." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "lance.health.ready", absolute = true)
+ public Response ready() {
+ HealthCheckDTO namespaceCheck = checkNamespaceWrapper();
+ HealthCheckDTO.Status overall = namespaceCheck.getStatus();
+ HealthResponse body = new HealthResponse(overall,
Collections.singletonList(namespaceCheck));
+ 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 = "lance.health." + MetricNames.HTTP_PROCESS_DURATION, absolute
= true)
+ @ResponseMetered(name = "lance.health", absolute = true)
+ public Response health() {
+ List<HealthCheckDTO> checks = new ArrayList<>(2);
+ checks.add(up(CHECK_HTTP_SERVER, Collections.emptyMap()));
+ checks.add(checkNamespaceWrapper());
+
+ 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 checkNamespaceWrapper() {
+ NamespaceWrapper wrapper = getNamespaceWrapper();
+ if (wrapper == null) {
+ return down(CHECK_NAMESPACE_WRAPPER, "reason", "namespace wrapper not
initialized");
+ }
+ if (wrapper.isInitialized()) {
+ return up(CHECK_NAMESPACE_WRAPPER, Collections.emptyMap());
+ } else {
+ return down(CHECK_NAMESPACE_WRAPPER, "reason", "namespace wrapper not
initialized");
+ }
+ }
+
+ /** Visible for testing — subclasses override to inject a different wrapper
instance. */
+ NamespaceWrapper getNamespaceWrapper() {
+ return namespaceWrapper;
+ }
+
+ 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/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
new file mode 100644
index 0000000000..0b73c7ae7f
--- /dev/null
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/TestLanceAuthenticationFilter.java
@@ -0,0 +1,157 @@
+/*
+ * 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.lance.service;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.UnauthorizedException;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.lance.namespace.model.ErrorResponse;
+
+public class TestLanceAuthenticationFilter {
+
+ private static final ObjectMapper MAPPER =
ObjectMapperProvider.objectMapper();
+
+ /** Exposes the protected {@code isHealthCheckRequest} method for white-box
testing. */
+ private static class TestableFilter extends LanceAuthenticationFilter {
+ boolean isHealth(ServletRequest request) {
+ return isHealthCheckRequest(request);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "/lance/health",
+ "/lance/health/live",
+ "/lance/health/ready",
+ "/health",
+ "/health/live",
+ "/health/ready",
+ "/health.html",
+ "/api/health",
+ "/api/health/live",
+ "/api/health/ready"
+ })
+ public void testHealthPathsBypassAuth(String path) {
+ TestableFilter filter = new TestableFilter();
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ when(req.getRequestURI()).thenReturn(path);
+ Assertions.assertTrue(filter.isHealth(req), "Expected health bypass for
path: " + path);
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "/lance/v1/namespace",
+ "/lance/v1/table",
+ "/lance/healthcheck",
+ "/iceberg/health",
+ "/iceberg/v1/namespaces"
+ })
+ public void testNonHealthPathsRequireAuth(String path) {
+ TestableFilter filter = new TestableFilter();
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ when(req.getRequestURI()).thenReturn(path);
+ Assertions.assertFalse(filter.isHealth(req), "Expected auth required for
path: " + path);
+ }
+
+ @Test
+ public void testNonHttpRequestReturnsFalse() {
+ TestableFilter filter = new TestableFilter();
+ ServletRequest nonHttpRequest = mock(ServletRequest.class);
+ Assertions.assertFalse(filter.isHealth(nonHttpRequest));
+ }
+
+ @Test
+ public void testUnauthorizedErrorReturnsJson() throws Exception {
+ LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ when(response.getWriter()).thenReturn(printWriter);
+
+ filter.sendAuthErrorResponse(
+ response, new UnauthorizedException("The provided credentials did not
support"));
+
+ verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ verify(response).setContentType("application/json");
+ verify(response).setCharacterEncoding("UTF-8");
+
+ printWriter.flush();
+ String json = stringWriter.toString();
+ ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+ Assertions.assertEquals(401, errorResponse.getCode());
+ Assertions.assertEquals("The provided credentials did not support",
errorResponse.getError());
+ }
+
+ @Test
+ public void testForbiddenExceptionReturnsJson() throws Exception {
+ LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ when(response.getWriter()).thenReturn(printWriter);
+
+ filter.sendAuthErrorResponse(response, new ForbiddenException("Access
denied"));
+
+ verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN);
+
+ printWriter.flush();
+ String json = stringWriter.toString();
+ ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+ Assertions.assertEquals(403, errorResponse.getCode());
+ Assertions.assertEquals("Access denied", errorResponse.getError());
+ }
+
+ @Test
+ public void testInternalServerErrorReturnsJson() throws Exception {
+ LanceAuthenticationFilter filter = new LanceAuthenticationFilter();
+
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ when(response.getWriter()).thenReturn(printWriter);
+
+ filter.sendAuthErrorResponse(response, new RuntimeException("Something
went wrong"));
+
+ verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+
+ printWriter.flush();
+ String json = stringWriter.toString();
+ ErrorResponse errorResponse = MAPPER.readValue(json, ErrorResponse.class);
+ Assertions.assertEquals(500, errorResponse.getCode());
+ Assertions.assertEquals("Authentication failed", errorResponse.getError());
+ }
+}
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
new file mode 100644
index 0000000000..24256fd67d
--- /dev/null
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/service/rest/TestLanceHealthOperations.java
@@ -0,0 +1,96 @@
+/*
+ * 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.lance.service.rest;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import javax.ws.rs.core.Response;
+import org.apache.gravitino.dto.HealthCheckDTO;
+import org.apache.gravitino.dto.responses.HealthResponse;
+import org.apache.gravitino.lance.common.ops.NamespaceWrapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestLanceHealthOperations {
+
+ private static LanceHealthOperations operationsWithWrapper(NamespaceWrapper
wrapper) {
+ return new LanceHealthOperations() {
+ @Override
+ NamespaceWrapper getNamespaceWrapper() {
+ return wrapper;
+ }
+ };
+ }
+
+ @Test
+ public void testLiveReturns200() {
+ LanceHealthOperations ops = operationsWithWrapper(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 testReadyReturns200WhenWrapperInitialized() {
+ NamespaceWrapper wrapper = mock(NamespaceWrapper.class);
+ when(wrapper.isInitialized()).thenReturn(true);
+ LanceHealthOperations ops = operationsWithWrapper(wrapper);
+ 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 testReadyReturns503WhenWrapperNotInitialized() {
+ LanceHealthOperations ops = operationsWithWrapper(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("namespaceWrapper",
body.getChecks().get(0).getName());
+ }
+
+ @Test
+ public void testHealthReturns200WhenWrapperInitialized() {
+ NamespaceWrapper wrapper = mock(NamespaceWrapper.class);
+ when(wrapper.isInitialized()).thenReturn(true);
+ LanceHealthOperations ops = operationsWithWrapper(wrapper);
+ 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 testHealthReturns503WhenWrapperNotInitialized() {
+ LanceHealthOperations ops = operationsWithWrapper(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 hasNamespaceCheck =
+ body.getChecks().stream().anyMatch(c ->
"namespaceWrapper".equals(c.getName()));
+ Assertions.assertTrue(hasNamespaceCheck);
+ }
+}