From a8bc5ec8423233c53c4b2131aa088f8fcf543889 Mon Sep 17 00:00:00 2001 From: Alfredo Gutierrez Date: Fri, 16 Aug 2024 16:22:10 -0600 Subject: [PATCH] feat: Health endpoints (#111) Signed-off-by: Alfredo Gutierrez --- .../templates/deployment.yaml | 18 ++- charts/hedera-block-node/values.yaml | 5 + .../java/com/hedera/block/server/Server.java | 22 +++- .../block/server/health/HealthService.java | 50 +++++++ .../server/health/HealthServiceImpl.java | 92 +++++++++++++ server/src/main/java/module-info.java | 6 +- .../server/health/HealthServiceTest.java | 123 ++++++++++++++++++ 7 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 server/src/main/java/com/hedera/block/server/health/HealthService.java create mode 100644 server/src/main/java/com/hedera/block/server/health/HealthServiceImpl.java create mode 100644 server/src/test/java/com/hedera/block/server/health/HealthServiceTest.java diff --git a/charts/hedera-block-node/templates/deployment.yaml b/charts/hedera-block-node/templates/deployment.yaml index 16327512f..03fc45124 100644 --- a/charts/hedera-block-node/templates/deployment.yaml +++ b/charts/hedera-block-node/templates/deployment.yaml @@ -40,16 +40,14 @@ spec: name: {{ include "hedera-block-node.fullname" . }}-config - secretRef: name: {{ include "hedera-block-node.fullname" . }}-secret - - # TODO: Uncomment them once we have the health and readiness probes in our application - #livenessProbe: - # httpGet: - # path: /health # we need to define this in our application - # port: 8080 # will be the same as server port? - #readinessProbe: - # httpGet: - # path: /ready # we need to define this in our application - # port: 8080 # will be the same as server port? + livenessProbe: + httpGet: + path: {{ .Values.blockNode.health.liveness.endpoint }} + port: {{ .Values.service.port }} + readinessProbe: + httpGet: + path: {{ .Values.blockNode.health.readiness.endpoint }} + port: {{ .Values.service.port }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/hedera-block-node/values.yaml b/charts/hedera-block-node/values.yaml index f81da5b5b..68b8d40b6 100644 --- a/charts/hedera-block-node/values.yaml +++ b/charts/hedera-block-node/values.yaml @@ -82,3 +82,8 @@ blockNode: secret: # if blank will use same as AppVersion of chart. PRIVATE_KEY: "fake_private_key" + health: + readiness: + endpoint: "/healthz/readiness" + liveness: + endpoint: "/healthz/liveness" diff --git a/server/src/main/java/com/hedera/block/server/Server.java b/server/src/main/java/com/hedera/block/server/Server.java index 53dd1e0fa..a454af26f 100644 --- a/server/src/main/java/com/hedera/block/server/Server.java +++ b/server/src/main/java/com/hedera/block/server/Server.java @@ -21,6 +21,8 @@ import com.hedera.block.server.config.BlockNodeContext; import com.hedera.block.server.config.BlockNodeContextFactory; import com.hedera.block.server.data.ObjectEvent; +import com.hedera.block.server.health.HealthService; +import com.hedera.block.server.health.HealthServiceImpl; import com.hedera.block.server.mediator.LiveStreamMediatorBuilder; import com.hedera.block.server.mediator.StreamMediator; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig; @@ -32,6 +34,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.helidon.webserver.WebServer; import io.helidon.webserver.grpc.GrpcRouting; +import io.helidon.webserver.http.HttpRouting; import java.io.IOException; /** Main class for the block node server */ @@ -82,16 +85,33 @@ public static void main(final String[] args) { final GrpcRouting.Builder grpcRouting = GrpcRouting.builder().service(blockStreamService); + @NonNull final HealthService healthService = new HealthServiceImpl(serviceStatus); + + @NonNull + final HttpRouting.Builder httpRouting = + HttpRouting.builder() + .register(healthService.getHealthRootPath(), healthService); + // Build the web server + // TODO: make port server a configurable value. @NonNull final WebServer webServer = - WebServer.builder().port(8080).addRouting(grpcRouting).build(); + WebServer.builder() + .port(8080) + .addRouting(grpcRouting) + .addRouting(httpRouting) + .build(); // Update the serviceStatus with the web server serviceStatus.setWebServer(webServer); // Start the web server webServer.start(); + + // Log the server status + LOGGER.log( + System.Logger.Level.INFO, + "Block Node Server started at port: " + webServer.port()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/server/src/main/java/com/hedera/block/server/health/HealthService.java b/server/src/main/java/com/hedera/block/server/health/HealthService.java new file mode 100644 index 000000000..2581655d4 --- /dev/null +++ b/server/src/main/java/com/hedera/block/server/health/HealthService.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed 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 com.hedera.block.server.health; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.helidon.webserver.http.HttpService; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** Defines the contract for health http service, needed for implementing an standard */ +public interface HealthService extends HttpService { + /** + * The path for the health group endpoints. Root path for all health endpoints. + * + * @return the root path for the health group endpoints + */ + @NonNull + String getHealthRootPath(); + + /** + * Handles the request for liveness endpoint, that it most be defined on routing implementation. + * + * @param req the server request + * @param res the server response + */ + void handleLiveness(@NonNull final ServerRequest req, @NonNull final ServerResponse res); + + /** + * Handles the request for readiness endpoint, that it most be defined on routing + * implementation. + * + * @param req the server request + * @param res the server response + */ + void handleReadiness(@NonNull final ServerRequest req, @NonNull final ServerResponse res); +} diff --git a/server/src/main/java/com/hedera/block/server/health/HealthServiceImpl.java b/server/src/main/java/com/hedera/block/server/health/HealthServiceImpl.java new file mode 100644 index 000000000..912d39963 --- /dev/null +++ b/server/src/main/java/com/hedera/block/server/health/HealthServiceImpl.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed 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 com.hedera.block.server.health; + +import com.hedera.block.server.ServiceStatus; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +/** Provides implementation for the health endpoints of the server. */ +public class HealthServiceImpl implements HealthService { + + private static final String LIVENESS_PATH = "/liveness"; + private static final String READINESS_PATH = "/readiness"; + + private final ServiceStatus serviceStatus; + + /** + * It initializes the HealthService with needed dependencies. + * + * @param serviceStatus is used to check the status of the service + */ + public HealthServiceImpl(@NonNull ServiceStatus serviceStatus) { + this.serviceStatus = serviceStatus; + } + + @Override + @NonNull + public String getHealthRootPath() { + return "/healthz"; + } + + /** + * Configures the health routes for the server. + * + * @param httpRules is used to configure the health endpoints routes + */ + @Override + public void routing(@NonNull final HttpRules httpRules) { + httpRules + .get(LIVENESS_PATH, this::handleLiveness) + .get(READINESS_PATH, this::handleReadiness); + } + + /** + * Handles the request for liveness endpoint, that it most be defined on routing implementation. + * + * @param req the server request + * @param res the server response + */ + @Override + public final void handleLiveness( + @NonNull final ServerRequest req, @NonNull final ServerResponse res) { + if (serviceStatus.isRunning()) { + res.status(200).send("OK"); + } else { + res.status(503).send("Service is not running"); + } + } + + /** + * Handles the request for readiness endpoint, that it most be defined on routing + * implementation. + * + * @param req the server request + * @param res the server response + */ + @Override + public final void handleReadiness( + @NonNull final ServerRequest req, @NonNull final ServerResponse res) { + if (serviceStatus.isRunning()) { + res.status(200).send("OK"); + } else { + res.status(503).send("Service is not running"); + } + } +} diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index 6f4c805d1..379ccf9b8 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -2,10 +2,8 @@ /** Runtime module of the server. */ module com.hedera.block.server { - exports com.hedera.block.server.consumer to - com.swirlds.config.impl; - exports com.hedera.block.server.persistence.storage to - com.swirlds.config.impl; + exports com.hedera.block.server.consumer; + exports com.hedera.block.server.persistence.storage; requires com.hedera.block.protos; requires com.google.protobuf; diff --git a/server/src/test/java/com/hedera/block/server/health/HealthServiceTest.java b/server/src/test/java/com/hedera/block/server/health/HealthServiceTest.java new file mode 100644 index 000000000..df2aed07f --- /dev/null +++ b/server/src/test/java/com/hedera/block/server/health/HealthServiceTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed 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 com.hedera.block.server.health; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import com.hedera.block.server.ServiceStatus; +import io.helidon.webserver.http.HttpRules; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HealthServiceTest { + + private static final String READINESS_PATH = "/readiness"; + private static final String LIVENESS_PATH = "/liveness"; + private static final String HEALTH_PATH = "/healthz"; + + @Mock private ServiceStatus serviceStatus; + + @Mock ServerRequest serverRequest; + + @Mock ServerResponse serverResponse; + + @Test + public void testHandleLiveness() { + // given + when(serviceStatus.isRunning()).thenReturn(true); + when(serverResponse.status(200)).thenReturn(serverResponse); + doNothing().when(serverResponse).send("OK"); + HealthService healthService = new HealthServiceImpl(serviceStatus); + + // when + healthService.handleLiveness(serverRequest, serverResponse); + + // then + verify(serverResponse, times(1)).status(200); + verify(serverResponse, times(1)).send("OK"); + } + + @Test + public void testHandleLiveness_notRunning() { + // given + when(serviceStatus.isRunning()).thenReturn(false); + when(serverResponse.status(503)).thenReturn(serverResponse); + doNothing().when(serverResponse).send("Service is not running"); + HealthService healthService = new HealthServiceImpl(serviceStatus); + + // when + healthService.handleLiveness(serverRequest, serverResponse); + + // then + verify(serverResponse, times(1)).status(503); + verify(serverResponse, times(1)).send("Service is not running"); + } + + @Test + public void testHandleReadiness() { + // given + when(serviceStatus.isRunning()).thenReturn(true); + when(serverResponse.status(200)).thenReturn(serverResponse); + doNothing().when(serverResponse).send("OK"); + HealthService healthService = new HealthServiceImpl(serviceStatus); + + // when + healthService.handleReadiness(serverRequest, serverResponse); + + // then + verify(serverResponse, times(1)).status(200); + verify(serverResponse, times(1)).send("OK"); + } + + @Test + public void testHandleReadiness_notRunning() { + // given + when(serviceStatus.isRunning()).thenReturn(false); + when(serverResponse.status(503)).thenReturn(serverResponse); + doNothing().when(serverResponse).send("Service is not running"); + HealthService healthService = new HealthServiceImpl(serviceStatus); + + // when + healthService.handleReadiness(serverRequest, serverResponse); + + // then + verify(serverResponse, times(1)).status(503); + verify(serverResponse, times(1)).send("Service is not running"); + } + + @Test + public void testRouting() { + // given + HealthService healthService = new HealthServiceImpl(serviceStatus); + HttpRules httpRules = mock(HttpRules.class); + when(httpRules.get(anyString(), any())).thenReturn(httpRules); + + // when + healthService.routing(httpRules); + + // then + verify(httpRules, times(1)).get(eq(LIVENESS_PATH), any()); + verify(httpRules, times(1)).get(eq(READINESS_PATH), any()); + assertEquals(HEALTH_PATH, healthService.getHealthRootPath()); + } +}