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.camel.impl.health; 018 019import java.time.Duration; 020import java.time.ZonedDateTime; 021import java.time.format.DateTimeFormatter; 022import java.util.Collections; 023import java.util.Map; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.concurrent.ConcurrentMap; 026 027import org.apache.camel.health.HealthCheck; 028import org.apache.camel.health.HealthCheckConfiguration; 029import org.apache.camel.health.HealthCheckResultBuilder; 030import org.apache.camel.util.ObjectHelper; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034public abstract class AbstractHealthCheck implements HealthCheck { 035 public static final String CHECK_ID = "check.id"; 036 public static final String CHECK_GROUP = "check.group"; 037 public static final String CHECK_ENABLED = "check.enabled"; 038 public static final String INVOCATION_COUNT = "invocation.count"; 039 public static final String INVOCATION_TIME = "invocation.time"; 040 public static final String INVOCATION_ATTEMPT_TIME = "invocation.attempt.time"; 041 public static final String FAILURE_COUNT = "failure.count"; 042 043 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHealthCheck.class); 044 045 private final Object lock; 046 private final String group; 047 private final String id; 048 private final ConcurrentMap<String, Object> meta; 049 050 private HealthCheckConfiguration configuration; 051 private HealthCheck.Result lastResult; 052 private ZonedDateTime lastInvocation; 053 054 protected AbstractHealthCheck(String id) { 055 this(null, id, null); 056 } 057 058 protected AbstractHealthCheck(String group, String id) { 059 this(group, id, null); 060 } 061 062 protected AbstractHealthCheck(String group, String id, Map<String, Object> meta) { 063 this.lock = new Object(); 064 this.group = group; 065 this.id = ObjectHelper.notNull(id, "HealthCheck ID"); 066 this.configuration = new HealthCheckConfiguration(); 067 this.meta = new ConcurrentHashMap<>(); 068 069 if (meta != null) { 070 this.meta.putAll(meta); 071 } 072 073 this.meta.put(CHECK_ID, id); 074 if (group != null) { 075 this.meta.putIfAbsent(CHECK_GROUP, group); 076 } 077 } 078 079 @Override 080 public String getId() { 081 return id; 082 } 083 084 @Override 085 public String getGroup() { 086 return group; 087 } 088 089 @Override 090 public Map<String, Object> getMetaData() { 091 return Collections.unmodifiableMap(this.meta); 092 } 093 094 @Override 095 public HealthCheckConfiguration getConfiguration() { 096 return this.configuration; 097 } 098 099 public void setConfiguration(HealthCheckConfiguration configuration) { 100 this.configuration = configuration; 101 } 102 103 @Override 104 public Result call() { 105 return call(Collections.emptyMap()); 106 } 107 108 @Override 109 public Result call(Map<String, Object> options) { 110 synchronized (lock) { 111 final HealthCheckConfiguration conf = getConfiguration(); 112 final HealthCheckResultBuilder builder = HealthCheckResultBuilder.on(this); 113 final ZonedDateTime now = ZonedDateTime.now(); 114 final boolean enabled = ObjectHelper.supplyIfEmpty(conf.isEnabled(), HealthCheckConfiguration::defaultValueEnabled); 115 final Duration interval = ObjectHelper.supplyIfEmpty(conf.getInterval(), HealthCheckConfiguration::defaultValueInterval); 116 final Integer threshold = ObjectHelper.supplyIfEmpty(conf.getFailureThreshold(), HealthCheckConfiguration::defaultValueFailureThreshold); 117 118 // Extract relevant information from meta data. 119 int invocationCount = (Integer)meta.getOrDefault(INVOCATION_COUNT, 0); 120 int failureCount = (Integer)meta.getOrDefault(FAILURE_COUNT, 0); 121 122 String invocationTime = now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME); 123 boolean call = true; 124 125 // Set common meta-data 126 meta.put(INVOCATION_ATTEMPT_TIME, invocationTime); 127 128 if (!enabled) { 129 LOGGER.debug("health-check {}/{} won't be invoked as not enabled", getGroup(), getId()); 130 131 builder.message("Disabled"); 132 builder.detail(CHECK_ENABLED, false); 133 134 return builder.unknown().build(); 135 } 136 137 // check if the last invocation is far enough to have this check invoked 138 // again without violating the interval configuration. 139 if (lastResult != null && lastInvocation != null && !interval.isZero()) { 140 Duration elapsed = Duration.between(lastInvocation, now); 141 142 if (elapsed.compareTo(interval) < 0) { 143 LOGGER.debug("health-check {}/{} won't be invoked as interval ({}) is not yet expired (last-invocation={})", 144 getGroup(), 145 getId(), 146 elapsed, 147 lastInvocation); 148 149 call = false; 150 } 151 } 152 153 // Invoke the check. 154 if (call) { 155 LOGGER.debug("Invoke health-check {}/{}", getGroup(), getId()); 156 157 doCall(builder, options); 158 159 // State should be set here 160 ObjectHelper.notNull(builder.state(), "Response State"); 161 162 if (builder.state() == State.DOWN) { 163 // If the service is un-healthy but the number of time it 164 // has been consecutively reported in this state is less 165 // than the threshold configured, mark it as UP. This is 166 // used to avoid false positive in case of glitches. 167 if (failureCount++ < threshold) { 168 LOGGER.debug("Health-check {}/{} has status DOWN but failure count ({}) is less than configured threshold ({})", 169 getGroup(), 170 getId(), 171 failureCount, 172 threshold); 173 174 builder.up(); 175 } 176 } else { 177 failureCount = 0; 178 } 179 180 meta.put(INVOCATION_TIME, invocationTime); 181 meta.put(FAILURE_COUNT, failureCount); 182 meta.put(INVOCATION_COUNT, ++invocationCount); 183 184 // Copy some of the meta-data bits to the response attributes so the 185 // response caches the health-check state at the time of the invocation. 186 builder.detail(INVOCATION_TIME, meta.get(INVOCATION_TIME)); 187 builder.detail(INVOCATION_COUNT, meta.get(INVOCATION_COUNT)); 188 builder.detail(FAILURE_COUNT, meta.get(FAILURE_COUNT)); 189 190 // update last invocation time. 191 lastInvocation = now; 192 } else if (lastResult != null) { 193 lastResult.getMessage().ifPresent(builder::message); 194 lastResult.getError().ifPresent(builder::error); 195 196 builder.state(lastResult.getState()); 197 builder.details(lastResult.getDetails()); 198 } 199 200 lastResult = builder.build(); 201 202 return lastResult; 203 } 204 } 205 206 @Override 207 public boolean equals(Object o) { 208 if (this == o) { 209 return true; 210 } 211 if (o == null || getClass() != o.getClass()) { 212 return false; 213 } 214 215 AbstractHealthCheck check = (AbstractHealthCheck) o; 216 217 return id != null ? id.equals(check.id) : check.id == null; 218 } 219 220 @Override 221 public int hashCode() { 222 return id != null ? id.hashCode() : 0; 223 } 224 225 protected final void addMetaData(String key, Object value) { 226 meta.put(key, value); 227 } 228 229 /** 230 * Invoke the health check. 231 * 232 * @see {@link HealthCheck#call(Map)} 233 */ 234 protected abstract void doCall(HealthCheckResultBuilder builder, Map<String, Object> options); 235}