You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2015/08/12 15:02:46 UTC

svn commit: r1695501 - in /sling/trunk/bundles/extensions/healthcheck/core: ./ src/main/java/org/apache/sling/hc/api/execution/ src/main/java/org/apache/sling/hc/core/impl/servlet/ src/test/java/org/apache/sling/hc/core/impl/servlet/

Author: bdelacretaz
Date: Wed Aug 12 13:02:45 2015
New Revision: 1695501

URL: http://svn.apache.org/r1695501
Log:
SLING-4862 - Health Checks Servlet, contributed by Georg Henzler, thanks!

Added:
    sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/
    sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServlet.java
    sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultHtmlSerializer.java
    sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultJsonSerializer.java
    sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/
    sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServletTest.java
Modified:
    sling/trunk/bundles/extensions/healthcheck/core/pom.xml
    sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/api/execution/HealthCheckExecutionOptions.java

Modified: sling/trunk/bundles/extensions/healthcheck/core/pom.xml
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/pom.xml?rev=1695501&r1=1695500&r2=1695501&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/pom.xml (original)
+++ sling/trunk/bundles/extensions/healthcheck/core/pom.xml Wed Aug 12 13:02:45 2015
@@ -134,6 +134,20 @@
 			<version>2.5</version>
 			<scope>provided</scope>
 		</dependency>        
+		
+		<dependency>
+			<groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.http.servlet-api</artifactId>
+			<version>1.1.0</version>
+			<scope>provided</scope>
+		</dependency>	
+		
+		<dependency>
+			<groupId>org.apache.sling</groupId>
+			<artifactId>org.apache.sling.commons.json</artifactId>
+			<version>2.0.6</version>
+			<scope>provided</scope>
+		</dependency>			
 		        
      </dependencies>
 </project>

Modified: sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/api/execution/HealthCheckExecutionOptions.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/api/execution/HealthCheckExecutionOptions.java?rev=1695501&r1=1695500&r2=1695501&view=diff
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/api/execution/HealthCheckExecutionOptions.java (original)
+++ sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/api/execution/HealthCheckExecutionOptions.java Wed Aug 12 13:02:45 2015
@@ -79,5 +79,32 @@ public class HealthCheckExecutionOptions
         return overrideGlobalTimeout;
     }
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (combineTagsWithOr ? 1231 : 1237);
+        result = prime * result + (forceInstantExecution ? 1231 : 1237);
+        result = prime * result + overrideGlobalTimeout;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        HealthCheckExecutionOptions other = (HealthCheckExecutionOptions) obj;
+        if (combineTagsWithOr != other.combineTagsWithOr)
+            return false;
+        if (forceInstantExecution != other.forceInstantExecution)
+            return false;
+        if (overrideGlobalTimeout != other.overrideGlobalTimeout)
+            return false;
+        return true;
+    }
 
 }
\ No newline at end of file

