CAMEL-10026: HealthCheck API
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/00d1d70b Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/00d1d70b Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/00d1d70b Branch: refs/heads/master Commit: 00d1d70ba10f96888791c0e9d73ad044e4126884 Parents: 9675bed Author: lburgazzoli <lburgazz...@gmail.com> Authored: Fri Aug 18 16:27:21 2017 +0200 Committer: lburgazzoli <lburgazz...@gmail.com> Committed: Wed Sep 20 18:42:09 2017 +0200 ---------------------------------------------------------------------- camel-core/src/main/docs/health-check.adoc | 125 +++++++ .../src/main/java/org/apache/camel/Builder.java | 22 ++ .../java/org/apache/camel/CamelContext.java | 20 ++ .../management/mbean/CamelOpenMBeanTypes.java | 15 + .../mbean/ManagedCamelHealthMBean.java | 38 +++ .../extension/ComponentExtensionHelper.java | 23 +- .../DefaultComponentVerifierExtension.java | 26 +- .../org/apache/camel/health/HealthCheck.java | 105 ++++++ .../camel/health/HealthCheckConfiguration.java | 195 +++++++++++ .../apache/camel/health/HealthCheckFilter.java | 31 ++ .../apache/camel/health/HealthCheckHelper.java | 164 +++++++++ .../camel/health/HealthCheckRegistry.java | 80 +++++ .../camel/health/HealthCheckRepository.java | 30 ++ .../camel/health/HealthCheckResultBuilder.java | 144 ++++++++ .../apache/camel/health/HealthCheckService.java | 108 ++++++ .../java/org/apache/camel/health/package.html | 25 ++ .../apache/camel/impl/DefaultCamelContext.java | 17 + .../camel/impl/health/AbstractHealthCheck.java | 235 +++++++++++++ .../camel/impl/health/ContextHealthCheck.java | 58 ++++ .../impl/health/DefaultHealthCheckRegistry.java | 150 ++++++++ .../impl/health/DefaultHealthCheckService.java | 280 +++++++++++++++ .../health/PerformanceCounterEvaluator.java | 30 ++ .../camel/impl/health/RegistryRepository.java | 45 +++ .../camel/impl/health/RouteHealthCheck.java | 108 ++++++ .../RoutePerformanceCounterEvaluators.java | 288 ++++++++++++++++ .../health/RoutesHealthCheckRepository.java | 159 +++++++++ .../org/apache/camel/impl/health/package.html | 25 ++ .../DefaultManagementLifecycleStrategy.java | 21 ++ .../DefaultManagementNamingStrategy.java | 18 + .../DefaultManagementObjectStrategy.java | 7 + .../management/ManagedManagementStrategy.java | 6 + .../management/mbean/ManagedCamelHealth.java | 117 +++++++ .../java/org/apache/camel/spi/GroupAware.java | 31 ++ .../org/apache/camel/spi/HasCamelContext.java | 32 ++ .../java/org/apache/camel/spi/HasGroup.java | 31 ++ .../camel/spi/ManagementNamingStrategy.java | 2 + .../camel/spi/ManagementObjectStrategy.java | 2 + .../org/apache/camel/util/ObjectHelper.java | 25 ++ .../org/apache/camel/util/ReferenceCounted.java | 38 +++ .../camel/util/concurrent/LockHelper.java | 44 ++- .../util/function/ThrowingToLongFunction.java | 23 ++ .../health/DefaultHealthCheckRegistryTest.java | 92 +++++ .../health/DefaultHealthCheckServiceTest.java | 103 ++++++ .../camel/impl/health/HealthCheckTest.java | 159 +++++++++ .../verifier/DefaultComponentVerifierTest.java | 8 +- .../consul/ConsulClientConfiguration.java | 342 +++++++++++++++++++ .../camel/component/consul/ConsulComponent.java | 1 - .../component/consul/ConsulConfiguration.java | 319 +---------------- .../camel/component/consul/ConsulEndpoint.java | 2 +- .../consul/cloud/ConsulServiceDiscovery.java | 10 +- .../consul/ha/ConsulClusterConfiguration.java | 4 +- .../component/consul/ha/ConsulClusterView.java | 10 +- .../health/ConsulHealthCheckRepository.java | 230 +++++++++++++ ...onsulHealthCheckRepositoryConfiguration.java | 93 +++++ .../consul/policy/ConsulRoutePolicy.java | 2 +- .../cloud/ConsulServiceDiscoveryTest.java | 2 +- .../xml/AbstractCamelContextFactoryBean.java | 29 +- .../camel-servicenow-component/pom.xml | 2 +- .../src/main/docs/servicenow-component.adoc | 6 +- .../component/servicenow/ServiceNowClient.java | 22 +- .../servicenow/ServiceNowComponent.java | 165 +++++---- .../ServiceNowComponentVerifierExtension.java | 95 +++++- .../servicenow/ServiceNowConstants.java | 2 + .../auth/AuthenticationRequestFilter.java | 2 +- .../spring/boot/CamelAutoConfiguration.java | 27 +- .../actuate/endpoint/AbstractCamelEndpoint.java | 42 +++ .../endpoint/AbstractCamelMvcEndpoint.java | 72 ++++ .../endpoint/CamelHealthCheckEndpoint.java | 180 ++++++++++ ...melHealthCheckEndpointAutoConfiguration.java | 49 +++ .../endpoint/CamelHealthCheckMvcEndpoint.java | 103 ++++++ .../endpoint/CamelRouteControllerEndpoint.java | 14 +- .../CamelRouteControllerMvcEndpoint.java | 52 +-- .../actuate/endpoint/CamelRoutesEndpoint.java | 319 ++--------------- .../endpoint/CamelRoutesMvcEndpoint.java | 61 +--- .../health/CamelHealthAutoConfiguration.java | 32 +- .../health/CamelHealthCheckIndicator.java | 68 ++++ ...elHealthCheckIndicatorAutoConfiguration.java | 94 +++++ .../CamelHealthCheckIndicatorConfiguration.java | 72 ++++ .../health/CamelHealthConfiguration.java | 36 ++ .../actuate/health/CamelHealthIndicator.java | 4 +- .../AbstractHealthCheckConfiguration.java | 91 +++++ .../HealthCheckRoutesAutoConfiguration.java | 165 +++++++++ .../health/HealthCheckRoutesConfiguration.java | 249 ++++++++++++++ .../HealthCheckServiceAutoConfiguration.java | 79 +++++ .../health/HealthCheckServiceConfiguration.java | 79 +++++ .../spring/boot/health/HealthConfiguration.java | 35 ++ .../spring/boot/health/HealthConstants.java | 26 ++ .../camel/spring/boot/model/RouteDetails.java | 216 ++++++++++++ .../spring/boot/model/RouteDetailsInfo.java | 41 +++ .../camel/spring/boot/model/RouteInfo.java | 73 ++++ .../main/resources/META-INF/spring.factories | 4 + .../endpoint/CamelHealthCheckEndpointTest.java | 121 +++++++ .../endpoint/CamelRoutesEndpointTest.java | 4 +- .../endpoint/CamelRoutesMvcEndpointTest.java | 10 +- .../boot/health/HealthCheckRegistryTest.java | 89 +++++ .../spring/health/HealthCheckRegistryTest.java | 67 ++++ .../spring/health/HealthCheckRegistryTest.xml | 56 +++ .../health/HealthCheckRegistryTest.java | 57 ++++ .../health/HealthCheckRegistryTest.xml | 54 +++ .../component/undertow/UndertowComponent.java | 4 +- .../UndertowComponentVerifierExtension.java | 64 +++- .../component/undertow/UndertowEndpoint.java | 12 +- .../component/undertow/UndertowHelper.java | 27 ++ .../component/undertow/UndertowProducer.java | 3 - .../rest/RestUndertowComponentVerifierTest.java | 6 +- .../application/pom.xml | 106 ++++++ .../src/main/java/sample/camel/Application.java | 35 ++ .../java/sample/camel/ApplicationCheck.java | 49 +++ .../sample/camel/ApplicationConfiguration.java | 59 ++++ .../src/main/resources/application.properties | 82 +++++ .../pom.xml | 68 ++++ .../readme.adoc | 213 ++++++++++++ .../service/pom.xml | 84 +++++ .../service/src/main/bash/consul-run-linux.sh | 42 +++ .../service/src/main/bash/consul-run-osx.sh | 45 +++ .../main/java/sample/service/Application.java | 36 ++ .../src/main/resources/application.properties | 25 ++ examples/pom.xml | 1 + .../ConsulComponentConfiguration.java | 76 ++--- .../HealthCheckRepositoryAutoConfiguration.java | 63 ++++ .../HealthCheckRepositoryConfiguration.java | 56 +++ .../main/resources/META-INF/spring.factories | 3 +- .../src/test/resources/application.properties | 16 + .../ServiceNowComponentConfiguration.java | 48 +++ .../main/resources/META-INF/spring.factories | 1 + .../main/resources/META-INF/spring.factories | 1 + .../src/test/resources/application.properties | 16 + 127 files changed, 7694 insertions(+), 931 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/docs/health-check.adoc ---------------------------------------------------------------------- diff --git a/camel-core/src/main/docs/health-check.adoc b/camel-core/src/main/docs/health-check.adoc new file mode 100644 index 0000000..1d2bf5e --- /dev/null +++ b/camel-core/src/main/docs/health-check.adoc @@ -0,0 +1,125 @@ +[[HealthCheck-HealthCheck]] +== HealthCheck + +*Available as of Camel 2.20* + +Camel 2.20 provides an *experimental* support to probe the state of a Camel integration via a pluggable Health Check strategy based on the following concepts: + +- *HealthCheck:* represent an health check and defines its basic contract; +- *HealthCheckResponse:* represent an health check invocation response; +- *HealthCheckConfiguration:* a basic configuration object that holds some basic settings like the minimum delay between calls, the number of time a service may be reported as unhealthy before marking the check as failed; beside those simple options, the check implementation is responsible to implement further limitations when needed; +- *HealthCheckRegistry:* a registry for health checks; +- *HealthCheckRepository:* a simple interface to define health check providers and by default there is one that grabs all the checks available in the registry so you can add your own check i.e. istantiating your bean in spring/spring-boot; components can provide theirs own repository; +- *HealthCheckService:* a simple service that runs in the background and invokes the checks according to a schedule; + +=== Examples: + +- *Spring Boot*: ++ +[source,properties] +---- +# Enable route checks +camel.health.check.routes.enabled = true + +# Configure default thresholds +camel.health.check.routes.thresholds.exchanges-failed = 10 + +# Configure a different exchanges-failed threshold for the route bar +camel.health.check.routes.threshold[bar].exchanges-failed = 20 + +# Configure different thresholds for the route slow without inherit global +# thresholds +camel.health.check.routes.threshold[slow].inherit = false +camel.health.check.routes.threshold[slow].last-processing-time.threshold = 1s +camel.health.check.routes.threshold[slow].last-processing-time.failures = 5 +---- + +- *Spring XML DSL*: ++ +[source,xml] +---- +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans.xsd + http://camel.apache.org/schema/spring + http://camel.apache.org/schema/spring/camel-spring.xsd"> + + <!-- + This repository will automatically be added to Camel's health check + repository list. + --> + <bean id="hc-repo-routes" class="org.apache.camel.impl.health.RoutesHealthCheckRepository"> + <property name="evaluators"> + <list> + <!-- + Set the checks that will be applied to every route if no per route + configuration is defined, see below. + --> + <bean class="org.apache.camel.impl.health.RoutePerformanceCounterEvaluators.ExchangesFailed"> + <constructor-arg value="10"/> + </bean> + <bean class="org.apache.camel.impl.health.RoutePerformanceCounterEvaluators.LastProcessingTime"> + <constructor-arg value="1000"/> + <constructor-arg value="1"/> + </bean> + </list> + </property> + <property name="routesEvaluators"> + <map> + <!-- + Set the checks to be associated with the route named route-1, note that + default checks are not inherit so there will be only one check for this + route. + --> + <entry key="route-1"> + <list> + <bean class="org.apache.camel.impl.health.RoutePerformanceCounterEvaluators.ExchangesInflight"> + <constructor-arg value="10"/> + </bean> + </list> + </entry> + </map> + </property> + </bean> + + <camelContext xmlns="http://camel.apache.org/schema/spring"> + ... + </camelContext> + +</beans> +---- + +=== Writing a custom check: + +As of version 2.20.0, there are a limited number of health checks provided by Camel out of the box so you may need to write your own check which you can do by implementing the _HealthCheck_ interface or by extending _AbstractHealthCheck_ which provides some usueful methods: + +[source,java] +---- +public final class MyHealthCheck extends AbstractHealthCheck { + public ContextHealthCheck() { + super("camel", "my-check"); + + // make this check enabled by default + getConfiguration().setEnabled(true); + } + + @Override + protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) { + // Default value + builder.unknown(); + + // Add some details to the check result + builder.detail("my.detail", camelContext.getName()); + + if (unhealtyCondition) { + builder.down(); + } else { + builder.up(); + } + } +} +---- + +You can now make _MyHealthCheck_ available to camel by adding an instance to the application context (Spring, Blueprint) or directly to the registry. http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/Builder.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/Builder.java b/camel-core/src/main/java/org/apache/camel/Builder.java new file mode 100644 index 0000000..33da85d --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/Builder.java @@ -0,0 +1,22 @@ +/** + * 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 ASF 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.camel; + +@FunctionalInterface +public interface Builder<T> { + T build(); +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/CamelContext.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/CamelContext.java b/camel-core/src/main/java/org/apache/camel/CamelContext.java index 3ccb716..b1080d4 100644 --- a/camel-core/src/main/java/org/apache/camel/CamelContext.java +++ b/camel-core/src/main/java/org/apache/camel/CamelContext.java @@ -30,6 +30,7 @@ import org.apache.camel.api.management.mbean.ManagedCamelContextMBean; import org.apache.camel.api.management.mbean.ManagedProcessorMBean; import org.apache.camel.api.management.mbean.ManagedRouteMBean; import org.apache.camel.builder.ErrorHandlerBuilder; +import org.apache.camel.health.HealthCheckRegistry; import org.apache.camel.model.DataFormatDefinition; import org.apache.camel.model.HystrixConfigurationDefinition; import org.apache.camel.model.ProcessorDefinition; @@ -2062,4 +2063,23 @@ public interface CamelContext extends SuspendableService, RuntimeConfiguration { */ void setHeadersMapFactory(HeadersMapFactory factory); + /** + * Returns an optional {@link HealthCheckRegistry}, by default no registry is + * present and it must be explicit activated. Components can register/unregister + * health checks in response to life-cycle events (i.e. start/stop). + * + * This registry is not used by the camel context but it is up to the impl to + * properly use it, i.e. + * + * - a RouteController could use the registry to decide to restart a route + * with failing health checks + * - spring boot could integrate such checks within its health endpoint or + * make it available only as separate endpoint. + */ + HealthCheckRegistry getHealthCheckRegistry(); + + /** + * Sets a {@link HealthCheckRegistry}. + */ + void setHealthCheckRegistry(HealthCheckRegistry healthCheckRegistry); } http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java b/camel-core/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java index 2672447..5e9f048 100644 --- a/camel-core/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java +++ b/camel-core/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java @@ -240,4 +240,19 @@ public final class CamelOpenMBeanTypes { new String[]{"Type", "Static", "Dynamic", "Description"}, new OpenType[]{SimpleType.STRING, SimpleType.BOOLEAN, SimpleType.BOOLEAN, SimpleType.STRING}); } + + + + + public static CompositeType camelHealthDetailsCompositeType() throws OpenDataException { + return new CompositeType("healthDetails", "Health Details", + new String[]{"id", "group", "state", "enabled", "interval", "failureThreshold"}, + new String[]{"ID", "Group", "State", "Enabled", "Interval", "Failure Threshold"}, + new OpenType[]{SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, SimpleType.BOOLEAN, SimpleType.LONG, SimpleType.INTEGER}); + } + + public static TabularType camelHealthDetailsTabularType() throws OpenDataException { + CompositeType ct = camelHealthDetailsCompositeType(); + return new TabularType("healthDetails", "Health Details", ct, new String[]{"id"}); + } } http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelHealthMBean.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelHealthMBean.java b/camel-core/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelHealthMBean.java new file mode 100644 index 0000000..26d6d63 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/api/management/mbean/ManagedCamelHealthMBean.java @@ -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 ASF 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.camel.api.management.mbean; + +import java.util.Collection; +import javax.management.openmbean.TabularData; + +import org.apache.camel.api.management.ManagedAttribute; +import org.apache.camel.api.management.ManagedOperation; + +public interface ManagedCamelHealthMBean { + + @ManagedAttribute(description = "Application Health") + boolean getIsHealthy(); + + @ManagedAttribute(description = "Registered Health Checks IDs") + Collection<String> getHealthChecksIDs(); + + @ManagedOperation(description = "Registered Health Checks Details") + TabularData details(); + + @ManagedOperation(description = "Invoke an Health Check by ID") + String invoke(String id); +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/component/extension/ComponentExtensionHelper.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/extension/ComponentExtensionHelper.java b/camel-core/src/main/java/org/apache/camel/component/extension/ComponentExtensionHelper.java index 3487b3c..2bb93ae 100644 --- a/camel-core/src/main/java/org/apache/camel/component/extension/ComponentExtensionHelper.java +++ b/camel-core/src/main/java/org/apache/camel/component/extension/ComponentExtensionHelper.java @@ -17,27 +17,26 @@ package org.apache.camel.component.extension; import org.apache.camel.CamelContext; -import org.apache.camel.CamelContextAware; import org.apache.camel.Component; -import org.apache.camel.ComponentAware; +import org.apache.camel.util.ObjectHelper; public final class ComponentExtensionHelper { private ComponentExtensionHelper() { } + /** + * @deprecated use {@link ObjectHelper#trySetCamelContext(Object, CamelContext)} + */ + @Deprecated public static <T> T trySetCamelContext(T object, CamelContext camelContext) { - if (object instanceof CamelContextAware) { - ((CamelContextAware) object).setCamelContext(camelContext); - } - - return object; + return ObjectHelper.trySetCamelContext(object, camelContext); } + /** + * @deprecated use {@link ObjectHelper#trySetComponent(Object, Component)} + */ + @Deprecated public static <T> T trySetComponent(T object, Component component) { - if (object instanceof ComponentAware) { - ((ComponentAware) object).setComponent(component); - } - - return object; + return ObjectHelper.trySetComponent(object, component); } } http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/component/extension/verifier/DefaultComponentVerifierExtension.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/extension/verifier/DefaultComponentVerifierExtension.java b/camel-core/src/main/java/org/apache/camel/component/extension/verifier/DefaultComponentVerifierExtension.java index a5495ca..b123ab2 100644 --- a/camel-core/src/main/java/org/apache/camel/component/extension/verifier/DefaultComponentVerifierExtension.java +++ b/camel-core/src/main/java/org/apache/camel/component/extension/verifier/DefaultComponentVerifierExtension.java @@ -23,6 +23,8 @@ import java.util.stream.Collectors; import org.apache.camel.CamelContext; import org.apache.camel.CamelContextAware; +import org.apache.camel.Component; +import org.apache.camel.ComponentAware; import org.apache.camel.ComponentVerifier; import org.apache.camel.TypeConverter; import org.apache.camel.component.extension.ComponentVerifierExtension; @@ -34,17 +36,23 @@ import org.apache.camel.util.IntrospectionSupport; import static org.apache.camel.util.StreamUtils.stream; -public class DefaultComponentVerifierExtension implements ComponentVerifierExtension, ComponentVerifier, CamelContextAware { +public class DefaultComponentVerifierExtension implements ComponentVerifierExtension, ComponentVerifier, CamelContextAware, ComponentAware { private final String defaultScheme; + private Component component; private CamelContext camelContext; - public DefaultComponentVerifierExtension(String defaultScheme) { - this(defaultScheme, null); + protected DefaultComponentVerifierExtension(String defaultScheme) { + this(defaultScheme, null, null); } - public DefaultComponentVerifierExtension(String defaultScheme, CamelContext camelContext) { + protected DefaultComponentVerifierExtension(String defaultScheme, CamelContext camelContext) { + this(defaultScheme, camelContext, null); + } + + protected DefaultComponentVerifierExtension(String defaultScheme, CamelContext camelContext, Component component) { this.defaultScheme = defaultScheme; this.camelContext = camelContext; + this.component = component; } // ************************************* @@ -62,6 +70,16 @@ public class DefaultComponentVerifierExtension implements ComponentVerifierExten } @Override + public Component getComponent() { + return component; + } + + @Override + public void setComponent(Component component) { + this.component = component; + } + + @Override public Result verify(Scope scope, Map<String, Object> parameters) { // Camel context is mandatory if (this.camelContext == null) { http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheck.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheck.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheck.java new file mode 100644 index 0000000..c6bc9df --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheck.java @@ -0,0 +1,105 @@ +/** + * 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 ASF 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.camel.health; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.apache.camel.Ordered; +import org.apache.camel.spi.HasGroup; +import org.apache.camel.spi.HasId; + +public interface HealthCheck extends HasGroup, HasId, Ordered { + enum State { + UP, + DOWN, + UNKNOWN + } + + @Override + default int getOrder() { + return Ordered.LOWEST; + } + + /** + * Return meta data associated with this {@link HealthCheck}. + */ + default Map<String, Object> getMetaData() { + return Collections.emptyMap(); + } + + /** + * Return the configuration associated with this {@link HealthCheck}. + */ + HealthCheckConfiguration getConfiguration(); + + /** + * Invoke the check. + * + * @see {@link #call(Map)} + */ + default Result call() { + return call(Collections.emptyMap()); + } + + /** + * Invoke the check. The implementation is responsible to eventually perform + * the check according to the limitation of the third party system i.e. + * it should not be performed too often to avoid rate limiting. The options + * argument can be used to pass information specific to the check like + * forcing the check to be performed against the the policies. The implementation + * is responsible to catch an handle any exception thrown by the underlying + * technology, including unchecked ones. + */ + Result call(Map<String, Object> options); + + /** + * Response to an health check invocation. + */ + interface Result { + + /** + * The {@link HealthCheck} associated to this response. + */ + HealthCheck getCheck(); + + /** + * The state of the service. + */ + State getState(); + + /** + * A message associated to the result, used to provide more information + * for unhealthy services. + */ + Optional<String> getMessage(); + + /** + * An error associated to the result, used to provide the error associated + * to unhealthy services. + */ + Optional<Throwable> getError(); + + /** + * An key/value combination of details. + * + * @return a non null details map + */ + Map<String, Object> getDetails(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckConfiguration.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckConfiguration.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckConfiguration.java new file mode 100644 index 0000000..2621e1b --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckConfiguration.java @@ -0,0 +1,195 @@ +/** + * 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 ASF 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.camel.health; + +import java.time.Duration; + +import org.apache.camel.RuntimeCamelException; +import org.apache.camel.converter.TimePatternConverter; +import org.apache.camel.util.ObjectHelper; + +public class HealthCheckConfiguration implements Cloneable { + public static final Boolean DEFAULT_VALUE_ENABLED = Boolean.FALSE; + public static final Duration DEFAULT_VALUE_INTERVAL = Duration.ZERO; + public static final Integer DEFAULT_VALUE_FAILURE_THRESHOLD = 0; + + /** + * Set if the check associated to this configuration is enabled or not. + */ + private Boolean enabled; + + /** + * Set the check interval. + */ + private Duration interval; + + /** + * Set the number of failure before reporting the service as un-healthy. + */ + private Integer failureThreshold; + + // ************************************************* + // Properties + // ************************************************* + + /** + * @return true if the check associated to this configuration is enabled, + * false otherwise. + */ + public Boolean isEnabled() { + return enabled; + } + + /** + * Set if the check associated to this configuration is enabled or not. + */ + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + /** + * @return the check interval. + */ + public Duration getInterval() { + return interval; + } + + /** + * Set the check interval. + */ + public void setInterval(Duration interval) { + this.interval = interval; + } + + /** + * Set the check interval in a human readable format. + */ + public void setInterval(String interval) { + if (ObjectHelper.isNotEmpty(interval)) { + this.interval = Duration.ofMillis(TimePatternConverter.toMilliSeconds(interval)); + } else { + this.interval = null; + } + } + + /** + * @return the number of failure before reporting the service as un-healthy. + */ + public Integer getFailureThreshold() { + return failureThreshold; + } + + /** + * Set the number of failure before reporting the service as un-healthy. + */ + public void setFailureThreshold(Integer failureThreshold) { + this.failureThreshold = failureThreshold; + } + + // ************************************************* + // + // ************************************************* + public static Boolean defaultValueEnabled() { + return DEFAULT_VALUE_ENABLED; + } + + public static Duration defaultValueInterval() { + return DEFAULT_VALUE_INTERVAL; + } + + public static Integer defaultValueFailureThreshold() { + return DEFAULT_VALUE_FAILURE_THRESHOLD; + } + + public HealthCheckConfiguration copy() { + try { + return (HealthCheckConfiguration)super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeCamelException(e); + } + } + + public static Builder builder() { + return new Builder(); + } + + // ************************************************* + // + // ************************************************* + + public static final class Builder implements org.apache.camel.Builder<HealthCheckConfiguration> { + private Boolean enabled; + private Duration interval; + private Integer failureThreshold; + + private Builder() { + } + + public Builder complete(HealthCheckConfiguration template) { + if (template != null) { + if (this.enabled == null) { + this.enabled = template.enabled; + } + if (this.interval == null) { + this.interval = template.interval; + } + if (this.failureThreshold == null) { + this.failureThreshold = template.failureThreshold; + } + } + + return this; + } + + public Builder enabled(Boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder interval(Duration interval) { + this.interval = interval; + return this; + } + + public Builder interval(Long interval) { + return ObjectHelper.isNotEmpty(interval) + ? interval(Duration.ofMillis(interval)) + : this; + } + + public Builder interval(String interval) { + return ObjectHelper.isNotEmpty(interval) + ? interval(TimePatternConverter.toMilliSeconds(interval)) + : this; + } + + public Builder failureThreshold(Integer failureThreshold) { + this.failureThreshold = failureThreshold; + return this; + } + + @Override + public HealthCheckConfiguration build() { + HealthCheckConfiguration conf = new HealthCheckConfiguration(); + conf.setEnabled(ObjectHelper.supplyIfEmpty(enabled, HealthCheckConfiguration::defaultValueEnabled)); + conf.setInterval(ObjectHelper.supplyIfEmpty(interval, HealthCheckConfiguration::defaultValueInterval)); + conf.setFailureThreshold(ObjectHelper.supplyIfEmpty(failureThreshold, HealthCheckConfiguration::defaultValueFailureThreshold)); + + return conf; + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckFilter.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckFilter.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckFilter.java new file mode 100644 index 0000000..57d48ca --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckFilter.java @@ -0,0 +1,31 @@ +/** + * 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 ASF 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.camel.health; + +/** + * Health check filter definition. + */ +@FunctionalInterface +public interface HealthCheckFilter { + /** + * Determine if the given {@link HealthCheck} has to be filtered out. + * + * @param check the check to evaluate. + * @return true if the given <dode>check</dode> has to be filtered out. + */ + boolean test(HealthCheck check); +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckHelper.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckHelper.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckHelper.java new file mode 100644 index 0000000..25596bc --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckHelper.java @@ -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 ASF 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.camel.health; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.camel.CamelContext; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class HealthCheckHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(HealthCheckHelper.class); + + private HealthCheckHelper() { + } + + /** + * Get the group of the given check or an empty string if the group is not set. + * + * @param check the health check + * @return the {@link HealthCheck#getGroup()} or an empty string if it is <code>null</code> + */ + public static String getGroup(HealthCheck check) { + return ObjectHelper.supplyIfEmpty(check.getGroup(), () -> ""); + } + + /** + * Invokes the checks and returns a collection of results. + */ + public static Collection<HealthCheck.Result> invoke(CamelContext camelContext) { + return invoke(camelContext, check -> Collections.emptyMap(), check -> false); + } + + /** + * Invokes the checks and returns a collection of results. + */ + public static Collection<HealthCheck.Result> invoke( + CamelContext camelContext, + Function<HealthCheck, Map<String, Object>> optionsSupplier) { + + return invoke(camelContext, optionsSupplier, check -> false); + } + + /** + * Invokes the checks and returns a collection of results. + */ + public static Collection<HealthCheck.Result> invoke( + CamelContext camelContext, + HealthCheckFilter filter) { + + return invoke(camelContext, check -> Collections.emptyMap(), filter); + } + + /** + * Invokes the checks and returns a collection of results. + * + * @param camelContext the camel context. + * @param optionsSupplier a supplier for options. + * @param filter filter to exclude some checks. + */ + public static Collection<HealthCheck.Result> invoke( + CamelContext camelContext, + Function<HealthCheck, Map<String, Object>> optionsSupplier, + HealthCheckFilter filter) { + + final HealthCheckRegistry registry = camelContext.getHealthCheckRegistry(); + final HealthCheckService service = camelContext.hasService(HealthCheckService.class); + + if (service != null) { + // If a health check service is defined retrieve the current status + // of the checks hold by the service. + return service.getResults().stream() + .filter(result -> !filter.test(result.getCheck())) + .collect(Collectors.toList()); + } else if (registry != null) { + // If no health check service is defined, this endpoint invokes the + // check one by one. + return registry.stream() + .collect(Collectors.groupingBy(HealthCheckHelper::getGroup)) + .entrySet().stream() + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .filter(check -> !filter.test(check)) + .sorted(Comparator.comparingInt(HealthCheck::getOrder)) + .map(check -> check.call(optionsSupplier.apply(check))) + .collect(Collectors.toList()); + } else { + LOGGER.debug("No health check source found"); + } + + return Collections.emptyList(); + } + + /** + * Query the status of a check by id. Note that this may result in an effective + * invocation of the {@link HealthCheck}, i.e. when no {@link HealthCheckService} + * is available. + * + * @param camelContext the camel context. + * @param id the check id. + * @param options the check options. + * @return an optional {@link HealthCheck.Result}. + */ + public static Optional<HealthCheck.Result> query(CamelContext camelContext, String id, Map<String, Object> options) { + final HealthCheckRegistry registry = camelContext.getHealthCheckRegistry(); + final HealthCheckService service = camelContext.hasService(HealthCheckService.class); + + if (service != null) { + return service.getResults().stream() + .filter(result -> ObjectHelper.equal(result.getCheck().getId(), id)) + .findFirst(); + } else if (registry != null) { + return registry.getCheck(id).map(check -> check.call(options)); + } else { + LOGGER.debug("No health check source found"); + } + + return Optional.empty(); + } + + /** + * Invoke a check by id. + * + * @param camelContext the camel context. + * @param id the check id. + * @param options the check options. + * @return an optional {@link HealthCheck.Result}. + */ + public static Optional<HealthCheck.Result> invoke(CamelContext camelContext, String id, Map<String, Object> options) { + final HealthCheckRegistry registry = camelContext.getHealthCheckRegistry(); + final HealthCheckService service = camelContext.hasService(HealthCheckService.class); + + if (service != null) { + return service.call(id, options); + } else if (registry != null) { + return registry.getCheck(id).map(check -> check.call(options)); + } else { + LOGGER.debug("No health check source found"); + } + + return Optional.empty(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRegistry.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckRegistry.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRegistry.java new file mode 100644 index 0000000..dc523eb --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRegistry.java @@ -0,0 +1,80 @@ +/** + * 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 ASF 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.camel.health; + +import java.util.Collection; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.camel.CamelContextAware; +import org.apache.camel.util.ObjectHelper; + +/** + * A registry for health checks. + * <p> + * Note that this registry can be superseded by the future camel context internal + * registry, @see <a href="https://issues.apache.org/jira/browse/CAMEL-10792"/>. + */ +public interface HealthCheckRegistry extends HealthCheckRepository, CamelContextAware { + /** + * Registers a service {@link HealthCheck}. + */ + boolean register(HealthCheck check); + + /** + * Unregisters a service {@link HealthCheck}. + */ + boolean unregister(HealthCheck check); + + /** + * Set the health check repositories to use.. + */ + void setRepositories(Collection<HealthCheckRepository> repositories); + + /** + * Get a collection of health check repositories. + */ + Collection<HealthCheckRepository> getRepositories(); + + /** + * Add an Health Check repository. + */ + boolean addRepository(HealthCheckRepository repository); + + /** + * Remove an Health Check repository. + */ + boolean removeRepository(HealthCheckRepository repository); + + /** + * A collection of health check IDs. + */ + default Collection<String> getCheckIDs() { + return stream() + .map(HealthCheck::getId) + .collect(Collectors.toList()); + } + + /** + * Returns the check identified by the given <code>id</code> if available. + */ + default Optional<HealthCheck> getCheck(String id) { + return stream() + .filter(check -> ObjectHelper.equal(check.getId(), id)) + .findFirst(); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRepository.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckRepository.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRepository.java new file mode 100644 index 0000000..65d5acf --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckRepository.java @@ -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 ASF 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.camel.health; + +import java.util.stream.Stream; + +/** + * A repository for health checks. + */ +public interface HealthCheckRepository { + /** + * Returns a sequential {@code Stream} with the known {@link HealthCheck} + * as its source. + */ + Stream<HealthCheck> stream(); +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckResultBuilder.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckResultBuilder.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckResultBuilder.java new file mode 100644 index 0000000..e5853b8 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckResultBuilder.java @@ -0,0 +1,144 @@ +/** + * 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 ASF 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.camel.health; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.camel.Builder; +import org.apache.camel.util.ObjectHelper; + +/** + * A builder helper to create a result. + */ +public final class HealthCheckResultBuilder implements Builder<HealthCheck.Result> { + private HealthCheck check; + private String message; + private Throwable error; + private Map<String, Object> details; + private HealthCheck.State state; + + private HealthCheckResultBuilder(HealthCheck check) { + this.check = check; + } + + public String message() { + return this.message; + } + + public HealthCheckResultBuilder message(String message) { + this.message = message; + return this; + } + + public Throwable error() { + return this.error; + } + + public HealthCheckResultBuilder error(Throwable error) { + this.error = error; + return this; + } + + public Object detail(String key) { + return this.details != null ? this.details.get(key) : null; + } + + public HealthCheckResultBuilder detail(String key, Object value) { + if (this.details == null) { + this.details = new HashMap<>(); + } + + this.details.put(key, value); + return this; + } + + public HealthCheckResultBuilder details(Map<String, Object> details) { + if (ObjectHelper.isNotEmpty(details)) { + details.forEach(this::detail); + } + + return this; + } + + public HealthCheck.State state() { + return this.state; + } + + public HealthCheckResultBuilder state(HealthCheck.State state) { + this.state = state; + return this; + } + + public HealthCheckResultBuilder up() { + return state(HealthCheck.State.UP); + } + + public HealthCheckResultBuilder down() { + return state(HealthCheck.State.DOWN); + } + + public HealthCheckResultBuilder unknown() { + return state(HealthCheck.State.UNKNOWN); + } + + @Override + public HealthCheck.Result build() { + // Validation + ObjectHelper.notNull(this.state, "Response State"); + + final HealthCheck.State responseState = this.state; + final Optional<String> responseMessage = Optional.ofNullable(this.message); + final Optional<Throwable> responseError = Optional.ofNullable(this.error); + final Map<String, Object> responseDetails = HealthCheckResultBuilder.this.details != null + ? Collections.unmodifiableMap(new HashMap<>(HealthCheckResultBuilder.this.details)) + : Collections.emptyMap(); + + return new HealthCheck.Result() { + @Override + public HealthCheck getCheck() { + return check; + } + + @Override + public HealthCheck.State getState() { + return responseState; + } + + @Override + public Optional<String> getMessage() { + return responseMessage; + } + + @Override + public Optional<Throwable> getError() { + return responseError; + } + + @Override + public Map<String, Object> getDetails() { + return responseDetails; + } + }; + } + + public static HealthCheckResultBuilder on(HealthCheck check) { + return new HealthCheckResultBuilder(check); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/HealthCheckService.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/HealthCheckService.java b/camel-core/src/main/java/org/apache/camel/health/HealthCheckService.java new file mode 100644 index 0000000..95a4b7c --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/HealthCheckService.java @@ -0,0 +1,108 @@ +/** + * 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 ASF 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.camel.health; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +import org.apache.camel.CamelContextAware; +import org.apache.camel.Service; + +/** + * An health check service that invokes the checks registered on the {@link HealthCheckRegistry} + * according to a schedule. + */ +public interface HealthCheckService extends Service, CamelContextAware { + /** + * Add a listener to invoke when the state of a check change. + * + * @param consumer the event listener. + */ + void addStateChangeListener(BiConsumer<HealthCheck.State, HealthCheck> consumer); + + /** + * Remove the state change listener. + * + * @param consumer the event listener to remove. + */ + void removeStateChangeListener(BiConsumer<HealthCheck.State, HealthCheck> consumer); + + /** + * Sets the options to be used when invoking the check identified by the + * given id. + * + * @param id the health check id. + * @param options the health check options. + */ + void setHealthCheckOptions(String id, Map<String, Object> options); + + /** + * @see {@link #call(String, Map)} + * + * @param id the health check id. + * @return the result of the check or {@link Optional#empty()} if the id is unknown. + */ + default Optional<HealthCheck.Result> call(String id) { + return call(id, Collections.emptyMap()); + } + + /** + * Invokes the check identified by the given <code>id</code> with the given + * <code>options</code>. + * + * @param id the health check id. + * @param options the health check options. + * @return the result of the check or {@link Optional#empty()} if the id is unknown. + */ + Optional<HealthCheck.Result> call(String id, Map<String, Object> options); + + /** + * Notify the service that a check has changed status. This may be useful for + * stateful checks like checks rely on tcp/ip connections. + * + * @param check the health check. + * @param result the health check result. + */ + void notify(HealthCheck check, HealthCheck.Result result); + + /** + * Return a list of the known checks status. + * + * @return the list of results. + */ + Collection<HealthCheck.Result> getResults(); + + /** + * Access the underlying concrete HealthCheckService implementation to + * provide access to further features. + * + * @param clazz the proprietary class or interface of the underlying concrete HealthCheckService. + * @return an instance of the underlying concrete HealthCheckService as the required type. + */ + default <T extends HealthCheckService> T unwrap(Class<T> clazz) { + if (HealthCheckService.class.isAssignableFrom(clazz)) { + return clazz.cast(this); + } + + throw new IllegalArgumentException( + "Unable to unwrap this HealthCheckService type (" + getClass() + ") to the required type (" + clazz + ")" + ); + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/health/package.html ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/health/package.html b/camel-core/src/main/java/org/apache/camel/health/package.html new file mode 100644 index 0000000..1e30b55 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/health/package.html @@ -0,0 +1,25 @@ +<!-- + ~ 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 ASF 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. + --> +<html> +<head> +</head> +<body> + +Camel Health Check support + +</body> +</html> http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/impl/DefaultCamelContext.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/impl/DefaultCamelContext.java b/camel-core/src/main/java/org/apache/camel/impl/DefaultCamelContext.java index ae70690..b2bf974 100644 --- a/camel-core/src/main/java/org/apache/camel/impl/DefaultCamelContext.java +++ b/camel-core/src/main/java/org/apache/camel/impl/DefaultCamelContext.java @@ -88,9 +88,11 @@ import org.apache.camel.builder.DefaultFluentProducerTemplate; import org.apache.camel.builder.ErrorHandlerBuilder; import org.apache.camel.builder.ErrorHandlerBuilderSupport; import org.apache.camel.component.properties.PropertiesComponent; +import org.apache.camel.health.HealthCheckRegistry; import org.apache.camel.impl.converter.BaseTypeConverterRegistry; import org.apache.camel.impl.converter.DefaultTypeConverter; import org.apache.camel.impl.converter.LazyLoadingTypeConverter; +import org.apache.camel.impl.health.DefaultHealthCheckRegistry; import org.apache.camel.impl.transformer.TransformerKey; import org.apache.camel.impl.validator.ValidatorKey; import org.apache.camel.management.DefaultManagementMBeanAssembler; @@ -312,6 +314,7 @@ public class DefaultCamelContext extends ServiceSupport implements ModelCamelCon private SSLContextParameters sslContextParameters; private final ThreadLocal<Set<String>> componentsInCreation = ThreadLocal.withInitial(HashSet::new); private RouteController routeController; + private HealthCheckRegistry healthCheckRegistry; /** * Creates the {@link CamelContext} using {@link JndiRegistry} as registry, @@ -352,6 +355,9 @@ public class DefaultCamelContext extends ServiceSupport implements ModelCamelCon // Route controller this.routeController = new DefaultRouteController(this); + // Health check registry + this.healthCheckRegistry = new DefaultHealthCheckRegistry(this); + // Call all registered trackers with this context // Note, this may use a partially constructed object CamelContextTrackerRegistry.INSTANCE.contextCreated(this); @@ -4741,4 +4747,15 @@ public class DefaultCamelContext extends ServiceSupport implements ModelCamelCon } } + @Override + public HealthCheckRegistry getHealthCheckRegistry() { + return healthCheckRegistry; + } + + /** + * Sets a {@link HealthCheckRegistry}. + */ + public void setHealthCheckRegistry(HealthCheckRegistry healthCheckRegistry) { + this.healthCheckRegistry = ObjectHelper.notNull(healthCheckRegistry, "HealthCheckRegistry"); + } } http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java b/camel-core/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java new file mode 100644 index 0000000..6ecb284 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java @@ -0,0 +1,235 @@ +/** + * 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 ASF 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.camel.impl.health; + +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.camel.health.HealthCheck; +import org.apache.camel.health.HealthCheckConfiguration; +import org.apache.camel.health.HealthCheckResultBuilder; +import org.apache.camel.util.ObjectHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractHealthCheck implements HealthCheck { + public static final String CHECK_ID = "check.id"; + public static final String CHECK_GROUP = "check.group"; + public static final String CHECK_ENABLED = "check.enabled"; + public static final String INVOCATION_COUNT = "invocation.count"; + public static final String INVOCATION_TIME = "invocation.time"; + public static final String INVOCATION_ATTEMPT_TIME = "invocation.attempt.time"; + public static final String FAILURE_COUNT = "failure.count"; + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHealthCheck.class); + + private final Object lock; + private final String group; + private final String id; + private final ConcurrentMap<String, Object> meta; + + private HealthCheckConfiguration configuration; + private HealthCheck.Result lastResult; + private ZonedDateTime lastInvocation; + + protected AbstractHealthCheck(String id) { + this(null, id, null); + } + + protected AbstractHealthCheck(String group, String id) { + this(group, id, null); + } + + protected AbstractHealthCheck(String group, String id, Map<String, Object> meta) { + this.lock = new Object(); + this.group = group; + this.id = ObjectHelper.notNull(id, "HealthCheck ID"); + this.configuration = new HealthCheckConfiguration(); + this.meta = new ConcurrentHashMap<>(); + + if (meta != null) { + this.meta.putAll(meta); + } + + this.meta.put(CHECK_ID, id); + if (group != null) { + this.meta.putIfAbsent(CHECK_GROUP, group); + } + } + + @Override + public String getId() { + return id; + } + + @Override + public String getGroup() { + return group; + } + + @Override + public Map<String, Object> getMetaData() { + return Collections.unmodifiableMap(this.meta); + } + + @Override + public HealthCheckConfiguration getConfiguration() { + return this.configuration; + } + + public void setConfiguration(HealthCheckConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public Result call() { + return call(Collections.emptyMap()); + } + + @Override + public Result call(Map<String, Object> options) { + synchronized (lock) { + final HealthCheckConfiguration conf = getConfiguration(); + final HealthCheckResultBuilder builder = HealthCheckResultBuilder.on(this); + final ZonedDateTime now = ZonedDateTime.now(); + final boolean enabled = ObjectHelper.supplyIfEmpty(conf.isEnabled(), HealthCheckConfiguration::defaultValueEnabled); + final Duration interval = ObjectHelper.supplyIfEmpty(conf.getInterval(), HealthCheckConfiguration::defaultValueInterval); + final Integer threshold = ObjectHelper.supplyIfEmpty(conf.getFailureThreshold(), HealthCheckConfiguration::defaultValueFailureThreshold); + + // Extract relevant information from meta data. + int invocationCount = (Integer)meta.getOrDefault(INVOCATION_COUNT, 0); + int failureCount = (Integer)meta.getOrDefault(FAILURE_COUNT, 0); + + String invocationTime = now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME); + boolean call = true; + + // Set common meta-data + meta.put(INVOCATION_ATTEMPT_TIME, invocationTime); + + if (!enabled) { + LOGGER.debug("health-check {}/{} won't be invoked as not enabled", getGroup(), getId()); + + builder.message("Disabled"); + builder.detail(CHECK_ENABLED, false); + + return builder.unknown().build(); + } + + // check if the last invocation is far enough to have this check invoked + // again without violating the interval configuration. + if (lastResult != null && lastInvocation != null && !interval.isZero()) { + Duration elapsed = Duration.between(lastInvocation, now); + + if (elapsed.compareTo(interval) < 0) { + LOGGER.debug("health-check {}/{} won't be invoked as interval ({}) is not yet expired (last-invocation={})", + getGroup(), + getId(), + elapsed, + lastInvocation); + + call = false; + } + } + + // Invoke the check. + if (call) { + LOGGER.debug("Invoke health-check {}/{}", getGroup(), getId()); + + doCall(builder, options); + + // State should be set here + ObjectHelper.notNull(builder.state(), "Response State"); + + if (builder.state() == State.DOWN) { + // If the service is un-healthy but the number of time it + // has been consecutively reported in this state is less + // than the threshold configured, mark it as UP. This is + // used to avoid false positive in case of glitches. + if (failureCount++ < threshold) { + LOGGER.debug("Health-check {}/{} has status DOWN but failure count ({}) is less than configured threshold ({})", + getGroup(), + getId(), + failureCount, + threshold); + + builder.up(); + } + } else { + failureCount = 0; + } + + meta.put(INVOCATION_TIME, invocationTime); + meta.put(FAILURE_COUNT, failureCount); + meta.put(INVOCATION_COUNT, ++invocationCount); + + // Copy some of the meta-data bits to the response attributes so the + // response caches the health-check state at the time of the invocation. + builder.detail(INVOCATION_TIME, meta.get(INVOCATION_TIME)); + builder.detail(INVOCATION_COUNT, meta.get(INVOCATION_COUNT)); + builder.detail(FAILURE_COUNT, meta.get(FAILURE_COUNT)); + + // update last invocation time. + lastInvocation = now; + } else if (lastResult != null) { + lastResult.getMessage().ifPresent(builder::message); + lastResult.getError().ifPresent(builder::error); + + builder.state(lastResult.getState()); + builder.details(lastResult.getDetails()); + } + + lastResult = builder.build(); + + return lastResult; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AbstractHealthCheck check = (AbstractHealthCheck) o; + + return id != null ? id.equals(check.id) : check.id == null; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } + + protected final void addMetaData(String key, Object value) { + meta.put(key, value); + } + + /** + * Invoke the health check. + * + * @see {@link HealthCheck#call(Map)} + */ + protected abstract void doCall(HealthCheckResultBuilder builder, Map<String, Object> options); +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/impl/health/ContextHealthCheck.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/impl/health/ContextHealthCheck.java b/camel-core/src/main/java/org/apache/camel/impl/health/ContextHealthCheck.java new file mode 100644 index 0000000..36539f3 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/impl/health/ContextHealthCheck.java @@ -0,0 +1,58 @@ +/** + * 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 ASF 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.camel.impl.health; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.health.HealthCheckResultBuilder; + +public final class ContextHealthCheck extends AbstractHealthCheck implements CamelContextAware { + private CamelContext camelContext; + + public ContextHealthCheck() { + super("camel", "context"); + } + + @Override + public CamelContext getCamelContext() { + return camelContext; + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + protected void doCall(HealthCheckResultBuilder builder, Map<String, Object> options) { + builder.unknown(); + + if (camelContext != null) { + builder.detail("context.name", camelContext.getName()); + builder.detail("context.version", camelContext.getVersion()); + builder.detail("context.status", camelContext.getStatus().name()); + + if (camelContext.getStatus().isStarted()) { + builder.up(); + } else if (camelContext.getStatus().isStopped()) { + builder.down(); + } + } + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/00d1d70b/camel-core/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java b/camel-core/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java new file mode 100644 index 0000000..58e63e0 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java @@ -0,0 +1,150 @@ +/** + * 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 ASF 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.camel.impl.health; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Stream; + +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.health.HealthCheck; +import org.apache.camel.health.HealthCheckRegistry; +import org.apache.camel.health.HealthCheckRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultHealthCheckRegistry implements HealthCheckRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHealthCheckRegistry.class); + + private final Set<HealthCheck> checks; + private final Set<HealthCheckRepository> repositories; + private CamelContext camelContext; + + public DefaultHealthCheckRegistry() { + this(null); + } + + public DefaultHealthCheckRegistry(CamelContext camelContext) { + this.checks = new CopyOnWriteArraySet<>(); + + this.repositories = new CopyOnWriteArraySet<>(); + this.repositories.add(new RegistryRepository()); + this.repositories.addAll(repositories); + + setCamelContext(camelContext); + } + + // ************************************ + // Properties + // ************************************ + + @Override + public final void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + + for (HealthCheck check: checks) { + if (check instanceof CamelContextAware) { + ((CamelContextAware) check).setCamelContext(camelContext); + } + } + + for (HealthCheckRepository repository: repositories) { + if (repository instanceof CamelContextAware) { + ((CamelContextAware) repository).setCamelContext(camelContext); + } + } + } + + @Override + public final CamelContext getCamelContext() { + return camelContext; + } + + @Override + public boolean register(HealthCheck check) { + boolean result = checks.add(check); + if (result) { + if (check instanceof CamelContextAware) { + ((CamelContextAware) check).setCamelContext(camelContext); + } + + LOGGER.debug("HealthCheck with id {} successfully registered", check.getId()); + } + + return result; + } + + @Override + public boolean unregister(HealthCheck check) { + boolean result = checks.remove(check); + if (result) { + LOGGER.debug("HealthCheck with id {} successfully un-registered", check.getId()); + } + + return result; + } + + @Override + public void setRepositories(Collection<HealthCheckRepository> repositories) { + repositories.clear(); + repositories.addAll(repositories); + } + + @Override + public Collection<HealthCheckRepository> getRepositories() { + return Collections.unmodifiableCollection(repositories); + } + + @Override + public boolean addRepository(HealthCheckRepository repository) { + boolean result = repositories.add(repository); + if (result) { + if (repository instanceof CamelContextAware) { + ((CamelContextAware) repository).setCamelContext(getCamelContext()); + + LOGGER.debug("HealthCheckRepository {} successfully registered", repository); + } + } + + return result; + } + + @Override + public boolean removeRepository(HealthCheckRepository repository) { + boolean result = repositories.remove(repository); + if (result) { + LOGGER.debug("HealthCheckRepository with {} successfully un-registered", repository); + } + + return result; + } + + // ************************************ + // + // ************************************ + + @Override + public Stream<HealthCheck> stream() { + return Stream.concat( + checks.stream(), + repositories.stream().flatMap(HealthCheckRepository::stream) + ).distinct(); + } +}