From 7aae2ccc3b79cd69cfbbeedbc74d7ada58b20c5a Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 3 Sep 2026 15:28:34 +0200 Subject: [PATCH] FINERACT-2684: Add "App is ready" message to log --- README.md | 4 + .../FineractApplicationLifecycleLogger.java | 94 +++++++++++++++++++ ...ineractApplicationLifecycleLoggerTest.java | 53 +++++++++++ 3 files changed, 151 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLogger.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLoggerTest.java diff --git a/README.md b/README.md index 3aa136a9b61..1e268558e74 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ cd fineract ``` After a minute or two, Fineract will be listening for API requests on port 8443. +Wait for the `APACHE FINERACT IS READY` message in the log before sending requests. + +Stopping a `bootRun` or `devRun` process is reported by Gradle as `Build cancelled while executing task`, but it does not mean that Fineract failed. +Graceful shutdowns are also log `APACHE FINERACT IS SHUTTING DOWN GRACEFULLY`. > [!TIP] > Java properties or environment variables can be used to override default settings. See `fineract-provider/src/main/resources/application.properties`. diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLogger.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLogger.java new file mode 100644 index 00000000000..622f81aff9f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLogger.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.fineract.infrastructure.core.boot; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class FineractApplicationLifecycleLogger { + + private volatile ConfigurableApplicationContext readyApplicationContext; + + @EventListener + public void onApplicationReady(ApplicationReadyEvent event) { + ConfigurableApplicationContext applicationContext = event.getApplicationContext(); + readyApplicationContext = applicationContext; + Environment environment = applicationContext.getEnvironment(); + log.info("Fineract lifecycle event: {}", createReadyMessage(environment, getServerPort(applicationContext))); + } + + @EventListener + public void onApplicationClosing(ContextClosedEvent event) { + if (event.getApplicationContext() == readyApplicationContext) { + log.info("Fineract lifecycle event: {}", createShuttingDownMessage()); + } + } + + static String createReadyMessage(Environment environment, int serverPort) { + String scheme = environment.getProperty("server.ssl.enabled", Boolean.class, false) ? "https" : "http"; + String host = formatHost(environment.getProperty("server.address", "localhost")); + String contextPath = normalizeContextPath(environment.getProperty("server.servlet.context-path", "")); + String baseUrl = scheme + "://" + host + ":" + serverPort + contextPath; + + return String.join(System.lineSeparator(), "", "========================================================================", + " APACHE FINERACT IS READY", " Base URL: " + baseUrl, " Health: " + baseUrl + "/actuator/health", + "========================================================================"); + } + + static String createShuttingDownMessage() { + return String.join(System.lineSeparator(), "", "========================================================================", + " APACHE FINERACT IS SHUTTING DOWN GRACEFULLY", + "========================================================================"); + } + + private static int getServerPort(ConfigurableApplicationContext applicationContext) { + if (applicationContext instanceof WebServerApplicationContext webApplicationContext + && webApplicationContext.getWebServer() != null) { + return webApplicationContext.getWebServer().getPort(); + } + return applicationContext.getEnvironment().getProperty("server.port", Integer.class, 8080); + } + + private static String formatHost(String host) { + if (host.isBlank()) { + return "localhost"; + } + if (host.equals("0.0.0.0") || host.equals("::")) { + return "localhost"; + } + return host.contains(":") && !host.startsWith("[") ? "[" + host + "]" : host; + } + + private static String normalizeContextPath(String contextPath) { + if (contextPath.isBlank() || contextPath.equals("/")) { + return ""; + } + String normalizedContextPath = contextPath.startsWith("/") ? contextPath : "/" + contextPath; + return normalizedContextPath.endsWith("/") ? normalizedContextPath.substring(0, normalizedContextPath.length() - 1) + : normalizedContextPath; + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLoggerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLoggerTest.java new file mode 100644 index 00000000000..ae4e8e5b1c5 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/boot/FineractApplicationLifecycleLoggerTest.java @@ -0,0 +1,53 @@ +/** + * 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.fineract.infrastructure.core.boot; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.mock.env.MockEnvironment; + +public class FineractApplicationLifecycleLoggerTest { + + @Test + public void readyMessageContainsConfiguredHttpsUrlAndHealthEndpoint() { + MockEnvironment environment = new MockEnvironment().withProperty("server.ssl.enabled", "true") + .withProperty("server.servlet.context-path", "/fineract-provider/"); + + String message = FineractApplicationLifecycleLogger.createReadyMessage(environment, 8443); + + assertThat(message).contains("APACHE FINERACT IS READY").contains("Base URL: https://localhost:8443/fineract-provider") + .contains("Health: https://localhost:8443/fineract-provider/actuator/health"); + } + + @Test + public void readyMessageSupportsHttpRootContextAndIpv6Address() { + MockEnvironment environment = new MockEnvironment().withProperty("server.address", "2001:db8::1") + .withProperty("server.servlet.context-path", "/"); + + String message = FineractApplicationLifecycleLogger.createReadyMessage(environment, 8080); + + assertThat(message).contains("Base URL: http://[2001:db8::1]:8080").contains("Health: http://[2001:db8::1]:8080/actuator/health"); + } + + @Test + public void shuttingDownMessageDescribesAnInProgressGracefulShutdown() { + assertThat(FineractApplicationLifecycleLogger.createShuttingDownMessage()).contains("APACHE FINERACT IS SHUTTING DOWN GRACEFULLY"); + } +}