Added: sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServlet.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServlet.java?rev=1695501&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServlet.java (added)
+++ sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServlet.java Wed Aug 12 13:02:45 2015
@@ -0,0 +1,254 @@
+/*
+ * 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.sling.hc.core.impl.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Dictionary;
+import java.util.HashMap;
+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.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.hc.api.Result;
+import org.apache.sling.hc.api.Result.Status;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.sling.hc.api.execution.HealthCheckExecutor;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.http.HttpService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Servlet that triggers the health check executor to return results via http. <br/>
+ * <br/>
+ * Parameters:<br/>
+ * <br/>
+ * tags: The health check tags to take into account<br/>
+ * format: html|json|jsonp<br/>
+ * includeDebug: If true, debug messages from result log are included.<br/>
+ * callback: For jsonp, the JS callback function name (defaults to "processHealthCheckResults")<br/>
+ * httpStatus: health check status to http status mapping in format httpStatus=WARN:418,CRITICAL:503,HEALTH_CHECK_ERROR:500. 
+ * 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. 
+ * Useful in combination with load balancers.<br/>
+ * 
+ * NOTE: This servlet registers directly (low-level) at the HttpService and is not processed by sling (better performance, fewer dependencies, no authentication required, 503 can be sent without the progress tracker information). */
+@Service
+@Component(label = "Health Check Executor Servlet",
+        description = "Serializes health check results into html or json format",
+        policy = ConfigurationPolicy.REQUIRE, metatype = true, immediate = true)
+public class HealthCheckExecutorServlet extends HttpServlet {
+    private static final long serialVersionUID = 8013511523994541848L;
+
+    private static final Logger LOG = LoggerFactory.getLogger(HealthCheckExecutorServlet.class);
+
+    static final String PARAM_TAGS = "tags";
+    static final String PARAM_FORMAT = "format";
+    static final String PARAM_HTTP_STATUS = "httpStatus";
+
+    static final String PARAM_COMBINE_TAGS_WITH_OR = "combineTagsWithOr";
+    static final String PARAM_FORCE_INSTANT_EXECUTION = "forceInstantExecution";
+    static final String PARAM_OVERRIDE_GLOBAL_TIMEOUT = "timeout";
+
+    static final String PARAM_INCLUDE_DEBUG = "includeDebug";
+
+    static final String FORMAT_HTML = "html";
+    static final String FORMAT_JSON = "json";
+    static final String FORMAT_JSONP = "jsonp";
+
+    static final String PARAM_JSONP_CALLBACK = "callback";
+    static final String JSONP_CALLBACK_DEFAULT = "processHealthCheckResults";
+
+    private static final String CONTENT_TYPE_HTML = "text/html";
+    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 static final String SERVLET_PATH_DEFAULT = "/system/health";
+    public static final String PROPERTY_SERVLET_PATH = "servletPath";
+    @Property(name = PROPERTY_SERVLET_PATH, label = "Path",
+            description = "Servlet path (defaults to " + SERVLET_PATH_DEFAULT + " in order to not be accessible via Apache/Internet)", value = SERVLET_PATH_DEFAULT)
+    private String servletPath;
+
+    @Reference
+    private HttpService httpService;
+
+    @Reference
+    HealthCheckExecutor healthCheckExecutor;
+
+    @Reference
+    ResultHtmlSerializer htmlSerializer;
+
+    @Reference
+    ResultJsonSerializer jsonSerializer;
+
+    @Activate
+    protected final void activate(final ComponentContext context) {
+        try {
+            final Dictionary<?, ?> properties = context.getProperties();
+            this.servletPath = (String) properties.get(PROPERTY_SERVLET_PATH);
+            LOG.debug("Registering {} to path {}", getClass().getSimpleName(), this.servletPath);
+            this.httpService.registerServlet(this.servletPath, this, null, null);
+        } catch (Exception e) {
+            LOG.error("Could not register health check servlet: "+e, e);
+        }
+    }
+
+    @Deactivate
+    public void deactivate(final ComponentContext componentContext) {
+        try {
+            LOG.debug("Unregistering path {}", this.servletPath);
+            this.httpService.unregister(this.servletPath);
+        } catch (Exception e) {
+            LOG.error("Could not unregister health check servlet: "+e, e);
+        }
+    }
+
+    @Override
+    protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
+
+        final String[] tags = StringUtils.defaultIfEmpty(request.getParameter(PARAM_TAGS), "").split("[, ;]+");
+
+        String format = StringUtils.substringAfterLast(request.getPathInfo(), ".");
+        if (StringUtils.isBlank(format)) {
+            // if not provided via extension use parameter or default
+            format = StringUtils.defaultIfEmpty(request.getParameter(PARAM_FORMAT), FORMAT_HTML);
+        }
+
+        final Boolean includeDebug = Boolean.valueOf(request.getParameter(PARAM_INCLUDE_DEBUG));
+        final Map<Result.Status, Integer> statusMapping = request.getParameter(PARAM_HTTP_STATUS) != null ? getStatusMapping(request
+                .getParameter(PARAM_HTTP_STATUS)) : null;
+
+        HealthCheckExecutionOptions options = new HealthCheckExecutionOptions();
+        options.setCombineTagsWithOr(Boolean.valueOf(StringUtils.defaultString(request.getParameter(PARAM_COMBINE_TAGS_WITH_OR), "true")));
+        options.setForceInstantExecution(Boolean.valueOf(request.getParameter(PARAM_FORCE_INSTANT_EXECUTION)));
+        String overrideGlobalTimeoutVal = request.getParameter(PARAM_OVERRIDE_GLOBAL_TIMEOUT);
+        if (StringUtils.isNumeric(overrideGlobalTimeoutVal)) {
+            options.setOverrideGlobalTimeout(Integer.valueOf(overrideGlobalTimeoutVal));
+        }
+
+        List<HealthCheckExecutionResult> executionResults = this.healthCheckExecutor.execute(options, tags);
+
+        Result.Status mostSevereStatus = Result.Status.DEBUG;
+        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);
+
+        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), JSONP_CALLBACK_DEFAULT);
+            sendJsonResponse(overallResult, executionResults, jsonpCallback, response, includeDebug);
+        } else {
+            response.setContentType("text/plain");
+            response.getWriter().println("Invalid format " + format + " - supported formats: html|json|jsonp");
+        }
+
+    }
+
+    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);
+        }
+
+        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 {
+
+        String resultHtml = this.htmlSerializer.serialize(overallResult, executionResults, includeDebug);
+
+        response.setContentType(CONTENT_TYPE_HTML);
+        response.setHeader(STATUS_HEADER_NAME, overallResult.toString());
+
+        PrintWriter writer = response.getWriter();
+        writer.append(resultHtml);
+
+    }
+
+    private void sendNoCacheHeaders(final HttpServletResponse response) {
+        response.setHeader(CACHE_CONTROL_KEY, CACHE_CONTROL_VALUE);
+    }
+
+    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;
+    }
+
+
+}

