001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.health; 018 019import static java.util.concurrent.TimeUnit.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022 023import java.time.*; 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.rest.server.*; 028 029/** 030 * Shared health-probe aggregation worker bean. 031 * 032 * <p> 033 * Holds the capability logic for evaluating {@link HealthIndicator} beans and summarizing them into a 034 * probe response. The {@link HealthServlet} (servlet flavor) and {@link HealthMixin} (mixin flavor) are 035 * independent {@code @Rest} classes that each delegate to a {@code HealthAggregator}, so the two 036 * deployment forms cannot drift. 037 * 038 * @since 10.0.0 039 */ 040@SuppressWarnings({ 041 "resource" // BeanStore is not owned here; its lifecycle is managed by RestContext. 042}) 043public class HealthAggregator { 044 045 /** 046 * Resolves the {@link HealthIndicator} beans registered in the given context's bean store. 047 * 048 * @param context The REST context whose bean store supplies the indicators. 049 * @return The indicator beans keyed by bean name. 050 */ 051 public Map<String,HealthIndicator> indicators(RestContext context) { 052 return context.getBeanStore().getBeansOfType(HealthIndicator.class); 053 } 054 055 /** 056 * Evaluates the supplied indicators, summarizes their statuses, and sets the response status. 057 * 058 * @param context The REST context (supplies the {@link HealthProbeSettings} timeout). 059 * @param indicators The indicators to evaluate. 060 * @param probe The probe filter, or {@code null} to evaluate all indicators. 061 * @param res The response whose status code is set (200 healthy, 503 down). 062 * @return The aggregated health payload. 063 */ 064 public HealthResponse aggregate(RestContext context, Map<String,HealthIndicator> indicators, HealthProbe probe, RestResponse res) { 065 var out = new LinkedHashMap<String,ComponentHealth>(); 066 var timeout = context.getBeanStore().getBean(HealthProbeSettings.class).map(HealthProbeSettings::getTimeout).orElse(Duration.ofSeconds(1)); 067 var timeoutMillis = Math.max(1L, timeout.toMillis()); 068 069 for (var e : indicators.entrySet()) { 070 var indicator = e.getValue(); 071 if (probe != null && !indicator.probes().contains(probe)) 072 continue; 073 var componentName = firstNonEmpty(e.getKey(), cns(indicator)); 074 out.put(componentName, runIndicator(componentName, indicator, timeoutMillis)); 075 } 076 077 var status = summarize(out.values()); 078 079 // Readiness gate: when shutdown has flipped the readiness state out of service, force the READY probe 080 // down so /readyz returns 503 before the connector closes. /livez (LIVE) and /healthz (null) are unaffected. 081 if (probe == HealthProbe.READY && ! ReadinessState.resolve(context.getBeanStore()).isReady()) { 082 status = HealthStatus.DOWN; 083 out.put("readiness", new ComponentHealth(HealthStatus.DOWN, Map.of("state", "OUT_OF_SERVICE"))); 084 } 085 086 res.setStatus(status == HealthStatus.DOWN ? 503 : 200); 087 return new HealthResponse(status, out); 088 } 089 090 private static ComponentHealth runIndicator(String name, HealthIndicator indicator, long timeoutMillis) { 091 var future = CompletableFuture.supplyAsync(indicator::check); 092 try { 093 var h = future.get(timeoutMillis, MILLISECONDS); 094 return ComponentHealth.from(h); 095 } catch (TimeoutException e) { 096 future.cancel(true); 097 return ComponentHealth.from(Health.down(name, e).detail("error", "Health check timed out after " + timeoutMillis + "ms").build()); 098 } catch (ExecutionException e) { 099 return ComponentHealth.from(Health.down(name, e.getCause()).build()); 100 } catch (InterruptedException e) { 101 Thread.currentThread().interrupt(); 102 return ComponentHealth.from(Health.down(name, e).build()); 103 } catch (Exception e) { 104 return ComponentHealth.from(Health.down(name, e).build()); 105 } 106 } 107 108 private static HealthStatus summarize(Collection<ComponentHealth> components) { 109 var hasUnknown = false; 110 for (var c : components) { 111 if (c.status == HealthStatus.DOWN) 112 return HealthStatus.DOWN; 113 if (c.status == HealthStatus.UNKNOWN) 114 hasUnknown = true; 115 } 116 return hasUnknown ? HealthStatus.UNKNOWN : HealthStatus.UP; 117 } 118 119 /** 120 * Probe response payload. 121 */ 122 public static class HealthResponse { 123 private final HealthStatus status; 124 private final Map<String,ComponentHealth> components; 125 126 HealthResponse(HealthStatus status, Map<String,ComponentHealth> components) { 127 this.status = status; 128 this.components = components; 129 } 130 131 /** 132 * @return The aggregated probe status. 133 */ 134 public HealthStatus getStatus() { return status; } 135 136 /** 137 * @return The per-component health entries. 138 */ 139 public Map<String,ComponentHealth> getComponents() { return components; } 140 } 141 142 /** 143 * Component payload in the response. 144 */ 145 public static class ComponentHealth { 146 private final HealthStatus status; 147 private final Map<String,Object> details; 148 149 ComponentHealth(HealthStatus status, Map<String,Object> details) { 150 this.status = status; 151 this.details = details; 152 } 153 154 static ComponentHealth from(Health h) { 155 var details = new LinkedHashMap<>(h.getDetails()); 156 if (h.getError() != null) 157 details.putIfAbsent("error", h.getError().toString()); 158 return new ComponentHealth(h.getStatus(), details); 159 } 160 161 /** 162 * @return The component status. 163 */ 164 public HealthStatus getStatus() { return status; } 165 166 /** 167 * @return The component detail map. 168 */ 169 public Map<String,Object> getDetails() { return details; } 170 } 171}