All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kiwiproject.metrics.health.HealthStatus Maven / Gradle / Ivy

Go to download

Very small library that augments Dropwizard Metrics health checks with a severity detail.

The newest version!
package org.kiwiproject.metrics.health;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toSet;
import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull;
import static org.kiwiproject.collect.KiwiMaps.isNullOrEmpty;
import static org.kiwiproject.metrics.health.HealthCheckResults.SEVERITY_DETAIL;

import com.google.common.collect.Iterables;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;

/**
 * This enum is used to indicate the health status/severity for both a service (i.e. multiple running instances)
 * and the status of individual service instances.
 * 

* If you need to compare {@link HealthStatus} instances by severity, use {@link HealthStatus#comparingSeverity()} * to obtain a {@link Comparator}. * * @implNote Even though the natural order of the constants is defined from lowest to highest severity, we don't * encourage relying on this and instead encourage using {@link HealthStatus#comparingSeverity()} to obtain a * Comparator. For example, even though no changes are anticipated, it is possible a new constant could be added * that falls between existing values, and could break existing assumptions. */ @Slf4j public enum HealthStatus { /** * Everything is OK and healthy. *

* This should only be used when health checks return {@code healthy=true}. */ OK(1), /** * This can be used to provide additional information that may help to diagnose a problem, or simply to communicate * some information that we might want to act upon. *

* This can be used whether health checks return {@code healthy=true} or {@code healthy=false}. */ INFO(2), /** * Something is wrong or potentially wrong, or there is something concerning or that we need to take action on. *

* This can be used whether health checks return {@code healthy=true} or {@code healthy=false}. */ WARN(3), /** * There is an actual problem that needs to be addressed and corrected quickly for the service to function properly. *

* This should only be used when health checks return {@code healthy=false}. */ CRITICAL(4), /** * This level is reserved for the case when all instances of a service are down, and thus the system cannot * perform its services and functions. It needs to be corrected immediately. *

* This should only be used when there are no service instances. */ FATAL(5); /** * Internal value used to compare severity (we do NOT want to rely on the ordinal of the enum constants). */ @Getter(AccessLevel.PACKAGE) private final int value; HealthStatus(int value) { this.value = value; } /** * Given a map containing the results of all the health checks in a service instance (e.g. the JSON that is * returned by calling the {@code healthcheck} endpoint of an instance), determine the appropriate health status * by checking both the {@code healthy} flag (true or false) as well as the {@code severity} if present. * * @param healthDetails the health check results as a map of maps * @return the most appropriate {@link HealthStatus} * @implNote Expects a map of maps structured like: {@code string ->(string -> object)}. The map's keys * are the names of the individual checks (e.g. database, serverErrors, rottenTomato, etc.) while the values * are maps containing the health check result, which at a minimum should contain a boolean {@code healthy} and * can contain a {@code severity} whose values should be the exact names of this enum as a string, e.g. "INFO". */ public static HealthStatus from(Map healthDetails) { if (isNullOrEmpty(healthDetails)) { return CRITICAL; } var healthStatuses = healthDetails.values() .stream() .filter(Map.class::isInstance) .map(Map.class::cast) .map(HealthStatus::determineOverallStatus) .collect(toSet()); if (healthStatuses.isEmpty()) { return CRITICAL; } if (healthStatuses.size() == 1) { return Iterables.getOnlyElement(healthStatuses); } return highestSeverity(healthStatuses); } private static HealthStatus determineOverallStatus(Map map) { var healthy = getHealthyValue(map); var severity = getHealthStatusOrNull(map); return determineOverallStatus(healthy, severity); } // Assumes the map contains a "healthy" key with boolean value, otherwise returns false. private static boolean getHealthyValue(Map map) { try { var value = map.getOrDefault("healthy", Boolean.FALSE).toString(); return Boolean.parseBoolean(value); } catch (Exception e) { LOG.warn("Something gave us a 'healthy' value that threw an exception on toString()"); return false; } } // Suppress "Exception handlers should preserve the original exceptions" - we know the cause in this case is that // the severity value is null or not a valid enum constant, so the stack trace provides no additional help @SuppressWarnings("java:S1166") private static HealthStatus getHealthStatusOrNull(Map map) { if (!map.containsKey(SEVERITY_DETAIL)) { return null; } Object severityObj = map.get(SEVERITY_DETAIL); String severity = severityOrNull(severityObj); try { return Optional.ofNullable(severity) .map(HealthStatus::valueOf) .orElse(HealthStatus.WARN); } catch (Exception e) { LOG.error("Something gave us an invalid severity: {} (returning WARN). Health map: {}", severity, map); return WARN; } } private static String severityOrNull(Object severityObj) { if (severityObj instanceof String severity) { return severity; } LOG.warn("Something gave us a severity that was not a String: {}", severityObj); return null; } private static HealthStatus determineOverallStatus(boolean healthy, @Nullable HealthStatus severity) { if (isInvalidCombination(healthy, severity)) { LOG.warn("Detected invalid (healthy, severity) combination: ({}, {})", healthy, severity); return HealthStatus.max(WARN, Optional.ofNullable(severity).orElse(WARN)); } if (isNull(severity)) { return HealthStatus.from(healthy); } if (healthy) { return HealthStatus.max(OK, severity); } return severity; } /** * Return the default severity for the given healthy value. * * @param healthy true if healthy, false otherwise * @return the default HealthStatus */ public static HealthStatus defaultSeverityForValue(boolean healthy) { return healthy ? OK : WARN; } /** * Is the given (healthy, severity) combination valid? * * @param healthy true if healthy, false otherwise * @param severity the severity, if null the return value will always be true * @return true if the given (healthy, severity) combination is valid, otherwise false */ public static boolean isValidCombination(boolean healthy, @Nullable HealthStatus severity) { return !isInvalidCombination(healthy, severity); } /** * Is the given (healthy, severity) combination invalid? * * @param healthy true if healthy, false otherwise * @param severity the severity, if null the return value will always be false (i.e. it's a valid combination) * @return true if the given (healthy, severity) combination is NOT valid, otherwise false */ public static boolean isInvalidCombination(boolean healthy, @Nullable HealthStatus severity) { if (isNull(severity)) { return false; } var healthyWithInvalidSeverity = healthy && (severity == CRITICAL || severity == FATAL); var unhealthyWithInvalidSeverity = !healthy && severity == OK; return healthyWithInvalidSeverity || unhealthyWithInvalidSeverity; } /** * Return {@link #OK} if {@code value} is {@code true}; {@link #WARN} otherwise (including if {@code null}). * * @param value a (nullable) Boolean value * @return {@link #OK} if {@code value} is {@code true}; {@link #WARN} otherwise */ public static HealthStatus from(Boolean value) { return BooleanUtils.toBoolean(value) ? OK : WARN; } /** * Return the higher of the two given status values. * * @param status1 the first status * @param status2 the second status * @return the highest of the statuses * @throws IllegalArgumentException if either argument is null */ public static HealthStatus max(HealthStatus status1, HealthStatus status2) { checkArgumentNotNull(status1); checkArgumentNotNull(status2); return compare(status1, status2) > 0 ? status1 : status2; } private static int compare(HealthStatus status1, HealthStatus status2) { return comparingSeverity().compare(status1, status2); } /** * Return the highest severity in the (non-null, non-empty) collection of status values. * * @param statuses a collection of {@link HealthStatus} * @return the highest severity of the given status values * @throws IllegalArgumentException if the status list is null or empty */ public static HealthStatus highestSeverity(Collection statuses) { checkArgument(nonNull(statuses) && !statuses.isEmpty(), "statuses cannot be empty or null"); return Collections.max(statuses, comparingSeverity()); } /** * Return a {@link Comparator} that compares {@link HealthStatus} objects from lowest to highest severity. * * @return a comparator that orders from lowest to highest severity */ public static Comparator comparingSeverity() { return HealthStatusComparator.INSTANCE; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy