/** * Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.sixt.service.framework.health; import com.google.inject.Inject; import com.google.inject.Singleton; import com.sixt.service.framework.ServiceProperties; import com.sixt.service.framework.metrics.GoGauge; import com.sixt.service.framework.metrics.MetricBuilderFactory; import com.sixt.service.framework.registry.consul.RegistrationManager; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import static com.sixt.service.framework.FeatureFlags.DEFAULT_HEALTH_CHECK_POLL_INTERVAL; import static com.sixt.service.framework.FeatureFlags.HEALTH_CHECK_POLL_INTERVAL; @Singleton public class HealthCheckManager implements Runnable { private static final Logger logger = LoggerFactory.getLogger(HealthCheckManager.class); private final int pollTime; //in seconds private final MetricBuilderFactory metricBuilderFactory; private final RegistrationManager registrationManager; private final String serviceId; private final ServiceProperties serviceProps; private final HttpClient httpClient; private final AtomicBoolean isShutdown = new AtomicBoolean(false); private ScheduledExecutorService executorService; protected Deque<HealthCheckContributor> pollingContributors = new ConcurrentLinkedDeque<>(); protected Deque<HealthCheckContributor> oneShotContributors = new ConcurrentLinkedDeque<>(); protected int failingChecksGauge; @Inject public HealthCheckManager(MetricBuilderFactory metricBuilderFactory, RegistrationManager registrationManager, ServiceProperties serviceProps, HttpClient httpClient) { this.metricBuilderFactory = metricBuilderFactory; this.registrationManager = registrationManager; this.serviceProps = serviceProps; this.serviceId = serviceProps.getServiceInstanceId(); this.httpClient = httpClient; pollTime = serviceProps.getIntegerProperty(HEALTH_CHECK_POLL_INTERVAL, DEFAULT_HEALTH_CHECK_POLL_INTERVAL); } public void registerPollingContributor(HealthCheckContributor contrib) { pollingContributors.add(contrib); } /** * A One-shot contributor is one that is removed from the list of contributors * as soon as it is healthy again. */ public void registerOneShotContributor(HealthCheckContributor contrib) { oneShotContributors.add(contrib); reportCurrentStatus(); } public void initialize() { GoGauge gauge = metricBuilderFactory.newMetric("health_checks").buildGauge(); gauge.register("failing", () -> failingChecksGauge); executorService = Executors.newScheduledThreadPool(1); executorService.scheduleAtFixedRate(this, 0, pollTime, TimeUnit.SECONDS); } @Override public void run() { reportCurrentStatus(); } private void reportCurrentStatus() { try { if (registrationManager.isRegistered()) { Collection<HealthCheck> healthChecks = getCurrentHealthChecks(); HealthCheck.Status summary = getSummaryFor(healthChecks); updateHealthStatus(summary); } else { logger.info("Waiting for {} to be registered before updating health" , serviceProps.getServiceName()); } } catch (Exception ex) { if (!isShutdown.get()) { logger.warn("Caught exception updating health status", ex); } } } //TODO: specific to consul; need to refactor public void updateHealthStatus(HealthCheck.Status status) throws Exception { logger.trace("Updating health of {}", serviceProps.getServiceName()); ContentResponse httpResponse = httpClient.newRequest(getHealthCheckUri(status)).send(); if (httpResponse.getStatus() != 200) { logger.warn("Received {} trying to update health", httpResponse.getStatus()); } } private String getHealthCheckUri(HealthCheck.Status status) throws UnsupportedEncodingException { StringBuilder sb = new StringBuilder("http://").append(serviceProps.getRegistryServer()). append("/v1/agent/check/").append(status.getConsulStatus()).append("/service:"). append(serviceId); return sb.toString(); } public Collection<HealthCheck> getCurrentHealthChecks() { int failCount = 0; List<HealthCheck> retval = new ArrayList<>(); Iterator<HealthCheckContributor> iter = pollingContributors.iterator(); while (iter.hasNext()) { HealthCheckContributor contributor = iter.next(); HealthCheck hc = contributor.getHealthCheck(); if (hc != null) { retval.add(hc); if (! hc.isStatus(HealthCheck.Status.PASS)) { failCount++; } } } iter = oneShotContributors.iterator(); while (iter.hasNext()) { HealthCheckContributor contributor = iter.next(); HealthCheck hc = contributor.getHealthCheck(); if (hc != null) { retval.add(hc); if (HealthCheck.Status.PASS.equals(hc.getStatus())) { iter.remove(); } else { failCount++; } } } failingChecksGauge = failCount; return retval; } /** * We return the most severe of the statuses within the collection */ public HealthCheck.Status getSummaryFor(Collection<HealthCheck> checks) { HealthCheck.Status retval = HealthCheck.Status.PASS; for (HealthCheck check : checks) { if (check.getStatus().moreSevereThan(retval)) { retval = check.getStatus(); } } return retval; } public void shutdown() { isShutdown.set(true); executorService.shutdown(); } }