From 8ef01f1fd0dccc6db9d2f7d293cd9eff1c8e803e Mon Sep 17 00:00:00 2001 From: erickgonzalez Date: Tue, 24 Sep 2024 13:49:30 -0600 Subject: [PATCH] #29719 include in 23.10.24 LTS --- dotCMS/hotfix_tracking.md | 2 +- .../api/v1/system/monitor/MonitorHelper.java | 188 ++++++++++-------- .../v1/system/monitor/MonitorResource.java | 62 +++--- .../api/v1/system/monitor/MonitorStats.java | 129 +++++++++--- 4 files changed, 232 insertions(+), 149 deletions(-) diff --git a/dotCMS/hotfix_tracking.md b/dotCMS/hotfix_tracking.md index 49efd7c90a4f..3bc1f615a452 100644 --- a/dotCMS/hotfix_tracking.md +++ b/dotCMS/hotfix_tracking.md @@ -158,4 +158,4 @@ This maintenance release includes the following code fixes: 151. https://github.com/dotCMS/core/issues/29293 : Cannot change folder URL to lowercase #29293 152. https://github.com/dotCMS/core/issues/29321 : An Asset name starts with number and specific alphabets considers as an Image #29321 153. https://github.com/dotCMS/core/issues/29668 : Spike: PP bundles not being processed by Receiver #29668 - +154. https://github.com/dotCMS/core/issues/29719 : relax ES checks in /api/v1/probes/startup #29719 diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorHelper.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorHelper.java index 8ea6239fa5e4..9d415b34fca9 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorHelper.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorHelper.java @@ -1,16 +1,13 @@ package com.dotcms.rest.api.v1.system.monitor; -import static com.dotcms.content.elasticsearch.business.ESIndexAPI.INDEX_OPERATIONS_TIMEOUT_IN_MS; - -import com.dotcms.content.elasticsearch.business.IndiciesInfo; +import com.dotcms.content.elasticsearch.business.ClusterStats; import com.dotcms.content.elasticsearch.util.RestHighLevelClientProvider; -import com.dotcms.enterprise.cluster.ClusterFactory; +import com.dotcms.exception.ExceptionUtil; import com.dotcms.util.HttpRequestDataUtil; import com.dotcms.util.network.IPUtils; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.common.db.DotConnect; -import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Config; import com.dotmarketing.util.ConfigUtils; import com.dotmarketing.util.Logger; @@ -35,6 +32,9 @@ class MonitorHelper { + final boolean accessGranted ; + final boolean useExtendedFormat; + private static final String IPV6_LOCALHOST = "0:0:0:0:0:0:0:1"; private static final String[] DEFAULT_IP_ACL_VALUE = new String[]{"127.0.0.1/32", "10.0.0.0/8", "172.16.0.0/12", @@ -52,41 +52,62 @@ class MonitorHelper { static final AtomicReference> cachedStats = new AtomicReference<>(); - boolean accessGranted = false; - boolean useExtendedFormat = false; - MonitorHelper(final HttpServletRequest request) { - try { - this.useExtendedFormat = request.getParameter("extended") != null; + MonitorHelper(final HttpServletRequest request, final boolean heavyCheck) { + this.useExtendedFormat = heavyCheck; + this.accessGranted = isAccessGranted(request); + } - // set this.accessGranted + /** + * Determines if the IP address of the request is allowed to access this monitor service. We use + * an ACL list to determine if the user/service accessing the monitor has permission to do so. + * ACL IPs can be defined via the {@code SYSTEM_STATUS_API_IP_ACL} property. + * + * @param request The current instance of the {@link HttpServletRequest}. + * + * @return If the IP address of the request is allowed to access this monitor service, returns + * {@code true}. + */ + boolean isAccessGranted(final HttpServletRequest request){ + try { + if(IPV6_LOCALHOST.equals(request.getRemoteAddr()) || ACLS_IPS == null || ACLS_IPS.length == 0){ + return true; + } final String clientIP = HttpRequestDataUtil.getIpAddress(request).toString().split(StringPool.SLASH)[1]; - if (ACLS_IPS == null || ACLS_IPS.length == 0) { - this.accessGranted = true; - } else { - for (String aclIP : ACLS_IPS) { - if (IPUtils.isIpInCIDR(clientIP, aclIP)) { - this.accessGranted = true; - break; - } + for (final String aclIP : ACLS_IPS) { + if (IPUtils.isIpInCIDR(clientIP, aclIP)) { + return true; } } - } catch (Exception e) { - Logger.warnAndDebug(this.getClass(), e.getMessage(), e); - throw new DotRuntimeException(e); + } catch (final Exception e) { + Logger.warnEveryAndDebug(this.getClass(), e, 60000); } + return false; } - boolean startedUp() { + /** + * Determines if dotCMS has started up by checking if the {@code dotcms.started.up} system + * property has been set. + * + * @return If dotCMS has started up, returns {@code true}. + */ + boolean isStartedUp() { return System.getProperty(WebKeys.DOTCMS_STARTED_UP)!=null; } - + /** + * Retrieves the current status of the different subsystems of dotCMS. This method caches the + * response for a period of time defined by the {@code SYSTEM_STATUS_CACHE_RESPONSE_SECONDS} + * property. + * + * @return An instance of {@link MonitorStats} containing the status of the different + * subsystems. + */ MonitorStats getMonitorStats() { if (cachedStats.get() != null && cachedStats.get()._1 > System.currentTimeMillis()) { return cachedStats.get()._2; @@ -94,30 +115,26 @@ MonitorStats getMonitorStats() { return getMonitorStatsNoCache(); } - + /** + * Retrieves the current status of the different subsystems of dotCMS. If cached monitor stats + * are available, return them instead. + * + * @return An instance of {@link MonitorStats} containing the status of the different + * subsystems. + */ synchronized MonitorStats getMonitorStatsNoCache() { // double check if (cachedStats.get() != null && cachedStats.get()._1 > System.currentTimeMillis()) { return cachedStats.get()._2; } - - - - final MonitorStats monitorStats = new MonitorStats(); - - final IndiciesInfo indiciesInfo = Try.of(()->APILocator.getIndiciesAPI().loadIndicies()).getOrElseThrow(DotRuntimeException::new); - - monitorStats.subSystemStats.isDBHealthy = isDBHealthy(); - monitorStats.subSystemStats.isLiveIndexHealthy = isIndexHealthy(indiciesInfo.getLive()); - monitorStats.subSystemStats.isWorkingIndexHealthy = isIndexHealthy(indiciesInfo.getWorking()); - monitorStats.subSystemStats.isCacheHealthy = isCacheHealthy(); - monitorStats.subSystemStats.isLocalFileSystemHealthy = isLocalFileSystemHealthy(); - monitorStats.subSystemStats.isAssetFileSystemHealthy = isAssetFileSystemHealthy(); - - - monitorStats.serverId = getServerID(); - monitorStats.clusterId = getClusterID(); - + final MonitorStats monitorStats = new MonitorStats + .Builder() + .cacheHealthy(isCacheHealthy()) + .assetFSHealthy(isAssetFileSystemHealthy()) + .localFSHealthy(isLocalFileSystemHealthy()) + .dBHealthy(isDBHealthy()) + .esHealthy(canConnectToES()) + .build(); // cache a healthy response if (monitorStats.isDotCMSHealthy()) { @@ -127,12 +144,16 @@ synchronized MonitorStats getMonitorStatsNoCache() { return monitorStats; } - + /** + * Determines if the database server is healthy by executing a simple query. + * + * @return If the database server is healthy, returns {@code true}. + */ boolean isDBHealthy() { return Try.of(()-> - new DotConnect().setSQL("SELECT count(*) as count FROM (SELECT 1 FROM dot_cluster LIMIT 1) AS t") + new DotConnect().setSQL("SELECT 1 as count") .loadInt("count")) .onFailure(e->Logger.warnAndDebug(MonitorHelper.class, "unable to connect to db:" + e.getMessage(),e)) .getOrElse(0) > 0; @@ -141,34 +162,28 @@ boolean isDBHealthy() { } - boolean isIndexHealthy(final String index) { - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - searchSourceBuilder.query(QueryBuilders.matchAllQuery()); - searchSourceBuilder.size(0); - searchSourceBuilder.timeout(TimeValue - .timeValueMillis(INDEX_OPERATIONS_TIMEOUT_IN_MS)); - searchSourceBuilder.fetchSource(new String[]{"inode"}, null); - SearchRequest searchRequest = new SearchRequest(); - searchRequest.source(searchSourceBuilder); - searchRequest.indices(index); - - long totalHits = Try.of(()-> - RestHighLevelClientProvider - .getInstance() - .getClient() - .search(searchRequest,RequestOptions.DEFAULT) - .getHits() - .getTotalHits() - .value) - .onFailure(e->Logger.warnAndDebug(MonitorHelper.class, "unable to connect to index:" + e.getMessage(),e)) - .getOrElse(0L); - - return totalHits > 0; - + /** + * Determines if dotCMS can connect to Elasticsearch by checking the ES Server cluster + * statistics. If they're available, it means dotCMS can connect to ES. + * + * @return If dotCMS can connect to Elasticsearch, returns {@code true}. + */ + boolean canConnectToES() { + try { + final ClusterStats stats = APILocator.getESIndexAPI().getClusterStats(); + return stats != null && stats.getClusterName() != null; + } catch (final Exception e) { + Logger.warnAndDebug(this.getClass(), + "Unable to connect to ES: " + ExceptionUtil.getErrorMessage(e), e); + return false; + } } - + /** + * Determines if the cache is healthy by checking if the SYSTEM_HOST identifier is available. + * + * @return If the cache is healthy, returns {@code true}. + */ boolean isCacheHealthy() { try { APILocator.getIdentifierAPI().find(Host.SYSTEM_HOST); @@ -181,30 +196,33 @@ boolean isCacheHealthy() { } + /** + * Determines if the local file system is healthy by writing a file to the Dynamic Content Path + * directory. + * + * @return If the local file system is healthy, returns {@code true}. + */ boolean isLocalFileSystemHealthy() { return new FileSystemTest(ConfigUtils.getDynamicContentPath()).call(); } + /** + * Determines if the asset file system is healthy by writing a file to the Asset Path + * directory. + * + * @return If the asset file system is healthy, returns {@code true}. + */ boolean isAssetFileSystemHealthy() { return new FileSystemTest(ConfigUtils.getAssetPath()).call(); } - - private String getServerID() { - return APILocator.getServerAPI().readServerId(); - - } - - private String getClusterID() { - return ClusterFactory.getClusterId(); - - - } - + /** + * This class is used to test the health of the file system by writing a file to a given path. + */ static final class FileSystemTest implements Callable { final String initialPath; @@ -229,10 +247,10 @@ public Boolean call() { return file.delete(); } } catch (Exception e) { - Logger.warnAndDebug(this.getClass(), e.getMessage(), e); + Logger.warnAndDebug(this.getClass(), "Unable to write a file to: " + initialPath + " : " +e.getMessage(), e); return false; } return false; } } -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorResource.java index 4a422e956361..5b16f246b5ad 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorResource.java @@ -2,7 +2,8 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.rest.annotation.NoCache; -import java.util.Map; +import org.glassfish.jersey.server.JSONP; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; @@ -11,16 +12,20 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.glassfish.jersey.server.JSONP; - +import java.util.Map; +/** + * This REST Endpoint provides a set of probes to check the status of the different subsystems used + * by dotCMS. This tool is crucial for Engineering Teams to check that dotCMS is running properly. + * + * @author Brent Griffin + * @since Jul 18th, 2018 + */ @Path("/v1/{a:system-status|probes}") public class MonitorResource { - - private static final int SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE; - private static final int FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; - + private static final int SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE; + private static final int FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; /** * This /startup and /ready probe is heavy - it is intended to report on when dotCMS first comes up @@ -44,46 +49,33 @@ public class MonitorResource { @Path("/") @Produces(MediaType.APPLICATION_JSON) @CloseDBIfOpened - public Response statusCheck(final @Context HttpServletRequest request) { - final MonitorHelper helper = new MonitorHelper(request); + public Response heavyCheck(final @Context HttpServletRequest request) { + final MonitorHelper helper = new MonitorHelper(request , true); if(!helper.accessGranted) { return Response.status(FORBIDDEN).entity(Map.of()).build(); } - if(!helper.startedUp()) { + if(!helper.isStartedUp()) { return Response.status(SERVICE_UNAVAILABLE).build(); } if(!helper.getMonitorStats().isDotCMSHealthy()) { return Response.status(SERVICE_UNAVAILABLE).build(); } - if(helper.useExtendedFormat) { - return Response.ok(helper.getMonitorStats().toMap()).build(); - } - return Response.ok().build(); - + return Response.ok(helper.getMonitorStats().toMap()).build(); } - - - @NoCache @GET @JSONP - @Path("/{a:startup|ready}") + @Path("/{a:|startup|ready|heavy}") @Produces(MediaType.APPLICATION_JSON) @CloseDBIfOpened public Response ready(final @Context HttpServletRequest request) { - - return statusCheck(request); - + return heavyCheck(request); } - - - - /** * This /alive probe is lightweight - it checks if the server is up by requesting a common object from * the dotCMS cache layer twice in a row. By the time a request gets here it has @@ -93,29 +85,25 @@ public Response ready(final @Context HttpServletRequest request) { * @param request * @return */ - @GET - @Path("/alive") + @Path("/{a:alive|light}") @CloseDBIfOpened @Produces(MediaType.APPLICATION_JSON) - public Response aliveCheck(final @Context HttpServletRequest request) { - - - final MonitorHelper helper = new MonitorHelper(request); + public Response lightCheck(final @Context HttpServletRequest request) { + final MonitorHelper helper = new MonitorHelper(request, false); if(!helper.accessGranted) { return Response.status(FORBIDDEN).build(); } - if(!helper.startedUp()) { + if(!helper.isStartedUp()) { return Response.status(SERVICE_UNAVAILABLE).build(); } //try this twice as it is an imperfect test - if(helper.isCacheHealthy() && helper.isCacheHealthy()) { + if(helper.isCacheHealthy() ) { return Response.ok().build(); } - return Response.status(SERVICE_UNAVAILABLE).build(); - } -} \ No newline at end of file + +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorStats.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorStats.java index 9627c5422776..74dd041b1e01 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorStats.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/monitor/MonitorStats.java @@ -1,53 +1,130 @@ package com.dotcms.rest.api.v1.system.monitor; -import com.liferay.util.StringPool; import java.util.Map; +/** + * This class is used to report on the status of the various subsystems used by dotCMS. + * + * @author Brent Griffin + * @since Jul 18th, 2018 + */ +public class MonitorStats { + + final boolean assetFSHealthy; + final boolean cacheHealthy; + final boolean dBHealthy; + final boolean esHealthy; + final boolean localFSHealthy; + + public MonitorStats(boolean assetFSHealthy, + boolean cacheHealthy, + boolean dBHealthy, + boolean esHealthy, + boolean localFSHealthy) { + this.assetFSHealthy = assetFSHealthy; + this.cacheHealthy = cacheHealthy; + this.dBHealthy = dBHealthy; + this.esHealthy = esHealthy; + this.localFSHealthy = localFSHealthy; + } -class MonitorStats { - - final MonitorSubSystemStats subSystemStats = new MonitorSubSystemStats(); - String clusterId = StringPool.BLANK; - String serverId = StringPool.BLANK; - + /** + * This method checks if the dotCMS instance is healthy. It does this by checking if the backend + * and frontend are healthy. + * + * @return If the dotCMS instance is healthy, returns {@code true}. + */ boolean isDotCMSHealthy() { return isBackendHealthy() && isFrontendHealthy(); } + /** + * This method checks if the backend is healthy. It does this by checking if the database, + * elasticsearch, cache, local file system, and asset file system are healthy. + * + * @return If the backend is healthy, returns {@code true}. + */ boolean isBackendHealthy() { - return subSystemStats.isDBHealthy && subSystemStats.isLiveIndexHealthy && subSystemStats.isWorkingIndexHealthy - && - subSystemStats.isCacheHealthy && subSystemStats.isLocalFileSystemHealthy - && subSystemStats.isAssetFileSystemHealthy; + return this.dBHealthy && this.esHealthy && this.cacheHealthy && this.localFSHealthy + && this.assetFSHealthy; } + /** + * This method checks if the frontend is healthy. It does this by checking if the database, + * elasticsearch, cache, local file system, and asset file system are healthy. + * + * @return If the frontend is healthy, returns {@code true}. + */ boolean isFrontendHealthy() { - return subSystemStats.isDBHealthy && subSystemStats.isLiveIndexHealthy && subSystemStats.isCacheHealthy && - subSystemStats.isLocalFileSystemHealthy && subSystemStats.isAssetFileSystemHealthy; + return this.dBHealthy && this.esHealthy && this.cacheHealthy && + this.localFSHealthy && this.assetFSHealthy; } - + /** + * This method converts the monitor stats to a map. + * + * @return A map containing the monitor stats. + */ Map toMap() { - final Map subsystems = Map.of( - "dbSelectHealthy", subSystemStats.isDBHealthy, - "indexLiveHealthy", subSystemStats.isLiveIndexHealthy, - "indexWorkingHealthy", subSystemStats.isWorkingIndexHealthy, - "cacheHealthy", subSystemStats.isCacheHealthy, - "localFSHealthy", subSystemStats.isLocalFileSystemHealthy, - "assetFSHealthy", subSystemStats.isAssetFileSystemHealthy); + "dbSelectHealthy", this.dBHealthy, + "esHealthy", this.esHealthy, + "cacheHealthy", this.cacheHealthy, + "localFSHealthy", this.localFSHealthy, + "assetFSHealthy", this.assetFSHealthy); return Map.of( - "serverID", this.serverId, - "clusterID", this.clusterId, "dotCMSHealthy", this.isDotCMSHealthy(), "frontendHealthy", this.isFrontendHealthy(), "backendHealthy", this.isBackendHealthy(), "subsystems", subsystems); + } - - + /** + * This class is used to build an instance of {@link MonitorStats}. + */ + public static final class Builder { + + private boolean assetFSHealthy; + private boolean cacheHealthy; + private boolean dBHealthy; + private boolean esHealthy; + private boolean localFSHealthy; + + public Builder assetFSHealthy(boolean assetFSHealthy) { + this.assetFSHealthy = assetFSHealthy; + return this; + } + + public Builder cacheHealthy(boolean cacheHealthy) { + this.cacheHealthy = cacheHealthy; + return this; + } + + public Builder dBHealthy(boolean dBHealthy) { + this.dBHealthy = dBHealthy; + return this; + } + + public Builder localFSHealthy(boolean localFSHealthy) { + this.localFSHealthy = localFSHealthy; + return this; + } + + public Builder esHealthy(boolean esHealthy) { + this.esHealthy = esHealthy; + return this; + } + + public MonitorStats build() { + return new MonitorStats( + assetFSHealthy, + cacheHealthy, + dBHealthy, + esHealthy, + localFSHealthy); + } } -} \ No newline at end of file +}