Author: ghenzler
Date: Wed Jan 9 20:34:22 2019
New Revision: 1850907
URL: http://svn.apache.org/viewvc?rev=1850907&view=rev
Log:
FELIX-6018 Servlet Filter to answer arbitrary http requests with 503 if certain
HC tags are in non-OK status
Added:
felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java
(with props)
Added:
felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java
URL:
http://svn.apache.org/viewvc/felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java?rev=1850907&view=auto
==============================================================================
---
felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java
(added)
+++
felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java
Wed Jan 9 20:34:22 2019
@@ -0,0 +1,257 @@
+/*
+ * 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.filter;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.felix.hc.api.HealthCheck;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionOptions;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.felix.hc.core.impl.executor.CombinedExecutionResult;
+import org.apache.felix.hc.core.impl.executor.ExtendedHealthCheckExecutor;
+import org.apache.felix.hc.core.impl.servlet.ResultTxtVerboseSerializer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+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.whiteboard.HttpWhiteboardConstants;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Returns a 503 Service Unavailable Page if certain tags are in non-ok
result. */
+@Component(service= {} /* Filter registers itself for better control */,
immediate = true,
+ configurationPolicy = ConfigurationPolicy.REQUIRE)
+@Designate(ocd = ServiceUnavailableFilter.Config.class, factory = true)
+public class ServiceUnavailableFilter implements Filter {
+ private static final Logger LOG =
LoggerFactory.getLogger(ServiceUnavailableFilter.class);
+
+ private static final String CONTENT_TYPE_HTML = "text/html";
+ private static final String CACHE_CONTROL_KEY = "Cache-control";
+ private static final String CACHE_CONTROL_VALUE = "no-cache";
+
+ @ObjectClassDefinition(name = "Health Check Service Unavailable Filter",
description = "Returns a 503 Service Unavailable Page if configured tags are in
non-ok result")
+ public @interface Config {
+
+ String HTML_RESPONSE_DEFAULT = "<html><head><title>Service
Unavailable</title><meta http-equiv=\"refresh\"
content=\"5\"></head><body><strong>Service Unavailable</strong></body></html>";
+
+ @AttributeDefinition(name = "Filter Request Path RegEx", description =
"Regex to be matched against request path")
+ String osgi_http_whiteboard_filter_regex();
+
+ @AttributeDefinition(name = "Filter Context", description = "Needs to
be set to correct whiteboard context filter (e.g.
'(osgi.http.whiteboard.context.name=default)'")
+ String osgi_http_whiteboard_context_select() default "(" +
HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)";
+
+ @AttributeDefinition(name = "Tags", description = "List of tags to
query the status in order to decide if it is 503 or not")
+ String[] tags() default {};
+
+ @AttributeDefinition(name = "Status for 503 response", description =
"First status that causes a 503 response. The default TEMPORARILY_UNAVAILABLE
will not send 503 for OK and WARN but for TEMPORARILY_UNAVAILABLE, CRITICAL and
HEALTH_CHECK_ERROR")
+ Result.Status statusFor503() default
Result.Status.TEMPORARILY_UNAVAILABLE;
+
+ @AttributeDefinition(name = "Include execution result as html
comment", description = "Will include the execution result in html comment.")
+ boolean includeExecutionResultInHtmlComment() default true;
+
+ @AttributeDefinition(name = "503 Html Content", description = "Html
content for 503 responses")
+ String htmlFor503() default HTML_RESPONSE_DEFAULT;
+
+ @AttributeDefinition(name = "Auto-disable filter", description = "If
true, will automatically disable the filter once the filter continued the
filter chain without 503 for the first time. Useful for server startup
scenarios.")
+ boolean autoDisableFilter() default false;
+
+ @AttributeDefinition
+ String webconsole_configurationFactory_nameHint() default "Send 503
for tags {tags} at status {statusFor503} (and worse) for path(s)
{osgi.http.whiteboard.filter.regex}";
+ }
+
+ private String[] tags;
+ private Result.Status statusFor503;
+ private String htmlFor503;
+ private boolean includeExecutionResultInHtmlComment;
+ private boolean autoDisableFilter;
+
+ @Reference
+ private ExtendedHealthCheckExecutor executor;
+
+ @Reference
+ ResultTxtVerboseSerializer verboseTxtSerializer;
+
+ private BundleContext bundleContext;
+ private Dictionary<String, Object> compProperties;
+ private ServiceListener healthCheckServiceListener;
+ private FrameworkListener frameworkListener;
+ private volatile ServiceRegistration<Filter> filterServiceRegistration;
+
+ private HealthCheckExecutionOptions healthCheckExecutionOptions;
+ private ServiceReference<HealthCheck>[]
relevantHealthCheckServiceReferences;
+
+ @Activate
+ protected final void activate(BundleContext bundleContext,
ComponentContext componentContext, Config config) throws InvalidSyntaxException
{
+ this.bundleContext = bundleContext;
+ this.compProperties = componentContext.getProperties();
+
+ this.tags = config.tags();
+ this.statusFor503 = config.statusFor503();
+ this.htmlFor503 = config.htmlFor503();
+ this.includeExecutionResultInHtmlComment =
config.includeExecutionResultInHtmlComment();
+ this.autoDisableFilter = config.autoDisableFilter();
+
+ healthCheckExecutionOptions = new
HealthCheckExecutionOptions().setCombineTagsWithOr(true);
+ healthCheckServiceListener = new HealthCheckServiceListener();
+ bundleContext.addServiceListener(healthCheckServiceListener,
"(objectclass="+HealthCheck.class.getName()+")");
+
+ if(autoDisableFilter) {
+ frameworkListener = new ReregisteringFilterFramworkListener();
+ bundleContext.addFrameworkListener(frameworkListener);
+ }
+
+ selectHcServiceReferences();
+ registerFilter();
+ }
+
+
+ @Deactivate
+ protected final void deactivate() {
+ if(healthCheckServiceListener!=null) {
+ bundleContext.removeServiceListener(healthCheckServiceListener);
+ }
+ if(frameworkListener!=null) {
+ bundleContext.removeFrameworkListener(frameworkListener);
+ }
+
+ // unregisterFilter() last because above listeners potentially
register the filter if they are still active
+ unregisterFilter();
+ }
+
+
+ private synchronized void registerFilter() {
+ if(filterServiceRegistration == null) {
+ filterServiceRegistration =
bundleContext.registerService(Filter.class, this, compProperties);
+ LOG.debug("Registered ServiceUnavailableFilter for tags {}",
Arrays.asList(tags));
+ }
+ }
+
+ private synchronized void unregisterFilter() {
+ if(filterServiceRegistration!=null) {
+ filterServiceRegistration.unregister();
+ filterServiceRegistration = null;
+ LOG.debug("Filter ServiceUnavailableFilter for tags {}
unregistered", Arrays.asList(tags));
+ }
+ }
+
+ // using ServiceListener and ExtendedHealthCheckExecutor to avoid overhead
of searching the service references on every request
+ private final void selectHcServiceReferences() {
+ LOG.debug("Reloading HC references for tags {}", Arrays.asList(tags));
+ relevantHealthCheckServiceReferences =
executor.selectHealthCheckReferences(HealthCheckSelector.tags(tags),
healthCheckExecutionOptions);
+ LOG.debug("Found {} health check service references for tags {}",
relevantHealthCheckServiceReferences.length, tags);
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
+
+ final long startTimeNs = System.nanoTime();
+
+ List<HealthCheckExecutionResult> executionResults =
executor.execute(relevantHealthCheckServiceReferences,
healthCheckExecutionOptions);
+ CombinedExecutionResult combinedExecutionResult = new
CombinedExecutionResult(executionResults);
+ Result overallResult = combinedExecutionResult.getHealthCheckResult();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Time consumed for executing checks: {}ns",
System.nanoTime() - startTimeNs);
+ }
+
+ if(overallResult.getStatus().ordinal() >= statusFor503.ordinal()) {
+ LOG.debug("Result for tags {} is {}, sending 503 for {}", tags,
overallResult.getStatus(), ((HttpServletRequest)request).getRequestURI());
+ String verboseTxtResult = includeExecutionResultInHtmlComment ?
verboseTxtSerializer.serialize(overallResult, executionResults, false) : null;
+ send503((HttpServletResponse) response, verboseTxtResult);
+
+ } else {
+ if(autoDisableFilter && filterServiceRegistration!=null) {
+ LOG.info("Unregistering filter ServiceUnavailableFilter for
tags {} since result was ok ", Arrays.asList(tags));
+ unregisterFilter();
+ }
+
+ // regular request processing
+ filterChain.doFilter(request, response);
+ }
+ }
+
+ private void send503(HttpServletResponse response, String
verboseTxtResult) throws IOException {
+ response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ response.setContentType(CONTENT_TYPE_HTML);
+ response.setHeader(CACHE_CONTROL_KEY, CACHE_CONTROL_VALUE);
+ response.setCharacterEncoding("UTF-8");
+ String bodyClosingTag = "</body>";
+ String htmlContent = verboseTxtResult!=null ?
htmlFor503.replace(bodyClosingTag, "<!--\n\n" + verboseTxtResult + "\n\n-->" +
bodyClosingTag) : htmlFor503;
+ response.getWriter().append(htmlContent);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // no action required
+ }
+
+ @Override
+ public void destroy() {
+ // no action required
+ }
+
+ private final class HealthCheckServiceListener implements ServiceListener {
+ @Override
+ public void serviceChanged(ServiceEvent event) {
+ LOG.debug("Service Event for Health Check: {}", event.getType());
+ selectHcServiceReferences();
+ if(filterServiceRegistration==null) {
+ registerFilter();
+ }
+ }
+ }
+
+ private final class ReregisteringFilterFramworkListener implements
FrameworkListener {
+ @Override
+ public void frameworkEvent(FrameworkEvent event) {
+ if(event.getType() == FrameworkEvent.STARTLEVEL_CHANGED) {
+ if(filterServiceRegistration==null) {
+ registerFilter();
+ }
+ }
+ }
+ }
+
+}
Propchange:
felix/trunk/healthcheck/core/src/main/java/org/apache/felix/hc/core/impl/filter/ServiceUnavailableFilter.java
------------------------------------------------------------------------------
svn:mime-type = text/plain