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(); + } +}
