| /* |
| * 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.ArrayList; |
| import java.util.Arrays; |
| import java.util.Dictionary; |
| 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.sling.commons.osgi.PropertiesUtil; |
| 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.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. |
| * <p> |
| * 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). */ |
| @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(); |
| |
| 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.debug("Registering {} 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.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); |
| 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); |
| } |
| } |
| |
| |
| } |