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}