You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gh...@apache.org on 2018/12/18 22:58:17 UTC

svn commit: r1849246 [3/6] - in /felix/trunk/healthcheck: ./ api/ api/src/ api/src/main/ api/src/main/java/ api/src/main/java/org/ api/src/main/java/org/apache/ api/src/main/java/org/apache/felix/ api/src/main/java/org/apache/felix/hc/ api/src/main/jav...

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckFuture.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor;
+
+import static org.apache.felix.hc.util.FormattingResultLog.msHumanReadable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.commons.lang3.reflect.MethodUtils;
+import org.apache.commons.lang3.time.StopWatch;
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.util.FormattingResultLog;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Future to be able to schedule a health check for parallel execution. */
+public class HealthCheckFuture extends FutureTask<ExecutionResult> {
+
+    public interface Callback {
+        public void finished(final HealthCheckExecutionResult result);
+    }
+
+    private final static Logger LOG = LoggerFactory.getLogger(HealthCheckFuture.class);
+
+    private final HealthCheckMetadata metadata;
+    private final Date createdTime;
+
+    public HealthCheckFuture(final HealthCheckMetadata metadata, final BundleContext bundleContext, final Callback callback) {
+        super(new Callable<ExecutionResult>() {
+            @Override
+            public ExecutionResult call() throws Exception {
+                Thread.currentThread().setName("HealthCheck " + metadata.getTitle());
+                LOG.debug("Starting check {}", metadata);
+
+                final StopWatch stopWatch = new StopWatch();
+                stopWatch.start();
+                Result resultFromHealthCheck = null;
+                ExecutionResult executionResult = null;
+
+                Object healthCheck = bundleContext.getService(metadata.getServiceReference());
+                try {
+                    if (healthCheck != null) {
+                        if ((healthCheck instanceof HealthCheck)) {
+                            resultFromHealthCheck = ((HealthCheck) healthCheck).execute();
+                        } else {
+                            resultFromHealthCheck = executeLegacyHc(healthCheck);
+                        }
+                    } else {
+                        throw new IllegalStateException("Service for " + metadata + " is gone");
+                    }
+
+                } catch (final Exception e) {
+                    resultFromHealthCheck = new Result(Result.Status.CRITICAL,
+                            "Exception during execution of '" + metadata.getName() + "': " + e, e);
+                } finally {
+                    // unget service ref
+                    bundleContext.ungetService(metadata.getServiceReference());
+
+                    // update result with information about this run
+                    stopWatch.stop();
+                    long elapsedTime = stopWatch.getTime();
+                    if (resultFromHealthCheck != null) {
+                        // wrap the result in an execution result
+                        executionResult = new ExecutionResult(metadata, resultFromHealthCheck, elapsedTime);
+                    }
+                    LOG.debug("Time consumed for {}: {}", metadata, msHumanReadable(elapsedTime));
+                }
+
+                callback.finished(executionResult);
+                Thread.currentThread().setName("HealthCheck-idle");
+                return executionResult;
+            }
+
+        });
+        this.createdTime = new Date();
+        this.metadata = metadata;
+
+    }
+
+    Date getCreatedTime() {
+        return this.createdTime;
+    }
+
+    public HealthCheckMetadata getHealthCheckMetadata() {
+        return metadata;
+    }
+
+    @Override
+    public String toString() {
+        return "[Future for " + this.metadata + ", createdTime=" + this.createdTime + "]";
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static Result executeLegacyHc(Object healthCheck) {
+
+        FormattingResultLog log = new FormattingResultLog();
+        log.debug("Running legacy HC {}, please convert to new interface org.apache.felix.hc.api.HealthCheck!",
+                healthCheck.getClass().getName());
+        try {
+            Object result = MethodUtils.invokeMethod(healthCheck, "execute");
+            Object resultLog = FieldUtils.readField(result, "resultLog", true);
+
+            List entries = (List) FieldUtils.readField(resultLog, "entries", true);
+            for (Object object : entries) {
+                String statusLegacy = String.valueOf(FieldUtils.readField(object, "status", true));
+                String message = (String) FieldUtils.readField(object, "message", true);
+                Exception exception = (Exception) FieldUtils.readField(object, "exception", true);
+                if(statusLegacy.equals("DEBUG")) {
+                    log.add(new ResultLog.Entry(message, true, exception));
+                } else {
+                    statusLegacy = statusLegacy.replace("INFO", "OK");
+                    log.add(new ResultLog.Entry(Result.Status.valueOf(statusLegacy), message, exception));
+                }
+            }
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            log.healthCheckError("Could call and convert Sling HC {} for Felix Runtime", healthCheck.getClass().getName());
+        }
+        return new Result(log);
+    }
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/HealthCheckResultCache.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.Result.Status;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.ResultLog.Entry;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Caches health check results. */
+public class HealthCheckResultCache {
+
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private static final List<Status> NOT_OK_STATUS_VALUES = Arrays.asList(Status.WARN, Status.CRITICAL, Status.HEALTH_CHECK_ERROR);
+
+    /** The map holding the cached results. */
+    private final Map<Long, HealthCheckExecutionResult> cache = new ConcurrentHashMap<Long, HealthCheckExecutionResult>();
+
+    @SuppressWarnings("serial")
+    private final Map<Result.Status, Map<Long, HealthCheckExecutionResult>> cacheOfNotOkResults = new ConcurrentHashMap<Result.Status, Map<Long, HealthCheckExecutionResult>>() {
+        {
+            for (Status status : NOT_OK_STATUS_VALUES) {
+                put(status, new ConcurrentHashMap<Long, HealthCheckExecutionResult>());
+            }
+        }
+    };
+
+    /** Update the cache with the result */
+    public void updateWith(HealthCheckExecutionResult result) {
+        final ExecutionResult executionResult = (ExecutionResult) result;
+        cache.put(executionResult.getServiceId(), result);
+
+        Status status = executionResult.getHealthCheckResult().getStatus();
+        if (status.ordinal() >= Result.Status.WARN.ordinal()) {
+            logger.debug("Caching {} result for HC {}", status, executionResult.getServiceId());
+            cacheOfNotOkResults.get(status).put(executionResult.getServiceId(), result);
+        }
+    }
+
+    /** Get the valid cache results */
+    public void useValidCacheResults(final List<HealthCheckMetadata> metadatas,
+            final Collection<HealthCheckExecutionResult> results,
+            final long resultCacheTtlInMs) {
+        final Set<HealthCheckExecutionResult> cachedResults = new TreeSet<HealthCheckExecutionResult>();
+        final Iterator<HealthCheckMetadata> checksIt = metadatas.iterator();
+        while (checksIt.hasNext()) {
+            final HealthCheckMetadata md = checksIt.next();
+            final HealthCheckExecutionResult result = getValidCacheResult(md, resultCacheTtlInMs);
+            if (result != null) {
+                cachedResults.add(result);
+                checksIt.remove();
+            }
+        }
+        logger.debug("Adding {} results from cache", cachedResults.size());
+        results.addAll(cachedResults);
+    }
+
+    /** Return the cached result if it's still valid. */
+    public HealthCheckExecutionResult getValidCacheResult(final HealthCheckMetadata metadata,
+            final long resultCacheTtlInMs) {
+        return get(metadata, resultCacheTtlInMs);
+    }
+
+    private HealthCheckExecutionResult get(final HealthCheckMetadata metadata, final long globalResultCacheTtlInMs) {
+        final Long key = metadata.getServiceId();
+        final HealthCheckExecutionResult cachedResult = cache.get(key);
+        if (cachedResult != null) {
+            Date finishedAt = cachedResult.getFinishedAt();
+            if (finishedAt == null) {
+                // never cache without proper meta data -> remove it
+                cache.remove(key);
+                return null;
+            }
+
+            long effectiveTtl = getEffectiveTtl(metadata, globalResultCacheTtlInMs);
+            long validUntilLong = finishedAt.getTime() + effectiveTtl;
+            if (validUntilLong < 0) { // if Long.MAX_VALUE is configured, this can become negative
+                validUntilLong = Long.MAX_VALUE;
+            }
+            Date validUntil = new Date(validUntilLong);
+            Date now = new Date();
+            if (validUntil.after(now)) {
+                logger.debug("Cache hit: validUntil={} cachedResult={}", validUntil, cachedResult);
+                return cachedResult;
+            } else {
+                logger.debug("Outdated result: validUntil={} cachedResult={}", validUntil, cachedResult);
+                // not removing result for key as out-dated results are shown for timed out checks if available
+            }
+        }
+
+        // null => no cache hit
+        return null;
+    }
+
+    /** Obtains the effective TTL for a given Metadata descriptor.
+     *
+     * @param metadata Metadata descriptor of health check
+     * @param globalTtl TTL from service configuration of health check executor (used as default)
+     * @return effective TTL */
+    private long getEffectiveTtl(HealthCheckMetadata metadata, long globalTtl) {
+        final long ttl;
+        Long hcTtl = metadata.getResultCacheTtlInMs();
+        if (hcTtl != null && hcTtl > 0) {
+            ttl = hcTtl;
+        } else {
+            ttl = globalTtl;
+        }
+        return ttl;
+    }
+
+    /** Creates a new execution result
+     * 
+     * @param origResult
+     * @return */
+    public HealthCheckExecutionResult createExecutionResultWithStickyResults(HealthCheckExecutionResult origResult) {
+        HealthCheckExecutionResult result = origResult;
+
+        HealthCheckMetadata healthCheckMetadata = origResult.getHealthCheckMetadata();
+        Long warningsStickForMinutes = healthCheckMetadata.getWarningsStickForMinutes();
+        if (warningsStickForMinutes != null) {
+            logger.debug("Taking into account sticky results (up to {} min old) for health check ", warningsStickForMinutes,
+                    healthCheckMetadata.getName());
+            List<HealthCheckExecutionResult> nonOkResultsFromPast = new ArrayList<HealthCheckExecutionResult>();
+            long cutOffTime = System.currentTimeMillis() - (warningsStickForMinutes * 60 * 1000);
+            for (Status status : NOT_OK_STATUS_VALUES) {
+                long hcServiceId = ((ExecutionResult) origResult).getServiceId();
+                HealthCheckExecutionResult nonOkResultFromPast = cacheOfNotOkResults.get(status).get(hcServiceId);
+                if (nonOkResultFromPast == null) {
+                    logger.debug("no sticky result in cache for HC {}", hcServiceId);
+                    continue;
+                }
+                if (nonOkResultFromPast == origResult) {
+                    logger.debug("result already in cache: {} for HC {}, not adding sticky result", origResult, hcServiceId);
+                    continue;
+                }
+                long pastHcTime = nonOkResultFromPast.getFinishedAt().getTime();
+                logger.debug("Time of old {} result: {}", status, pastHcTime);
+                logger.debug("Cut off time: {}", cutOffTime);
+                if (pastHcTime > cutOffTime) {
+                    logger.debug("Found sticky result: {}", nonOkResultFromPast);
+                    nonOkResultsFromPast.add(nonOkResultFromPast);
+                }
+            }
+
+            if (!nonOkResultsFromPast.isEmpty()) {
+                ResultLog resultLog = new ResultLog();
+                resultLog.add(new Entry(Result.Status.OK, "*** Current Result ***"));
+                for (ResultLog.Entry entry : origResult.getHealthCheckResult()) {
+                    resultLog.add(entry);
+                }
+                DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
+                for (HealthCheckExecutionResult nonOkResultFromPast : nonOkResultsFromPast) {
+                    Status status = nonOkResultFromPast.getHealthCheckResult().getStatus();
+                    resultLog.add(
+                            new Entry(Result.Status.WARN,
+                                    "*** Sticky Result " + status + " from " + df.format(nonOkResultFromPast.getFinishedAt()) + " ***"));
+                    for (ResultLog.Entry entry : nonOkResultFromPast.getHealthCheckResult()) {
+                        resultLog.add(entry);
+                    }
+                }
+                result = new ExecutionResult(healthCheckMetadata, new Result(resultLog), origResult.getElapsedTimeInMs());
+            }
+        }
+
+        return result;
+    }
+
+    /** Clear the whole cache */
+    public void clear() {
+        this.cache.clear();
+        for (Status status : NOT_OK_STATUS_VALUES) {
+            cacheOfNotOkResults.get(status).clear();
+        }
+    }
+
+    /** Remove entry from cache */
+    public void removeCachedResult(final Long serviceId) {
+        this.cache.remove(serviceId);
+        for (Status status : NOT_OK_STATUS_VALUES) {
+            cacheOfNotOkResults.get(status).remove(serviceId);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "[HealthCheckResultCache size=" + cache.size() + "]";
+    }
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckExecutor.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor.async;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.felix.hc.core.impl.executor.ExecutionResult;
+import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool;
+import org.apache.felix.hc.core.impl.executor.HealthCheckResultCache;
+import org.apache.felix.hc.util.HealthCheckFilter;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Runs health checks that are configured with a cron expression for asynchronous execution. Used by HealthCheckExecutor.
+ * 
+ * This implementation uses quartz to support the cron syntax (which is not supported by executors from standard java java.util.concurrent
+ * package. */
+@Component(service = AsyncHealthCheckExecutor.class, immediate = true)
+public class AsyncHealthCheckExecutor implements ServiceListener {
+
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class);
+
+    @Reference
+    HealthCheckExecutorThreadPool healthCheckExecutorThreadPool;
+
+    private Map<HealthCheckMetadata, ExecutionResult> asyncResultsByDescriptor = new ConcurrentHashMap<HealthCheckMetadata, ExecutionResult>();
+
+    private Map<HealthCheckMetadata, AsyncHealthCheckJob> registeredJobs = new HashMap<HealthCheckMetadata, AsyncHealthCheckJob>();
+
+    private BundleContext bundleContext;
+
+    private QuartzCronScheduler quartzCronScheduler = null;
+
+    @Activate
+    protected final void activate(final ComponentContext componentContext) {
+        this.bundleContext = componentContext.getBundleContext();
+        this.bundleContext.addServiceListener(this);
+
+        int count = 0;
+        HealthCheckFilter healthCheckFilter = new HealthCheckFilter(bundleContext);
+        final ServiceReference[] healthCheckReferences = healthCheckFilter.getHealthCheckServiceReferences(HealthCheckSelector.empty());
+        for (ServiceReference serviceReference : healthCheckReferences) {
+            HealthCheckMetadata healthCheckMetadata = new HealthCheckMetadata(serviceReference);
+            if (isAsync(healthCheckMetadata)) {
+                if (scheduleHealthCheck(healthCheckMetadata)) {
+                    count++;
+                }
+            }
+        }
+        LOG.debug("Scheduled {} jobs for asynchronous health checks during bundle startup", count);
+    }
+
+    @Deactivate
+    protected final void deactivate(final ComponentContext componentContext) {
+        this.bundleContext.removeServiceListener(this);
+        this.bundleContext = null;
+
+        LOG.debug("Unscheduling {} jobs for asynchronous health checks", registeredJobs.size());
+        for (HealthCheckMetadata healthCheckDescriptor : new LinkedList<HealthCheckMetadata>(registeredJobs.keySet())) {
+            unscheduleHealthCheck(healthCheckDescriptor);
+        }
+
+        if (quartzCronScheduler != null) {
+            quartzCronScheduler.shutdown();
+        }
+    }
+
+    @Override
+    public void serviceChanged(ServiceEvent event) {
+        if (bundleContext == null) {
+            // already deactivated?
+            return;
+        }
+        ServiceReference serviceReference = event.getServiceReference();
+        final boolean isHealthCheck = serviceReference.isAssignableTo(bundleContext.getBundle(), HealthCheck.class.getName());
+
+        if (isHealthCheck) {
+            HealthCheckMetadata healthCheckMetadata = new HealthCheckMetadata(serviceReference);
+            int eventType = event.getType();
+            if (eventType == ServiceEvent.REGISTERED) {
+                LOG.debug("Received service event REGISTERED for health check {}", healthCheckMetadata);
+                scheduleHealthCheck(healthCheckMetadata);
+            } else if (eventType == ServiceEvent.UNREGISTERING) {
+                LOG.debug("Received service event UNREGISTERING for health check {}", healthCheckMetadata);
+                unscheduleHealthCheck(healthCheckMetadata);
+            } else if (eventType == ServiceEvent.MODIFIED) {
+                LOG.debug("Received service event MODIFIED for health check {}", healthCheckMetadata);
+                unscheduleHealthCheck(healthCheckMetadata);
+                scheduleHealthCheck(healthCheckMetadata);
+            }
+
+        }
+    }
+
+    private boolean scheduleHealthCheck(HealthCheckMetadata descriptor) {
+
+        try {
+            AsyncHealthCheckJob healthCheckAsyncJob = null;
+
+            if (isAsyncCron(descriptor)) {
+
+                if (quartzCronScheduler == null) {
+                    if (classExists("org.quartz.CronTrigger")) {
+                        quartzCronScheduler = new QuartzCronScheduler(healthCheckExecutorThreadPool);
+                        LOG.info("Created quartz scheduler for async HC");
+                    } else {
+                        LOG.warn("Can not schedule async health check with cron expression since quartz library is not on classpath");
+                        return false;
+                    }
+                }
+
+                healthCheckAsyncJob = new AsyncHealthCheckQuartzCronJob(descriptor, this, bundleContext, quartzCronScheduler);
+            } else if (isAsyncInterval(descriptor)) {
+
+                healthCheckAsyncJob = new AsyncHealthCheckIntervalJob(descriptor, this, bundleContext, healthCheckExecutorThreadPool);
+            }
+
+            if (healthCheckAsyncJob != null) {
+                healthCheckAsyncJob.schedule();
+                registeredJobs.put(descriptor, healthCheckAsyncJob);
+                return true;
+            } else {
+                return false;
+            }
+
+        } catch (Exception e) {
+            LOG.warn("Could not schedule job for " + descriptor + ". Exception: " + e, e);
+            return false;
+        }
+
+    }
+
+    private boolean unscheduleHealthCheck(HealthCheckMetadata descriptor) {
+
+        // here no check for isAsync must be used to ensure previously
+        // scheduled async checks are correctly unscheduled if they have
+        // changed from async to sync.
+
+        AsyncHealthCheckJob job = registeredJobs.remove(descriptor);
+        if (job != null) {
+            return job.unschedule();
+        } else {
+            LOG.warn("No job was registered for descriptor {}", descriptor);
+            return false;
+        }
+    }
+
+    /** Called by the main Executor to get results from async HCs */
+    public void collectAsyncResults(List<HealthCheckMetadata> healthCheckDescriptors, Collection<HealthCheckExecutionResult> results,
+            HealthCheckResultCache cache) {
+        Iterator<HealthCheckMetadata> checksIt = healthCheckDescriptors.iterator();
+
+        Set<ExecutionResult> asyncResults = new TreeSet<ExecutionResult>();
+        while (checksIt.hasNext()) {
+            HealthCheckMetadata healthCheckMetadata = checksIt.next();
+            if (isAsync(healthCheckMetadata)) {
+                ExecutionResult result = asyncResultsByDescriptor.get(healthCheckMetadata);
+                if (result == null) {
+
+                    result = new ExecutionResult(healthCheckMetadata,
+                            new Result(Result.Status.OK, "Async Health Check with cron expression '"
+                                    + healthCheckMetadata.getAsyncCronExpression() + "' has not yet been executed."),
+                            0L);
+
+                    asyncResults.add(result);
+                }
+                asyncResults.add(result);
+                // remove from HC collection to not execute the check in HealthCheckExecutorImpl
+                checksIt.remove();
+            }
+        }
+
+        LOG.debug("Caching {} results from async results", asyncResults.size());
+        for (ExecutionResult result : asyncResults) {
+            cache.updateWith(result);
+        }
+
+        LOG.debug("Adding {} results from async results", asyncResults.size());
+        results.addAll(asyncResults);
+
+    }
+
+    public void updateWith(HealthCheckExecutionResult result) {
+        if (isAsync(result.getHealthCheckMetadata())) {
+            asyncResultsByDescriptor.put(result.getHealthCheckMetadata(), (ExecutionResult) result);
+            LOG.debug("Updated result for async hc {} with {}", result.getHealthCheckMetadata(), result);
+        }
+    }
+
+    private boolean isAsync(HealthCheckMetadata healthCheckMetadata) {
+        return isAsyncCron(healthCheckMetadata) || isAsyncInterval(healthCheckMetadata);
+    }
+
+    private boolean isAsyncCron(HealthCheckMetadata healthCheckMetadata) {
+        return StringUtils.isNotBlank(healthCheckMetadata.getAsyncCronExpression());
+    }
+
+    private boolean isAsyncInterval(HealthCheckMetadata healthCheckMetadata) {
+        return healthCheckMetadata.getAsyncIntervalInSec() != null;
+    }
+
+    private boolean classExists(String className) {
+        try {
+            Class.forName(className);
+            return true;
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+    }
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckIntervalJob.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor.async;
+
+import java.util.concurrent.ScheduledFuture;
+
+import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AsyncHealthCheckIntervalJob extends AsyncHealthCheckJob implements Runnable {
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class);
+
+    private final HealthCheckExecutorThreadPool healthCheckExecutorThreadPool;
+    private ScheduledFuture<?> scheduleFuture = null;
+
+    public AsyncHealthCheckIntervalJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor,
+            BundleContext bundleContext, HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) {
+        super(healthCheckDescriptor, asyncHealthCheckExecutor, bundleContext);
+        this.healthCheckExecutorThreadPool = healthCheckExecutorThreadPool;
+    }
+
+    public boolean schedule() {
+        Long asyncIntervalInSec = healthCheckDescriptor.getAsyncIntervalInSec();
+        scheduleFuture = healthCheckExecutorThreadPool.scheduleAtFixedRate(this, asyncIntervalInSec);
+        LOG.info("Scheduled job {} for execution every {}sec", this, asyncIntervalInSec);
+        return true;
+    }
+
+    @Override
+    public boolean unschedule() {
+
+        if (scheduleFuture != null) {
+            LOG.debug("Unscheduling async job for {}", healthCheckDescriptor);
+            return scheduleFuture.cancel(false);
+        } else {
+            LOG.debug("No scheduled future for {} exists", healthCheckDescriptor);
+            return false;
+        }
+    }
+
+}
\ No newline at end of file

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckJob.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor.async;
+
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.core.impl.executor.HealthCheckFuture;
+import org.apache.felix.hc.core.impl.executor.HealthCheckFuture.Callback;
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AsyncHealthCheckJob implements Runnable {
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckJob.class);
+
+    protected final HealthCheckMetadata healthCheckDescriptor;
+    protected final AsyncHealthCheckExecutor asyncHealthCheckExecutor;
+    protected final BundleContext bundleContext;
+
+    public AsyncHealthCheckJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor,
+            BundleContext bundleContext) {
+        this.healthCheckDescriptor = healthCheckDescriptor;
+        this.asyncHealthCheckExecutor = asyncHealthCheckExecutor;
+        this.bundleContext = bundleContext;
+    }
+
+    @Override
+    public void run() {
+
+        LOG.debug("Running job {}", this);
+        HealthCheckFuture healthCheckFuture = new HealthCheckFuture(healthCheckDescriptor, bundleContext, new Callback() {
+
+            @Override
+            public void finished(HealthCheckExecutionResult result) {
+                asyncHealthCheckExecutor.updateWith(result);
+            }
+        });
+
+        // run future in same thread (as we are already async via scheduler)
+        healthCheckFuture.run();
+
+    }
+
+    public abstract boolean schedule();
+
+    public abstract boolean unschedule();
+
+    @Override
+    public String toString() {
+        return "[Async job for " + this.healthCheckDescriptor + "]";
+    }
+}
\ No newline at end of file

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/AsyncHealthCheckQuartzCronJob.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor.async;
+
+import static org.quartz.CronScheduleBuilder.cronSchedule;
+import static org.quartz.JobBuilder.newJob;
+import static org.quartz.TriggerBuilder.newTrigger;
+
+import org.apache.felix.hc.util.HealthCheckMetadata;
+import org.osgi.framework.BundleContext;
+import org.quartz.CronTrigger;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AsyncHealthCheckQuartzCronJob extends AsyncHealthCheckJob implements Runnable {
+    private static final Logger LOG = LoggerFactory.getLogger(AsyncHealthCheckExecutor.class);
+
+    private static final String JOB_DATA_KEY_JOB = "asyncHcJob";
+
+    protected final QuartzCronScheduler quartzCronScheduler;
+    private JobKey jobKey = null;
+
+    public AsyncHealthCheckQuartzCronJob(HealthCheckMetadata healthCheckDescriptor, AsyncHealthCheckExecutor asyncHealthCheckExecutor,
+            BundleContext bundleContext, QuartzCronScheduler quartzScheduler) {
+        super(healthCheckDescriptor, asyncHealthCheckExecutor, bundleContext);
+        this.quartzCronScheduler = quartzScheduler;
+    }
+
+    public JobKey getJobKey() {
+        return jobKey;
+    }
+
+    private JobDetail getQuartzJobDetail() {
+        JobDataMap jobData = new JobDataMap();
+        jobData.put(JOB_DATA_KEY_JOB, this);
+
+        JobDetail job = newJob(AsyncHealthCheckQuartzCronJob.QuartzJob.class).setJobData(jobData)
+                .withIdentity("job-hc-" + healthCheckDescriptor.getServiceId(), "async-healthchecks")
+                .build();
+
+        jobKey = job.getKey();
+
+        return job;
+    }
+
+    public boolean schedule() {
+
+        try {
+            Scheduler scheduler = quartzCronScheduler.getScheduler();
+
+            JobDetail job = getQuartzJobDetail();
+            CronTrigger cronTrigger = newTrigger().withSchedule(cronSchedule(healthCheckDescriptor.getAsyncCronExpression())).forJob(job)
+                    .build();
+
+            scheduler.scheduleJob(job, cronTrigger);
+            LOG.info("Scheduled job {} with trigger {}", job, cronTrigger);
+            return true;
+        } catch (SchedulerException e) {
+            LOG.error("Could not schedule job for " + healthCheckDescriptor + ": " + e, e);
+            return false;
+        }
+
+    }
+
+    @Override
+    public boolean unschedule() {
+        Scheduler scheduler = quartzCronScheduler.getScheduler();
+        LOG.debug("Unscheduling job {}", jobKey);
+        try {
+            scheduler.deleteJob(jobKey);
+            return true;
+        } catch (SchedulerException e) {
+            LOG.error("Could not unschedule job for " + jobKey + ": " + e, e);
+            return false;
+        }
+    }
+
+    // quartz forces to pass in a class object (and not an instance), hence this helper class is needed
+    public static class QuartzJob implements Job {
+
+        @Override
+        public void execute(JobExecutionContext context) throws JobExecutionException {
+            AsyncHealthCheckQuartzCronJob hc = (AsyncHealthCheckQuartzCronJob) context.getJobDetail().getJobDataMap().get(JOB_DATA_KEY_JOB);
+            hc.run();
+        }
+
+    }
+
+}
\ No newline at end of file

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/executor/async/QuartzCronScheduler.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.executor.async;
+
+import org.apache.felix.hc.core.impl.executor.HealthCheckExecutorThreadPool;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.impl.DirectSchedulerFactory;
+import org.quartz.simpl.RAMJobStore;
+import org.quartz.spi.ThreadPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class QuartzCronScheduler {
+    private static final Logger LOG = LoggerFactory.getLogger(QuartzCronScheduler.class);
+
+    private static final String HC_SCHEDULER_NAME = "quartz.hc.scheduler_name";
+
+    private Scheduler scheduler;
+
+    public QuartzCronScheduler(HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) {
+        try {
+            DirectSchedulerFactory schedulerFactory = DirectSchedulerFactory.getInstance();
+            ThreadPool threadPool = new QuartzThreadPool(healthCheckExecutorThreadPool);
+            schedulerFactory.createScheduler(HC_SCHEDULER_NAME, "id_" + System.currentTimeMillis(), threadPool, new RAMJobStore());
+            scheduler = schedulerFactory.getScheduler(HC_SCHEDULER_NAME);
+            scheduler.start();
+            LOG.debug("Started quartz scheduler {}", scheduler);
+        } catch (SchedulerException e) {
+            throw new IllegalStateException("Could not initialise/start quartz scheduler " + HC_SCHEDULER_NAME, e);
+        }
+    }
+
+    public void shutdown() {
+        if (scheduler != null) {
+            try {
+                scheduler.shutdown(false);
+                LOG.debug("Shutdown of quartz scheduler finished: {}", scheduler);
+            } catch (SchedulerException e) {
+                throw new IllegalStateException("Could not shutdown quartz scheduler " + HC_SCHEDULER_NAME, e);
+            }
+        }
+    }
+
+    public Scheduler getScheduler() {
+        return scheduler;
+    }
+
+    public class QuartzThreadPool implements ThreadPool {
+
+        private final HealthCheckExecutorThreadPool healthCheckExecutorThreadPool;
+
+        public QuartzThreadPool(HealthCheckExecutorThreadPool healthCheckExecutorThreadPool) {
+            this.healthCheckExecutorThreadPool = healthCheckExecutorThreadPool;
+        }
+
+        /** @see org.quartz.spi.QuartzThreadPool#getPoolSize() */
+        @Override
+        public int getPoolSize() {
+            return healthCheckExecutorThreadPool.getPoolSize();
+        }
+
+        /** @see org.quartz.spi.QuartzThreadPool#initialize() */
+        @Override
+        public void initialize() {
+            // nothing to do
+        }
+
+        /** @see org.quartz.spi.ThreadPool#setInstanceId(java.lang.String) */
+        @Override
+        public void setInstanceId(final String id) {
+            // we ignore this
+        }
+
+        /** @see org.quartz.spi.ThreadPool#setInstanceName(java.lang.String) */
+        @Override
+        public void setInstanceName(final String name) {
+            // we ignore this
+        }
+
+        /** @see org.quartz.spi.QuartzThreadPool#runInThread(java.lang.Runnable) */
+        @Override
+        public boolean runInThread(final Runnable job) {
+            healthCheckExecutorThreadPool.execute(job);
+            return true;
+        }
+
+        /** @see org.quartz.spi.ThreadPool#blockForAvailableThreads() */
+        @Override
+        public int blockForAvailableThreads() {
+            return healthCheckExecutorThreadPool.getMaxCurrentlyAvailableThreads();
+        }
+
+        /** @see org.quartz.spi.QuartzThreadPool#shutdown(boolean) */
+        @Override
+        public void shutdown(final boolean waitForJobsToComplete) {
+            // this executor is bound to the SCR lifecycle of HealthCheckExecutorThreadPool
+        }
+    }
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServlet.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,405 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.Result.Status;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Servlet that triggers the health check executor to return results via http.
+ *
+ * Parameters:
+ * <ul>
+ * <li>tags: The health check tags to take into account
+ * <li>format: html|json|jsonp
+ * <li>includeDebug: If true, debug messages from result log are included.
+ * <li>callback: For jsonp, the JS callback function name (defaults to "processHealthCheckResults")
+ * <li>httpStatus: health check status to http status mapping in format httpStatus=WARN:418,CRITICAL:503,HEALTH_CHECK_ERROR:500.
+ * </ul>
+ *
+ * For omitted health check status values the next best code will be used (e.g. for httpStatus=CRITICAL:503 a result WARN will return 200,
+ * CRITICAL 503 and HEALTH_CHECK_ERROR also 503). By default all requests answer with an http status of 200.
+ * <p>
+ * Useful in combination with load balancers. */
+@Component(configurationPolicy = ConfigurationPolicy.REQUIRE)
+@Designate(ocd = HealthCheckExecutorServletConfiguration.class)
+public class HealthCheckExecutorServlet extends HttpServlet {
+    private static final long serialVersionUID = 8013511523994541848L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(HealthCheckExecutorServlet.class);
+    public static final String PARAM_SPLIT_REGEX = "[,;]+";
+
+    static class Param {
+        final String name;
+        final String description;
+
+        Param(String n, String d) {
+            name = n;
+            description = d;
+        }
+    }
+
+    static final Param PARAM_TAGS = new Param("tags",
+            "Comma-separated list of health checks tags to select - can also be specified via path, e.g. /system/health/tag1,tag2.json. Exclusions can be done by prepending '-' to the tag name");
+    static final Param PARAM_FORMAT = new Param("format", "Output format, html|json|jsonp|txt - an extension in the URL overrides this");
+    static final Param PARAM_HTTP_STATUS = new Param("httpStatus", "Specify HTTP result code, for example"
+            + " CRITICAL:503 (status 503 if result >= CRITICAL)"
+            + " or CRITICAL:503,HEALTH_CHECK_ERROR:500,OK:418 for more specific HTTP status");
+
+    static final Param PARAM_COMBINE_TAGS_WITH_OR = new Param("combineTagsWithOr",
+            "Combine tags with OR, active by default. Set to false to combine with AND");
+    static final Param PARAM_FORCE_INSTANT_EXECUTION = new Param("forceInstantExecution",
+            "If true, forces instant execution by executing async health checks directly, circumventing the cache (2sec by default) of the HealthCheckExecutor");
+    static final Param PARAM_OVERRIDE_GLOBAL_TIMEOUT = new Param("timeout",
+            "(msec) a timeout status is returned for any health check still running after this period. Overrides the default HealthCheckExecutor timeout");
+
+    static final Param PARAM_INCLUDE_DEBUG = new Param("hcDebug", "Include the DEBUG output of the Health Checks");
+
+    static final Param PARAM_NAMES = new Param("names",
+            "Comma-separated list of health check names to select. Exclusions can be done by prepending '-' to the health check name");
+
+    static final String JSONP_CALLBACK_DEFAULT = "processHealthCheckResults";
+    static final Param PARAM_JSONP_CALLBACK = new Param("callback",
+            "name of the JSONP callback function to use, defaults to " + JSONP_CALLBACK_DEFAULT);
+
+    static final Param[] PARAM_LIST = { PARAM_TAGS, PARAM_NAMES, PARAM_FORMAT, PARAM_HTTP_STATUS, PARAM_COMBINE_TAGS_WITH_OR,
+            PARAM_FORCE_INSTANT_EXECUTION, PARAM_OVERRIDE_GLOBAL_TIMEOUT, PARAM_INCLUDE_DEBUG, PARAM_JSONP_CALLBACK };
+
+    static final String FORMAT_HTML = "html";
+    static final String FORMAT_JSON = "json";
+    static final String FORMAT_JSONP = "jsonp";
+    static final String FORMAT_TXT = "txt";
+    static final String FORMAT_VERBOSE_TXT = "verbose.txt";
+
+    private static final String CONTENT_TYPE_HTML = "text/html";
+    private static final String CONTENT_TYPE_TXT = "text/plain";
+    private static final String CONTENT_TYPE_JSON = "application/json";
+    private static final String CONTENT_TYPE_JSONP = "application/javascript";
+    private static final String STATUS_HEADER_NAME = "X-Health";
+
+    private static final String CACHE_CONTROL_KEY = "Cache-control";
+    private static final String CACHE_CONTROL_VALUE = "no-cache";
+
+    private String[] servletPaths;
+
+    private boolean disabled;
+
+    private String servletPath;
+
+    private String corsAccessControlAllowOrigin;
+
+    private static final String CORS_ORIGIN_HEADER_NAME = "Access-Control-Allow-Origin";
+
+    @Reference
+    private HttpService httpService;
+
+    @Reference
+    HealthCheckExecutor healthCheckExecutor;
+
+    @Reference
+    ResultHtmlSerializer htmlSerializer;
+
+    @Reference
+    ResultJsonSerializer jsonSerializer;
+
+    @Reference
+    ResultTxtSerializer txtSerializer;
+
+    @Reference
+    ResultTxtVerboseSerializer verboseTxtSerializer;
+
+    @Activate
+    protected final void activate(final HealthCheckExecutorServletConfiguration configuration) {
+        this.servletPath = configuration.servletPath();
+        this.disabled = configuration.disabled();
+        this.corsAccessControlAllowOrigin = configuration.cors_accessControlAllowOrigin();
+
+        LOG.info("servletPath={}", servletPath);
+        LOG.info("disabled={}", disabled);
+        LOG.info("corsAccessControlAllowOrigin={}", corsAccessControlAllowOrigin);
+
+        Map<String, HttpServlet> servletsToRegister = new LinkedHashMap<String, HttpServlet>();
+        servletsToRegister.put(this.servletPath, this);
+        servletsToRegister.put(this.servletPath + "." + FORMAT_HTML, new ProxyServlet(FORMAT_HTML));
+        servletsToRegister.put(this.servletPath + "." + FORMAT_JSON, new ProxyServlet(FORMAT_JSON));
+        servletsToRegister.put(this.servletPath + "." + FORMAT_JSONP, new ProxyServlet(FORMAT_JSONP));
+        servletsToRegister.put(this.servletPath + "." + FORMAT_TXT, new ProxyServlet(FORMAT_TXT));
+        servletsToRegister.put(this.servletPath + "." + FORMAT_VERBOSE_TXT, new ProxyServlet(FORMAT_VERBOSE_TXT));
+
+        if (disabled) {
+            LOG.info("Health Check Servlet is disabled by configuration");
+            return;
+        }
+
+        for (final Map.Entry<String, HttpServlet> servlet : servletsToRegister.entrySet()) {
+            try {
+                LOG.info("Registering HC servlet {} to path {}", getClass().getSimpleName(), servlet.getKey());
+                this.httpService.registerServlet(servlet.getKey(), servlet.getValue(), null, null);
+            } catch (Exception e) {
+                LOG.error("Could not register health check servlet: " + e, e);
+            }
+        }
+        this.servletPaths = servletsToRegister.keySet().toArray(new String[0]);
+
+    }
+
+    @Deactivate
+    public void deactivate(final ComponentContext componentContext) {
+        if (disabled || this.servletPaths == null) {
+            return;
+        }
+
+        for (final String servletPath : this.servletPaths) {
+            try {
+                LOG.debug("Unregistering path {}", servletPath);
+                this.httpService.unregister(servletPath);
+            } catch (Exception e) {
+                LOG.error("Could not unregister health check servlet: " + e, e);
+            }
+        }
+        this.servletPaths = null;
+    }
+
+    protected void doGet(final HttpServletRequest request, final HttpServletResponse response, final String format)
+            throws ServletException, IOException {
+        HealthCheckSelector selector = HealthCheckSelector.empty();
+        String pathInfo = request.getPathInfo();
+        String pathTokensStr = StringUtils.removeStart(splitFormat(pathInfo)[0], "/");
+
+        List<String> tags = new ArrayList<String>();
+        List<String> names = new ArrayList<String>();
+
+        if (StringUtils.isNotBlank(pathTokensStr)) {
+            String[] pathTokens = pathTokensStr.split(PARAM_SPLIT_REGEX);
+            for (String pathToken : pathTokens) {
+                if (pathToken.indexOf(' ') >= 0) {
+                    // token contains space. assume it is a name
+                    names.add(pathToken);
+                } else {
+                    tags.add(pathToken);
+                }
+            }
+        }
+        if (tags.size() == 0) {
+            // if not provided via path use parameter or default
+            tags = Arrays.asList(StringUtils.defaultIfEmpty(request.getParameter(PARAM_TAGS.name), "").split(PARAM_SPLIT_REGEX));
+        }
+        selector.withTags(tags.toArray(new String[0]));
+
+        if (names.size() == 0) {
+            // if not provided via path use parameter or default
+            names = Arrays.asList(StringUtils.defaultIfEmpty(request.getParameter(PARAM_NAMES.name), "").split(PARAM_SPLIT_REGEX));
+        }
+        selector.withNames(names.toArray(new String[0]));
+
+        final Boolean includeDebug = Boolean.valueOf(request.getParameter(PARAM_INCLUDE_DEBUG.name));
+        final Map<Result.Status, Integer> statusMapping = request.getParameter(PARAM_HTTP_STATUS.name) != null ? getStatusMapping(request
+                .getParameter(PARAM_HTTP_STATUS.name)) : null;
+
+        HealthCheckExecutionOptions executionOptions = new HealthCheckExecutionOptions();
+        executionOptions.setCombineTagsWithOr(
+                Boolean.valueOf(StringUtils.defaultString(request.getParameter(PARAM_COMBINE_TAGS_WITH_OR.name), "true")));
+        executionOptions.setForceInstantExecution(Boolean.valueOf(request.getParameter(PARAM_FORCE_INSTANT_EXECUTION.name)));
+        String overrideGlobalTimeoutVal = request.getParameter(PARAM_OVERRIDE_GLOBAL_TIMEOUT.name);
+        if (StringUtils.isNumeric(overrideGlobalTimeoutVal)) {
+            executionOptions.setOverrideGlobalTimeout(Integer.valueOf(overrideGlobalTimeoutVal));
+        }
+
+        List<HealthCheckExecutionResult> executionResults = this.healthCheckExecutor.execute(selector, executionOptions);
+
+        Result.Status mostSevereStatus = Result.Status.OK;
+        for (HealthCheckExecutionResult executionResult : executionResults) {
+            Status status = executionResult.getHealthCheckResult().getStatus();
+            if (status.ordinal() > mostSevereStatus.ordinal()) {
+                mostSevereStatus = status;
+            }
+        }
+        Result overallResult = new Result(mostSevereStatus, "Overall status " + mostSevereStatus);
+
+        sendNoCacheHeaders(response);
+        sendCorsHeaders(response);
+
+        if (statusMapping != null) {
+            Integer httpStatus = statusMapping.get(overallResult.getStatus());
+            response.setStatus(httpStatus);
+        }
+
+        if (FORMAT_HTML.equals(format)) {
+            sendHtmlResponse(overallResult, executionResults, request, response, includeDebug);
+        } else if (FORMAT_JSON.equals(format)) {
+            sendJsonResponse(overallResult, executionResults, null, response, includeDebug);
+        } else if (FORMAT_JSONP.equals(format)) {
+            String jsonpCallback = StringUtils.defaultIfEmpty(request.getParameter(PARAM_JSONP_CALLBACK.name), JSONP_CALLBACK_DEFAULT);
+            sendJsonResponse(overallResult, executionResults, jsonpCallback, response, includeDebug);
+        } else if (StringUtils.endsWith(format, FORMAT_TXT)) {
+            sendTxtResponse(overallResult, response, StringUtils.equals(format, FORMAT_VERBOSE_TXT), executionResults, includeDebug);
+        } else {
+            response.setContentType("text/plain");
+            response.getWriter().println("Invalid format " + format + " - supported formats: html|json|jsonp|txt|verbose.txt");
+        }
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
+        String pathInfo = request.getPathInfo();
+        String format = splitFormat(pathInfo)[1];
+        if (StringUtils.isBlank(format)) {
+            // if not provided via extension use parameter or default
+            format = StringUtils.defaultIfEmpty(request.getParameter(PARAM_FORMAT.name), FORMAT_HTML);
+        }
+        doGet(request, response, format);
+    }
+
+    private String[] splitFormat(String pathInfo) {
+        for (String format : new String[] { FORMAT_HTML, FORMAT_JSON, FORMAT_JSONP, FORMAT_VERBOSE_TXT, FORMAT_TXT }) {
+            String formatWithDot = "." + format;
+            if (StringUtils.endsWith(pathInfo, formatWithDot)) {
+                return new String[] { StringUtils.substringBeforeLast(pathInfo, formatWithDot), format };
+            }
+        }
+        return new String[] { pathInfo, null };
+    }
+
+    private void sendTxtResponse(final Result overallResult, final HttpServletResponse response, boolean verbose,
+            List<HealthCheckExecutionResult> executionResults, boolean includeDebug) throws IOException {
+        response.setContentType(CONTENT_TYPE_TXT);
+        response.setCharacterEncoding("UTF-8");
+        if (verbose) {
+            response.getWriter().write(verboseTxtSerializer.serialize(overallResult, executionResults, includeDebug));
+        } else {
+            response.getWriter().write(txtSerializer.serialize(overallResult));
+        }
+    }
+
+    private void sendJsonResponse(final Result overallResult, final List<HealthCheckExecutionResult> executionResults,
+            final String jsonpCallback,
+            final HttpServletResponse response, boolean includeDebug)
+            throws IOException {
+        if (StringUtils.isNotBlank(jsonpCallback)) {
+            response.setContentType(CONTENT_TYPE_JSONP);
+        } else {
+            response.setContentType(CONTENT_TYPE_JSON);
+        }
+        response.setCharacterEncoding("UTF-8");
+
+        String resultJson = this.jsonSerializer.serialize(overallResult, executionResults, jsonpCallback, includeDebug);
+        PrintWriter writer = response.getWriter();
+        writer.append(resultJson);
+    }
+
+    private void sendHtmlResponse(final Result overallResult, final List<HealthCheckExecutionResult> executionResults,
+            final HttpServletRequest request, final HttpServletResponse response, boolean includeDebug)
+            throws IOException {
+        response.setContentType(CONTENT_TYPE_HTML);
+        response.setCharacterEncoding("UTF-8");
+        response.setHeader(STATUS_HEADER_NAME, overallResult.toString());
+        response.getWriter().append(this.htmlSerializer.serialize(overallResult, executionResults, getHtmlHelpText(), includeDebug));
+    }
+
+    private void sendNoCacheHeaders(final HttpServletResponse response) {
+        response.setHeader(CACHE_CONTROL_KEY, CACHE_CONTROL_VALUE);
+    }
+
+    private void sendCorsHeaders(final HttpServletResponse response) {
+        if (StringUtils.isNotBlank(corsAccessControlAllowOrigin)) {
+            response.setHeader(CORS_ORIGIN_HEADER_NAME, corsAccessControlAllowOrigin);
+        }
+    }
+
+    private String getHtmlHelpText() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("<h3>Supported URL parameters</h3>\n");
+        for (Param p : PARAM_LIST) {
+            sb.append("<b>").append(p.name).append("</b>:");
+            sb.append(StringEscapeUtils.escapeHtml4(p.description));
+            sb.append("<br/>");
+        }
+        return sb.toString();
+    }
+
+    Map<Result.Status, Integer> getStatusMapping(String mappingStr) throws ServletException {
+        Map<Result.Status, Integer> statusMapping = new HashMap<Result.Status, Integer>();
+        try {
+            String[] bits = mappingStr.split("[,]");
+            for (String bit : bits) {
+                String[] tuple = bit.split("[:]");
+                statusMapping.put(Result.Status.valueOf(tuple[0]), Integer.parseInt(tuple[1]));
+            }
+        } catch (Exception e) {
+            throw new ServletException("Invalid parameter httpStatus=" + mappingStr + " " + e, e);
+        }
+
+        if (!statusMapping.containsKey(Result.Status.OK)) {
+            statusMapping.put(Result.Status.OK, 200);
+        }
+        if (!statusMapping.containsKey(Result.Status.WARN)) {
+            statusMapping.put(Result.Status.WARN, statusMapping.get(Result.Status.OK));
+        }
+        if (!statusMapping.containsKey(Result.Status.CRITICAL)) {
+            statusMapping.put(Result.Status.CRITICAL, statusMapping.get(Result.Status.WARN));
+        }
+        if (!statusMapping.containsKey(Result.Status.HEALTH_CHECK_ERROR)) {
+            statusMapping.put(Result.Status.HEALTH_CHECK_ERROR, statusMapping.get(Result.Status.CRITICAL));
+        }
+        return statusMapping;
+    }
+
+    private class ProxyServlet extends HttpServlet {
+
+        private final String format;
+
+        private ProxyServlet(final String format) {
+            this.format = format;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+            HealthCheckExecutorServlet.this.doGet(req, resp, format);
+        }
+    }
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/HealthCheckExecutorServletConfiguration.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(name = "Apache Felix Health Check Executor Servlet", description = "Serializes health check results into html, json or txt format")
+@interface HealthCheckExecutorServletConfiguration {
+
+    String SERVLET_PATH_DEFAULT = "/system/health";
+
+    @AttributeDefinition(name = "Disabled", description = "Allows to disable the servlet if required for security reasons")
+    boolean disabled() default false;
+
+    @AttributeDefinition(name = "Path", description = "Servlet path (defaults to " + SERVLET_PATH_DEFAULT
+            + " in order to not be accessible via Apache/Internet)")
+    String servletPath() default SERVLET_PATH_DEFAULT;
+
+    @AttributeDefinition(name = "CORS Access-Control-Allow-Origin", description = "Sets the Access-Control-Allow-Origin CORS header. If blank no header is sent.")
+    String cors_accessControlAllowOrigin() default "*";
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializer.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog.Entry;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.util.FormattingResultLog;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+/** Serializes health check results into html format. */
+@Component(service = ResultHtmlSerializer.class)
+public class ResultHtmlSerializer {
+
+    private String styleString;
+
+    @Activate
+    protected final void activate(final ResultHtmlSerializerConfiguration configuration) {
+        this.styleString = configuration.styleString();
+    }
+
+    public String serialize(final Result overallResult, final List<HealthCheckExecutionResult> executionResults, String escapedHelpText,
+            boolean includeDebug) {
+
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter writer = new PrintWriter(stringWriter);
+
+        writer.println("<html><head><title>System Health</title>" +
+                "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /><style>" + styleString +
+                "</style></head><body><h1>System Health</h1>");
+
+        writer.println("<p><span class=\"" + getClassForStatus(overallResult.getStatus()) + "\"><strong>Overall Result: "
+                + overallResult.getStatus() + "</strong></span></p>");
+
+        final DateFormat dfShort = new SimpleDateFormat("HH:mm:ss.SSS");
+        final DateFormat dfLong = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+        writer.println("<table id=\"healthCheckResults\" cellspacing=\"0\">");
+        writer.println(
+                "<thead><tr><th>Health Check <span style='color:gray'>(tags)</span></th><th>Status</th><th>Log</th><th>Finished At</th><th>Time</th></tr></thead>");
+        for (HealthCheckExecutionResult executionResult : executionResults) {
+            Result result = executionResult.getHealthCheckResult();
+            List<String> tags = executionResult.getHealthCheckMetadata().getTags();
+            boolean hasTags = tags != null && tags.size() > 0 && StringUtils.isNotBlank(tags.get(0));
+            writer.print("<tr class=\"" + getClassForStatus(result.getStatus()) + "\">");
+            writer.print("<td><p title=\"" + StringEscapeUtils.escapeHtml4(executionResult.getHealthCheckMetadata().getName()) + "\">"
+                    + StringEscapeUtils.escapeHtml4(executionResult.getHealthCheckMetadata().getTitle()) + "");
+            if (hasTags) {
+                writer.println("<br/><span style='color:gray'>" + StringEscapeUtils.escapeHtml4(StringUtils.join(tags, ", ")) + "</span>");
+            }
+            writer.println("</p></td>");
+            writer.println("<td style='font-weight:bold;'>" + StringEscapeUtils.escapeHtml4(result.getStatus().toString()) + "</td>");
+            writer.println("<td>");
+            boolean isFirst = true;
+
+            boolean isSingleResult = isSingleResult(result);
+
+            for (Entry entry : result) {
+                if (!includeDebug && entry.isDebug()) {
+                    continue;
+                }
+
+                if (isFirst) {
+                    isFirst = false;
+                } else {
+                    writer.println("<br/>\n");
+                }
+
+                boolean showStatus = !isSingleResult && !entry.isDebug() && entry.getStatus() != Result.Status.OK;
+
+                String message = StringEscapeUtils.escapeHtml4(entry.getMessage());
+                if (entry.isDebug()) {
+                    message = "<span style='color:gray'/>" + message + "</span>";
+                }
+                writer.println((showStatus ? StringEscapeUtils.escapeHtml4(entry.getStatus().toString()) + " " : "") + message);
+
+                Exception exception = entry.getException();
+                if (exception != null) {
+                    writer.println("<span style='width:20px'/>" + StringEscapeUtils.escapeHtml4(exception.toString()));
+                    writer.println("<!--");
+                    exception.printStackTrace(writer);
+                    writer.println("-->");
+                }
+            }
+            writer.println("</td>");
+            Date finishedAt = executionResult.getFinishedAt();
+            writer.println("<td>" + (isToday(finishedAt) ? dfShort.format(finishedAt) : dfLong.format(finishedAt)) + "</td>");
+            writer.println("<td>" + FormattingResultLog.msHumanReadable(executionResult.getElapsedTimeInMs()) + "</td>");
+
+            writer.println("</tr>");
+        }
+        writer.println("</table>");
+
+        writer.println("<div class='helpText'>");
+        writer.println(escapedHelpText);
+        writer.println("</div>");
+        writer.println("</body></html>");
+
+        return stringWriter.toString();
+
+    }
+
+    private String getClassForStatus(final Result.Status status) {
+        return "status" + status.name();
+    }
+
+    private boolean isSingleResult(final Result result) {
+        int count = 0;
+        for (Entry entry : result) {
+            count++;
+            if (count > 1) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isToday(Date date) {
+        Calendar cal1 = Calendar.getInstance();
+        Calendar cal2 = Calendar.getInstance();
+        cal2.setTime(date);
+        boolean isToday = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+                cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
+        return isToday;
+
+    }
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultHtmlSerializerConfiguration.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@ObjectClassDefinition(name = "Apache Felix Health Check Result HTML Serializer", description = "Serializer for health check results in HTML format")
+@interface ResultHtmlSerializerConfiguration {
+
+    String CSS_STYLE_DEFAULT = "body { font-size:12px; font-family:arial,verdana,sans-serif;background-color:#FFFDF1; }\n"
+            + "h1 { font-size:20px;}\n"
+            + "table { font-size:12px; border:#ccc 1px solid; border-radius:3px; }\n"
+            + "table th { padding:5px; text-align: left; background: #ededed; }\n"
+            + "table td { padding:5px; border-top: 1px solid #ffffff; border-bottom:1px solid #e0e0e0; border-left: 1px solid #e0e0e0; }\n"
+            + ".statusOK { background-color:#CCFFCC;}\n"
+            + ".statusWARN { background-color:#FFE569;}\n"
+            + ".statusCRITICAL { background-color:#F0975A;}\n"
+            + ".statusHEALTH_CHECK_ERROR { background-color:#F16D4E;}\n"
+            + ".helpText { color:grey; font-size:80%; }\n";
+
+    @AttributeDefinition(name = "CSS Style", description = "CSS Style - can be configured to change the look and feel of the html result page.")
+    String styleString() default CSS_STYLE_DEFAULT;
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultJsonSerializer.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.utils.json.JSONWriter;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Serializes health check results into json format. */
+@Component(service = ResultJsonSerializer.class)
+public class ResultJsonSerializer {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ResultJsonSerializer.class);
+
+    static final String OVERALL_RESULT_KEY = "OverallResult";
+
+    public String serialize(final Result overallResult, final List<HealthCheckExecutionResult> executionResults, final String jsonpCallback,
+            boolean includeDebug) {
+
+        LOG.debug("Sending json response... ");
+
+        StringWriter writer = new StringWriter();
+        try {
+            JSONWriter jsonWriter = new JSONWriter(writer);
+            jsonWriter.object();
+            jsonWriter.key("overallResult");
+            jsonWriter.value(overallResult.getStatus().toString());
+            jsonWriter.key("results");
+            jsonWriter.array();
+            for (HealthCheckExecutionResult healthCheckResult : executionResults) {
+                writeResult(healthCheckResult, includeDebug, jsonWriter);
+            }
+            jsonWriter.endArray();
+            jsonWriter.endObject();            
+        } catch(IOException e) {
+            LOG.error("Could not serialise health check result: e="+e, e);
+            writer.write("{error:'"+e.getMessage()+"'}");
+        }
+        String resultStr = writer.toString();
+
+        if (StringUtils.isNotBlank(jsonpCallback)) {
+            resultStr = jsonpCallback + "(" + resultStr + ");";
+        }
+
+        return resultStr;
+
+    }
+
+    private void writeResult(final HealthCheckExecutionResult healthCheckResult, boolean includeDebug, JSONWriter jsonWriter) throws IOException {
+
+        jsonWriter.object()
+            .key("name").value(healthCheckResult.getHealthCheckMetadata().getName())
+            .key("status").value(healthCheckResult.getHealthCheckResult().getStatus().toString()) 
+            .key("timeInMs").value(healthCheckResult.getElapsedTimeInMs())
+            .key("finishedAt").value(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").format(healthCheckResult.getFinishedAt())) ;
+        
+        jsonWriter.key("tags").array();
+        for(String tag: healthCheckResult.getHealthCheckMetadata().getTags()) {
+            jsonWriter.value(tag);
+        }
+        jsonWriter.endArray();
+        
+        jsonWriter.key("messages").array();
+        for (ResultLog.Entry entry : healthCheckResult.getHealthCheckResult()) {
+            if (!includeDebug && entry.isDebug()) {
+                continue;
+            }
+            jsonWriter.object()
+                .key("status").value(entry.getStatus().toString())
+                .key("message").value(entry.getMessage());
+            
+            Exception exception = entry.getException();
+            if (exception != null) {
+                StringWriter stringWriter = new StringWriter();
+                exception.printStackTrace(new PrintWriter(stringWriter));
+                jsonWriter.key("exception").value(stringWriter.toString());
+            }
+            jsonWriter.endObject();
+        }
+        jsonWriter.endArray();
+        
+        
+        jsonWriter.endObject();
+    }
+
+}

Added: felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java
URL: http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java?rev=1849246&view=auto
==============================================================================
--- felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java (added)
+++ felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/servlet/ResultTxtSerializer.java Tue Dec 18 22:58:15 2018
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you 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 org.apache.felix.hc.core.impl.servlet;
+
+import org.apache.felix.hc.api.Result;
+import org.osgi.service.component.annotations.Component;
+
+/** Serializes health check results into a simple text message (ideal to be used by a load balancer that would discard further
+ * information). */
+@Component(service = ResultTxtSerializer.class)
+public class ResultTxtSerializer {
+    public String serialize(final Result overallResult) {
+        return overallResult.getStatus().toString();
+    }
+}