/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.health;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.error.PwmException;
import password.pwm.svc.PwmService;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.logging.PwmLogger;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class HealthMonitor implements PwmService {
private static final PwmLogger LOGGER = PwmLogger.forClass(HealthMonitor.class);
private PwmApplication pwmApplication;
private final Set<HealthRecord> healthRecords = new TreeSet<>();
private static final List<HealthChecker> HEALTH_CHECKERS;
static {
final List<HealthChecker> records = new ArrayList<>();
records.add(new LDAPStatusChecker());
records.add(new JavaChecker());
records.add(new ConfigurationChecker());
records.add(new LocalDBHealthChecker());
records.add(new CertificateChecker());
records.add(new ApplianceStatusChecker());
HEALTH_CHECKERS = records;
}
private ScheduledExecutorService executorService;
private HealthMonitorSettings settings;
private volatile Instant lastHealthCheckTime = Instant.now();
private volatile Instant lastRequestedUpdateTime = Instant.now();
private Map<HealthMonitorFlag, Serializable> healthProperties = new HashMap<>();
private STATUS status = STATUS.NEW;
enum HealthMonitorFlag {
LdapVendorSameCheck,
AdPasswordPolicyApiCheck,
}
public enum CheckTimeliness {
/* Execute update immediately and wait for results */
Immediate,
/* Take current data unless its ancient */
CurrentButNotAncient,
/* Take current data even if its ancient and never block */
NeverBlock,
}
public HealthMonitor() {
}
public Instant getLastHealthCheckTime() {
if (status != STATUS.OPEN) {
return null;
}
return lastHealthCheckTime;
}
public HealthStatus getMostSevereHealthStatus(final CheckTimeliness timeliness) {
if (status != STATUS.OPEN) {
return HealthStatus.GOOD;
}
return getMostSevereHealthStatus(getHealthRecords(timeliness));
}
public static HealthStatus getMostSevereHealthStatus(final Collection<HealthRecord> healthRecords) {
HealthStatus returnStatus = HealthStatus.GOOD;
if (healthRecords != null) {
for (final HealthRecord record : healthRecords) {
if (record.getStatus().getSeverityLevel() > returnStatus.getSeverityLevel()) {
returnStatus = record.getStatus();
}
}
}
return returnStatus;
}
public STATUS status() {
return status;
}
public void init(final PwmApplication pwmApplication) throws PwmException {
status = STATUS.OPENING;
this.pwmApplication = pwmApplication;
settings = HealthMonitorSettings.fromConfiguration(pwmApplication.getConfig());
if (!Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.HEALTHCHECK_ENABLED))) {
LOGGER.debug("health monitor will remain inactive due to AppProperty " + AppProperty.HEALTHCHECK_ENABLED.getKey());
status = STATUS.CLOSED;
return;
}
executorService = Executors.newSingleThreadScheduledExecutor(
JavaHelper.makePwmThreadFactory(
JavaHelper.makeThreadName(pwmApplication, this.getClass()) + "-",
true
));
executorService.scheduleAtFixedRate(new ScheduledUpdater(), 0, settings.getNominalCheckInterval().getTotalMilliseconds(), TimeUnit.MILLISECONDS);
status = STATUS.OPEN;
}
public Set<HealthRecord> getHealthRecords(final CheckTimeliness timeliness) {
if (status != STATUS.OPEN) {
return Collections.emptySet();
}
lastRequestedUpdateTime = Instant.now();
{
final boolean recordsAreStale = TimeDuration.fromCurrent(lastHealthCheckTime).isLongerThan(settings.getMaximumRecordAge());
if (timeliness == CheckTimeliness.Immediate || (timeliness == CheckTimeliness.CurrentButNotAncient && recordsAreStale)) {
final ScheduledFuture updateTask = executorService.schedule(new ImmediateUpdater(), 0, TimeUnit.NANOSECONDS);
final Date beginWaitTime = new Date();
while (!updateTask.isDone() && TimeDuration.fromCurrent(beginWaitTime).isShorterThan(settings.getMaximumForceCheckWait())) {
JavaHelper.pause(500);
}
}
}
final boolean recordsAreStale = TimeDuration.fromCurrent(lastHealthCheckTime).isLongerThan(settings.getMaximumRecordAge());
if (recordsAreStale) {
return Collections.singleton(HealthRecord.forMessage(HealthMessage.NoData));
}
return Collections.unmodifiableSet(healthRecords);
}
public void close() {
if (executorService != null) {
executorService.shutdown();
}
healthRecords.clear();
status = STATUS.CLOSED;
}
public List<HealthRecord> healthCheck() {
return Collections.emptyList();
}
private void doHealthChecks() {
if (status != STATUS.OPEN) {
return;
}
final TimeDuration timeSinceLastUpdate = TimeDuration.fromCurrent(lastHealthCheckTime);
if (timeSinceLastUpdate.isShorterThan(settings.getMinimumCheckInterval().getTotalMilliseconds(), TimeUnit.MILLISECONDS)) {
return;
}
final Instant startTime = Instant.now();
LOGGER.trace("beginning background health check process");
final List<HealthRecord> tempResults = new ArrayList<>();
for (final HealthChecker loopChecker : HEALTH_CHECKERS) {
try {
final List<HealthRecord> loopResults = loopChecker.doHealthCheck(pwmApplication);
if (loopResults != null) {
tempResults.addAll(loopResults);
}
} catch (Exception e) {
if (status == STATUS.OPEN) {
LOGGER.warn("unexpected error during healthCheck: " + e.getMessage(), e);
}
}
}
for (final PwmService service : pwmApplication.getPwmServices()) {
try {
final List<HealthRecord> loopResults = service.healthCheck();
if (loopResults != null) {
tempResults.addAll(loopResults);
}
} catch (Exception e) {
if (status == STATUS.OPEN) {
LOGGER.warn("unexpected error during healthCheck: " + e.getMessage(), e);
}
}
}
healthRecords.clear();
healthRecords.addAll(tempResults);
lastHealthCheckTime = Instant.now();
LOGGER.trace("health check process completed (" + TimeDuration.fromCurrent(startTime).asCompactString() + ")");
}
public ServiceInfo serviceInfo()
{
return new ServiceInfo(Collections.emptyList());
}
public Map<HealthMonitorFlag, Serializable> getHealthProperties()
{
return healthProperties;
}
private class ScheduledUpdater implements Runnable {
@Override
public void run() {
final TimeDuration timeSinceLastRequest = TimeDuration.fromCurrent(lastRequestedUpdateTime);
if (timeSinceLastRequest.isShorterThan(settings.getNominalCheckInterval().getTotalMilliseconds() + 1000, TimeUnit.MILLISECONDS)) {
try {
doHealthChecks();
} catch (Throwable e) {
LOGGER.error("error during health check execution: " + e.getMessage(), e);
}
}
}
}
private class ImmediateUpdater implements Runnable {
@Override
public void run() {
final TimeDuration timeSinceLastUpdate = TimeDuration.fromCurrent(lastHealthCheckTime);
if (timeSinceLastUpdate.isLongerThan(settings.getMinimumCheckInterval().getTotalMilliseconds(), TimeUnit.MILLISECONDS)){
try {
doHealthChecks();
} catch (Throwable e) {
LOGGER.error("error during health check execution: " + e.getMessage(), e);
}
}
}
}
}