Added: sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultHtmlSerializer.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultHtmlSerializer.java?rev=1695501&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultHtmlSerializer.java (added)
+++ sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultHtmlSerializer.java Wed Aug 12 13:02:45 2015
@@ -0,0 +1,164 @@
+/*
+ * 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.sling.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.Dictionary;
+import java.util.List;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.hc.api.Result;
+import org.apache.sling.hc.api.ResultLog.Entry;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.sling.hc.util.FormattingResultLog;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Serializes health check results into html format. */
+@Service(ResultHtmlSerializer.class)
+@Component(metatype = true)
+public class ResultHtmlSerializer {
+    private static final Logger LOG = LoggerFactory.getLogger(ResultHtmlSerializer.class);
+
+    private static final 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";
+    public static final String PROPERTY_CSS_STYLE = "styleString";
+    @Property(name = PROPERTY_CSS_STYLE, label = "CSS Style",
+            description = "CSS Style - can be configured to change the look and feel of the html result page.", value = CSS_STYLE_DEFAULT)
+    private String styleString;
+
+    @Activate
+    protected final void activate(final ComponentContext context) {
+        final Dictionary<?, ?> properties = context.getProperties();
+        this.styleString = PropertiesUtil.toString(properties.get(PROPERTY_CSS_STYLE), CSS_STYLE_DEFAULT);
+    }
+
+    public String serialize(final Result overallResult, final List<HealthCheckExecutionResult> executionResults, 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</th><th>Status</th><th>Log</th><th colspan=\"2\">Execution Time</th></tr></thead>");
+        for (HealthCheckExecutionResult executionResult : executionResults) {
+            Result result = executionResult.getHealthCheckResult();
+            writer.println("<tr class=\"" + getClassForStatus(result.getStatus()) + "\" "
+                    + "title=\"Tags: " + StringEscapeUtils.escapeHtml(StringUtils.join(executionResult.getHealthCheckMetadata().getTags(), ",")) + "\">");
+            writer.println("<td><span title=\"" + StringEscapeUtils.escapeHtml(executionResult.getHealthCheckMetadata().getName()) + "\">"
+                    + StringEscapeUtils.escapeHtml(executionResult.getHealthCheckMetadata().getTitle()) + "</span></td>");
+            writer.println("<td style='font-weight:bold;'>" + result.getStatus() + "</td>");
+            writer.println("<td>");
+            boolean isFirst = true;
+
+            boolean isSingleResult = isSingleResult(result);
+
+            for (Entry entry : result) {
+            	if(!includeDebug && entry.getStatus()==Result.Status.DEBUG) {
+            		continue;
+            	}
+            	
+                if (isFirst) {
+                    isFirst = false;
+                } else {
+                    writer.println("<br/>\n");
+                }
+                
+                boolean showStatus = !isSingleResult && entry.getStatus()!=Result.Status.DEBUG && entry.getStatus() !=Result.Status.INFO;
+                
+                String message = StringEscapeUtils.escapeHtml(entry.getMessage());
+                if(entry.getStatus()==Result.Status.DEBUG) {
+                	message = "<span style='color:gray'/>"+message + "</span>";
+                }
+				writer.println((showStatus ? entry.getStatus() + " " : "") + message);
+
+                Exception exception = entry.getException();
+                if (exception != null) {
+                    writer.println("<span style='width:20px'/>" + StringEscapeUtils.escapeHtml(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("</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: sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultJsonSerializer.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultJsonSerializer.java?rev=1695501&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultJsonSerializer.java (added)
+++ sling/trunk/bundles/extensions/healthcheck/core/src/main/java/org/apache/sling/hc/core/impl/servlet/ResultJsonSerializer.java Wed Aug 12 13:02:45 2015
@@ -0,0 +1,106 @@
+/*
+ * 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.sling.hc.core.impl.servlet;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.json.JSONArray;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.JSONObject;
+import org.apache.sling.hc.api.Result;
+import org.apache.sling.hc.api.ResultLog;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Serializes health check results into json format. */
+@Service(ResultJsonSerializer.class)
+@Component(metatype = false)
+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... ");
+
+        JSONObject result = new JSONObject();
+        try {
+
+            result.put("overallResult", overallResult.getStatus());
+            JSONArray resultsJsonArr = new JSONArray();
+            result.put("results", resultsJsonArr);
+
+            for (HealthCheckExecutionResult healthCheckResult : executionResults) {
+                resultsJsonArr.put(getJsonForSimpleResult(healthCheckResult, includeDebug));
+            }
+
+        } catch (JSONException e) {
+            LOG.info("Could not serialize health check result: " + e, e);
+        }
+
+        String resultStr;
+        if (StringUtils.isNotBlank(jsonpCallback)) {
+            resultStr = jsonpCallback + "(" + result.toString() + ");";
+        } else {
+            resultStr = result.toString();
+        }
+
+        return resultStr;
+
+    }
+
+    private JSONObject getJsonForSimpleResult(final HealthCheckExecutionResult healthCheckResult, boolean includeDebug) throws JSONException {
+
+        JSONObject result = new JSONObject();
+
+        result.put("name", healthCheckResult.getHealthCheckMetadata().getName());
+        result.put("status", healthCheckResult.getHealthCheckResult().getStatus());
+        result.put("timeInMs", healthCheckResult.getElapsedTimeInMs());
+        result.put("finishedAt", healthCheckResult.getFinishedAt());
+
+        JSONArray messagesArr = new JSONArray();
+        result.put("messages", messagesArr);
+        for (ResultLog.Entry entry : healthCheckResult.getHealthCheckResult()) {
+            if (!includeDebug && entry.getStatus() == Result.Status.DEBUG) {
+                continue;
+            }
+            JSONObject jsonEntry = new JSONObject();
+            jsonEntry.put("status", entry.getStatus());
+            jsonEntry.put("message", entry.getMessage());
+            Exception exception = entry.getException();
+            if (exception != null) {
+                StringWriter stringWriter = new StringWriter();
+                exception.printStackTrace(new PrintWriter(stringWriter));
+                jsonEntry.put("exception", stringWriter.toString());
+            }
+            messagesArr.put(jsonEntry);
+        }
+
+        return result;
+    }
+
+}

Added: sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServletTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServletTest.java?rev=1695501&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServletTest.java (added)
+++ sling/trunk/bundles/extensions/healthcheck/core/src/test/java/org/apache/sling/hc/core/impl/servlet/HealthCheckExecutorServletTest.java Wed Aug 12 13:02:45 2015
@@ -0,0 +1,208 @@
+/*
+ * 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.sling.hc.core.impl.servlet;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.hc.api.Result;
+import org.apache.sling.hc.api.Result.Status;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.sling.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.sling.hc.api.execution.HealthCheckExecutor;
+import org.apache.sling.hc.core.impl.executor.ExecutionResult;
+import org.apache.sling.hc.util.HealthCheckMetadata;
+import org.hamcrest.Description;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class HealthCheckExecutorServletTest {
+
+    private HealthCheckExecutorServlet healthCheckExecutorServlet;
+
+    @Mock
+    private HttpServletRequest request;
+
+    @Mock
+    private HttpServletResponse response;
+
+    @Mock
+    private HealthCheckExecutor healthCheckExecutor;
+
+    @Mock
+    private ResultHtmlSerializer htmlSerializer;
+
+    @Mock
+    private ResultJsonSerializer jsonSerializer;
+
+    @Mock
+    private ServiceReference hcServiceRef;
+
+    @Mock
+    private PrintWriter writer;
+
+    @Before
+    public void setup() throws IOException {
+
+        healthCheckExecutorServlet = new HealthCheckExecutorServlet();
+
+        MockitoAnnotations.initMocks(this);
+        healthCheckExecutorServlet.healthCheckExecutor = healthCheckExecutor;
+        healthCheckExecutorServlet.htmlSerializer = htmlSerializer;
+        healthCheckExecutorServlet.jsonSerializer = jsonSerializer;
+
+        doReturn(500L).when(hcServiceRef).getProperty(Constants.SERVICE_ID);
+        doReturn(writer).when(response).getWriter();
+    }
+
+    @Test
+    public void testDoGetHtml() throws ServletException, IOException {
+
+        String testTag = "testTag";
+        doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS);
+        doReturn("false").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR);
+        List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.CRITICAL);
+        doReturn(executionResults).when(healthCheckExecutor).execute(new HealthCheckExecutionOptions(), testTag);
+        
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(jsonSerializer);
+        verify(htmlSerializer)
+                .serialize(resultEquals(new Result(Result.Status.CRITICAL, "Overall Status CRITICAL")), eq(executionResults), eq(false));
+    }
+
+    @Test
+    public void testDoGetJson() throws ServletException, IOException {
+
+        String testTag = "testTag";
+        doReturn(testTag).when(request).getParameter(HealthCheckExecutorServlet.PARAM_TAGS);
+        doReturn("true").when(request).getParameter(HealthCheckExecutorServlet.PARAM_COMBINE_TAGS_WITH_OR);
+        int timeout = 5000;
+        doReturn(timeout + "").when(request).getParameter(HealthCheckExecutorServlet.PARAM_OVERRIDE_GLOBAL_TIMEOUT);
+        doReturn("/result.json").when(request).getPathInfo();
+        List<HealthCheckExecutionResult> executionResults = getExecutionResults(Result.Status.WARN);
+        HealthCheckExecutionOptions options = new HealthCheckExecutionOptions();
+        options.setCombineTagsWithOr(true);
+        options.setOverrideGlobalTimeout(timeout);
+        doReturn(executionResults).when(healthCheckExecutor).execute(options, testTag);
+
+        healthCheckExecutorServlet.doGet(request, response);
+
+        verifyZeroInteractions(htmlSerializer);
+        verify(jsonSerializer).serialize(resultEquals(new Result(Result.Status.WARN, "Overall Status WARN")), eq(executionResults), anyString(),
+                eq(false));
+
+    }
+
+    private List<HealthCheckExecutionResult> getExecutionResults(Result.Status worstStatus) {
+        List<HealthCheckExecutionResult> results = new ArrayList<HealthCheckExecutionResult>();
+        results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(worstStatus, worstStatus.name()), 100));
+        results.add(new ExecutionResult(new HealthCheckMetadata(hcServiceRef), new Result(Result.Status.OK, "OK"), 100));
+        return results;
+    }
+
+    @Test
+    public void testGetStatusMapping() throws ServletException {
+
+        Map<Status, Integer> statusMapping = healthCheckExecutorServlet.getStatusMapping("CRITICAL:503");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 503);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:333");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 333);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 333);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("OK:200,WARN:418,CRITICAL:503,HEALTH_CHECK_ERROR:500");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 418);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500);
+
+        statusMapping = healthCheckExecutorServlet.getStatusMapping("CRITICAL:503,HEALTH_CHECK_ERROR:500");
+        assertEquals(statusMapping.get(Result.Status.OK), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.WARN), (Integer) 200);
+        assertEquals(statusMapping.get(Result.Status.CRITICAL), (Integer) 503);
+        assertEquals(statusMapping.get(Result.Status.HEALTH_CHECK_ERROR), (Integer) 500);
+
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidToken() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("CRITICAL");
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidStatus() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("INVALID:200");
+    }
+
+    @Test(expected = ServletException.class)
+    public void testGetStatusMappingInvalidStatusCode() throws ServletException {
+        healthCheckExecutorServlet.getStatusMapping("CRITICAL:xxx");
+    }
+
+    static Result resultEquals(Result expected) {
+        return argThat(new ResultMatcher(expected));
+    }
+
+    static class ResultMatcher extends ArgumentMatcher<Result> {
+
+        private final Result expectedResult;
+
+        public ResultMatcher(Result expected) {
+            this.expectedResult = expected;
+        }
+
+        @Override
+        public boolean matches(Object actual) {
+            Result actualResult = (Result) actual;
+            return actualResult.getStatus().equals(expectedResult.getStatus()); // simple status matching only sufficient for this test case
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText(expectedResult == null ? null : expectedResult.toString());
+        }
+    }
+
+
+}