This is an automated email from the ASF dual-hosted git repository.
jsinovassinnaik pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/master by this push:
new c5a2030a6 [UNOMI-854] Add a HealthCheck Endpoint (#698)
c5a2030a6 is described below
commit c5a2030a6927890b1aa75b7ae299a5a5b437f5ae
Author: Jérôme Blanchard <[email protected]>
AuthorDate: Thu Oct 17 18:24:49 2024 +0200
[UNOMI-854] Add a HealthCheck Endpoint (#698)
* UNOMI-854: Add a healthcheck extension
* UNOMI-854: Add configuration to healthcheck
* UNOMI-854: Integrate authentication for servlet using JAAS and realm
config
* UNOMI-854: Add documentation (javadoc and adoc) for the healthcheck
extension
* UNOMI-854: Include Integration Test
* UNOMI-854: Change log level to debug for some unrelevant runtime logs,
add timeout default response, add exception details in logs.
* UNOMI-853: Avoid using 2 testsuite because of conflict.
* UNOMI-854: Add a healthcheck extension
* UNOMI-854: Add configuration to healthcheck
* UNOMI-854: Integrate authentication for servlet using JAAS and realm
config
* UNOMI-854: Include Integration Test
* UNOMI-854: Update config capabilities, use code 206 when all checks are
not LIVE, fix order of checks.
* UNOMI-854: Include small value cache to ensure better memory consumption
and avoid DoS.
* UNOMI-854: Missed test suite removal.
---
extensions/healthcheck/README.md | 85 +++++++++
extensions/healthcheck/pom.xml | 141 ++++++++++++++
.../unomi/healthcheck/HealthCheckConfig.java | 95 ++++++++++
.../unomi/healthcheck/HealthCheckProvider.java | 30 +++
.../unomi/healthcheck/HealthCheckResponse.java | 160 ++++++++++++++++
.../unomi/healthcheck/HealthCheckService.java | 147 +++++++++++++++
.../provider/ClusterHealthCheckProvider.java | 109 +++++++++++
.../provider/ElasticSearchHealthCheckProvider.java | 130 +++++++++++++
.../provider/PersistenceHealthCheckProvider.java | 91 +++++++++
.../provider/UnomiBundlesHealthCheckProvider.java | 88 +++++++++
.../servlet/HealthCheckAuthorization.java | 51 +++++
.../servlet/HealthCheckHttpContext.java | 117 ++++++++++++
.../healthcheck/servlet/HealthCheckServlet.java | 79 ++++++++
.../apache/unomi/healthcheck/util/CachedValue.java | 48 +++++
.../resources/org.apache.unomi.healthcheck.cfg | 31 ++++
extensions/pom.xml | 1 +
.../test/java/org/apache/unomi/itests/AllITs.java | 3 +-
.../test/java/org/apache/unomi/itests/BaseIT.java | 15 +-
.../org/apache/unomi/itests/HealthCheckIT.java | 206 +++++++++++++++++++++
kar/src/main/feature/feature.xml | 5 +-
manual/src/main/asciidoc/configuration.adoc | 89 +++++++++
.../main/resources/etc/custom.system.properties | 18 +-
package/src/main/resources/etc/users.properties | 1 +
23 files changed, 1730 insertions(+), 10 deletions(-)
diff --git a/extensions/healthcheck/README.md b/extensions/healthcheck/README.md
new file mode 100644
index 000000000..83a560789
--- /dev/null
+++ b/extensions/healthcheck/README.md
@@ -0,0 +1,85 @@
+/*
+* 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.
+ */
+
+# Health Check Extension
+
+The Health Check extension provides a simple health check endpoint that can be
used to determine if the server is up and running.
+The health check endpoint is available at
+
+```
+/health/check
+```
+and returns a simple JSON response that includes all health check provider
responses.
+
+Basic Http Authentication is enabled by default for the health check endpoint.
The user needs to have the role `health` to access the endpoint. Users and
roles can be configured in the etc/users.properties file. By default a user
health/health is configured.
+
+The healthcheck is available even if unomi is not started. It gives health
information about :
+ - Karaf (as soon as the karaf container is started)
+ - Elasticsearch (connection to elasticsearch cluster and its health)
+ - Unomi (unomi bundles status)
+ - Persistence (unomi to elasticsearch binding)
+ - Cluster health (unomi cluster status and nodes information)
+
+All healthcheck can have a status :
+ - DOWN (service is not available)
+ - UP (service is up but does not respond to request (starting or
misconfigured))
+ - LIVE (service is ready to serve request)
+ - ERROR (an error occurred during service health check)
+
+Any subsystem health check have a timeout of 500ms where check is cancelled
and will be returned as error.
+
+Typical response to /health/check when unomi NOT started is :
+
+```json
+[
+ {
+ "name":"karaf",
+ "status":"LIVE",
+ "collectingTime":0
+ },
+ {
+ "name":"cluster",
+ "status":"DOWN",
+ "collectingTime":0
+ },
+ {
+ "name":"elasticsearch",
+ "status":"LIVE",
+ "collectingTime":6
+ },
+ {
+ "name":"persistence",
+ "status":"DOWN",
+ "collectingTime":0
+ },
+ {
+ "name":"unomi",
+ "status":"DOWN",
+ "collectingTime":0
+ }
+]
+```
+
+## Configuration
+
+Configuration is located in the file etc/org.apache.unomi.healthcheck.cfg
+
+Extension can be disabled by setting the property `enabled` to `false`. An
environment variable can be used to set this property :
UNOMI_HEALTHCHECK_ENABLED
+
+By default, all healthcheck providers are included but the list of those
included providers can be customized by setting the property `providers` with a
comma separated list of provider names. An environment variable can be used to
set this property : UNOMI_HEALTHCHECK_PROVIDERS
+
+The timeout used for each health check can be set by setting the property
`timeout` to the desired value in milliseconds. An environment variable can be
used to set this property : UNOMI_HEALTHCHECK_TIMEOUT
diff --git a/extensions/healthcheck/pom.xml b/extensions/healthcheck/pom.xml
new file mode 100644
index 000000000..aa399e370
--- /dev/null
+++ b/extensions/healthcheck/pom.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-extensions</artifactId>
+ <version>2.6.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>healthcheck</artifactId>
+ <name>Apache Unomi :: Extensions :: HealthCheck</name>
+ <description>Apache Unomi HealthCheck extension that provide liveliness
information about unomi</description>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-api</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-lifecycle-watcher</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>unomi-persistence-spi</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.unomi</groupId>
+ <artifactId>shell-commands</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>osgi.cmpn</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore-osgi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.jaxrs</groupId>
+ <artifactId>jackson-jaxrs-json-provider</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.karaf.jaas</groupId>
+ <artifactId>org.apache.karaf.jaas.boot</artifactId>
+ <version>${karaf.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+
<Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
+ <Import-Package>
+ sun.misc;resolution:=optional,
+ *
+ </Import-Package>
+ <Export-Package>
+
org.apache.unomi.healthcheck;version=${project.version},
+ org.osgi.service.useradmin;version=1.1.0
+ </Export-Package>
+ <_dsannotations>
+ org.apache.unomi.healthcheck.*,
+ org.apache.unomi.healthcheck.provider.*,
+ org.apache.unomi.healthcheck.servlet.*,
+ </_dsannotations>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-artifacts</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attach-artifact</goal>
+ </goals>
+ <configuration>
+ <artifacts>
+ <artifact>
+ <file>
+
src/main/resources/org.apache.unomi.healthcheck.cfg
+ </file>
+ <type>cfg</type>
+ <classifier>healthcheck</classifier>
+ </artifact>
+ </artifacts>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java
new file mode 100644
index 000000000..a36501fda
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java
@@ -0,0 +1,95 @@
+/*
+ * 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.unomi.healthcheck;
+
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Health check configuration.
+ */
+@Component(immediate = true, service = HealthCheckConfig.class,
configurationPid = {"org.apache.unomi.healthcheck"})
+public class HealthCheckConfig {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(HealthCheckConfig.class.getName());
+
+ public static final String CONFIG_ES_ADDRESSES = "esAddresses";
+ public static final String CONFIG_ES_SSL_ENABLED = "esSSLEnabled";
+ public static final String CONFIG_ES_LOGIN = "esLogin";
+ public static final String CONFIG_ES_PASSWORD = "esPassword";
+ public static final String CONFIG_TRUST_ALL_CERTIFICATES =
"httpClient.trustAllCertificates";
+ public static final String CONFIG_AUTH_REALM = "authentication.realm";
+ public static final String ENABLED = "healthcheck.enabled";
+ public static final String PROVIDERS = "healthcheck.providers";
+ public static final String TIMEOUT = "healthcheck.timeout";
+
+ private Map<String, String> config = new HashMap<>();
+ private boolean enabled = true;
+ private List<String> enabledProviders = new ArrayList<>();
+ private int timeout = 400;
+
+ @Activate
+ @Modified
+ public void modified(Map<String, String> config) {
+ LOGGER.info("Updating healthcheck configuration, config size: {}",
config.size());
+ this.setConfig(config);
+ this.setEnabled(config.getOrDefault(ENABLED,
"true").equalsIgnoreCase("true"));
+ this.setEnabledProviders(config.getOrDefault(PROVIDERS, "").isEmpty()
? new ArrayList<>() : List.of(config.get(PROVIDERS).split(",")));
+ this.setTimeout(Integer.parseInt(config.getOrDefault(TIMEOUT, "400")));
+ }
+
+ public String get(String configKey) {
+ return this.config.get(configKey);
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public List<String> getEnabledProviders() {
+ return enabledProviders;
+ }
+
+ public int getTimeout() {
+ return timeout;
+ }
+
+ public void setConfig(Map<String, String> config) {
+ this.config = config;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public void setEnabledProviders(List<String> enabledProviders) {
+ this.enabledProviders = enabledProviders;
+ }
+
+ public void setTimeout(int timeout) {
+ this.timeout = timeout;
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckProvider.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckProvider.java
new file mode 100644
index 000000000..982ea8a5b
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckProvider.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.unomi.healthcheck;
+
+public interface HealthCheckProvider {
+
+ String name();
+
+ HealthCheckResponse execute();
+
+ default HealthCheckResponse timeout() {
+ return new
HealthCheckResponse.Builder().name(name()).withData("error.cause",
"timeout").error().build();
+ }
+
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckResponse.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckResponse.java
new file mode 100644
index 000000000..b4912b2c4
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckResponse.java
@@ -0,0 +1,160 @@
+/*
+ * 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.unomi.healthcheck;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A Health Check response.
+ */
+public class HealthCheckResponse {
+
+ private final String name;
+ private final Status status;
+ private final long collectingTime;
+ private final Map<String, Object> data;
+
+ protected HealthCheckResponse(String name, Status status, long
collectingTime, Map<String, Object> data) {
+ this.name = name;
+ this.status = status;
+ this.collectingTime = collectingTime;
+ this.data = data;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public long getCollectingTime() {
+ return collectingTime;
+ }
+
+ public Map<String, Object> getData() {
+ return data;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static Builder named(String name) {
+ return new Builder().name(name);
+ }
+
+ public static HealthCheckResponse up(String name) {
+ return named(name).up().build();
+ }
+
+ public static HealthCheckResponse live(String name) {
+ return named(name).live().build();
+ }
+
+ public static HealthCheckResponse down(String name) {
+ return named(name).down().build();
+ }
+
+ public static HealthCheckResponse error(String name) {
+ return named(name).error().build();
+ }
+
+ public boolean isLive() {
+ return this.status == Status.LIVE;
+ }
+
+ public boolean isUp() {
+ return this.status == Status.UP;
+ }
+
+ public boolean isDown() {
+ return this.status == Status.DOWN;
+ }
+
+ public boolean isError() {
+ return this.status == Status.ERROR;
+ }
+
+ public static class Builder {
+ private final long borntime;
+ private String name;
+ private HealthCheckResponse.Status status;
+ private final Map<String, Object> data;
+
+ public Builder() {
+ this.borntime = System.currentTimeMillis();
+ this.status = Status.DOWN;
+ this.data = new LinkedHashMap<>();
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder withData(String key, String value) {
+ this.data.put(key, value);
+ return this;
+ }
+
+ public Builder withData(String key, long value) {
+ this.data.put(key, value);
+ return this;
+ }
+
+ public Builder withData(String key, boolean value) {
+ this.data.put(key, value);
+ return this;
+ }
+
+ public Builder up() {
+ this.status = Status.UP;
+ return this;
+ }
+
+ public Builder live() {
+ this.status = Status.LIVE;
+ return this;
+ }
+
+ public Builder down() {
+ this.status = Status.DOWN;
+ return this;
+ }
+
+ public Builder error() {
+ this.status = Status.ERROR;
+ return this;
+ }
+
+ public HealthCheckResponse build() {
+ return new HealthCheckResponse(this.name, this.status,
(System.currentTimeMillis() - borntime), this.data.isEmpty() ? null :
this.data);
+ }
+ }
+
+ public enum Status {
+ DOWN, //Not available
+ UP, //Running or starting
+ LIVE, //Ready to serve requests
+ ERROR //Errors during check
+ }
+
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java
new file mode 100644
index 000000000..54a288063
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckService.java
@@ -0,0 +1,147 @@
+/*
+ * 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.unomi.healthcheck;
+
+import org.apache.unomi.healthcheck.servlet.HealthCheckHttpContext;
+import org.apache.unomi.healthcheck.servlet.HealthCheckServlet;
+import org.osgi.service.component.annotations.*;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+import static org.apache.unomi.healthcheck.HealthCheckConfig.CONFIG_AUTH_REALM;
+
+/**
+ * Health check service that aggregates health checks from multiple providers
and ensure asynchronous execution. The service is
+ * aware of any configuration changes.
+ */
+@Component (service = HealthCheckService.class, immediate = true)
+public class HealthCheckService {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(HealthCheckService.class.getName());
+
+ private final List<HealthCheckProvider> providers = new ArrayList<>();
+ private ExecutorService executor;
+ private boolean busy = false;
+ private boolean registered = false;
+
+ @Reference
+ protected HttpService httpService;
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY, updated =
"updated")
+ private HealthCheckConfig config;
+
+ public HealthCheckService() {
+ LOGGER.info("Building healthcheck service...");
+ }
+
+ public void setConfig(HealthCheckConfig config) {
+ this.config = config;
+ }
+
+ @Activate
+ public void activate() throws ServletException, NamespaceException {
+ if (config.isEnabled()) {
+ LOGGER.info("Activating healthcheck service...");
+ executor = Executors.newSingleThreadExecutor();
+ httpService.registerServlet("/health/check", new
HealthCheckServlet(this), null,
+ new HealthCheckHttpContext(config.get(CONFIG_AUTH_REALM)));
+ this.registered = true;
+ } else {
+ LOGGER.info("Healthcheck service is disabled");
+ }
+ }
+
+ public void updated() throws ServletException, NamespaceException {
+ if (config.isEnabled()) {
+ LOGGER.info("Updating healthcheck service...");
+ if (registered) {
+ httpService.unregister("/health/check");
+ registered = false;
+ }
+ httpService.registerServlet("/health/check", new
HealthCheckServlet(this), null,
+ new HealthCheckHttpContext(config.get(CONFIG_AUTH_REALM)));
+ registered = true;
+ } else {
+ LOGGER.info("Healthcheck service is disabled");
+ }
+ }
+
+ @Deactivate
+ public void deactivate() {
+ LOGGER.info("Deactivating healthcheck service...");
+ if (registered) {
+ httpService.unregister("/health/check");
+ registered = false;
+ }
+ if (executor != null) {
+ executor.shutdown();
+ }
+ }
+
+ @Reference(service = HealthCheckProvider.class, cardinality =
ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC, unbind =
"unbind")
+ protected void bind(HealthCheckProvider provider) {
+ LOGGER.info("Binding provider {}", provider.name());
+ providers.add(provider);
+ }
+
+ protected void unbind(HealthCheckProvider provider) {
+ LOGGER.info("Unbinding provider {}", provider.name());
+ providers.remove(provider);
+ }
+
+ public List<HealthCheckResponse> check() throws RejectedExecutionException
{
+ if (config.isEnabled()) {
+ LOGGER.debug("Health check called");
+ if (busy) {
+ throw new RejectedExecutionException("Health check already in
progress");
+ } else {
+ try {
+ busy = true;
+ List<HealthCheckResponse> health = new ArrayList<>();
+ health.add(HealthCheckResponse.live("karaf"));
+ for (HealthCheckProvider provider :
providers.stream().filter(p ->
config.getEnabledProviders().contains(p.name())).collect(Collectors.toList())) {
+ Future<HealthCheckResponse> future =
executor.submit(provider::execute);
+ try {
+ HealthCheckResponse response =
future.get(config.getTimeout(), TimeUnit.MILLISECONDS);
+ health.add(response);
+ } catch (TimeoutException e) {
+ future.cancel(true);
+ health.add(provider.timeout());
+ } catch (Exception e) {
+ LOGGER.error("Error while executing health check",
e);
+ }
+ }
+ return health;
+ } finally {
+ busy = false;
+ }
+ }
+ } else {
+ LOGGER.info("Healthcheck service is disabled");
+ return Collections.emptyList();
+ }
+ }
+
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ClusterHealthCheckProvider.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ClusterHealthCheckProvider.java
new file mode 100644
index 000000000..26e3628a8
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ClusterHealthCheckProvider.java
@@ -0,0 +1,109 @@
+/*
+ * 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.unomi.healthcheck.provider;
+
+import org.apache.unomi.api.ClusterNode;
+import org.apache.unomi.api.services.ClusterService;
+import org.apache.unomi.healthcheck.HealthCheckResponse;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
+import org.apache.unomi.healthcheck.util.CachedValue;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A Health Check that checks the status of the Unomi cluster service.
+ */
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class ClusterHealthCheckProvider implements HealthCheckProvider {
+
+ public static final String NAME = "cluster";
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ClusterHealthCheckProvider.class.getName());
+ private final CachedValue<HealthCheckResponse> cache = new
CachedValue<>(10, java.util.concurrent.TimeUnit.SECONDS);
+
+ @Reference(service = ClusterService.class, cardinality =
ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, bind = "bind",
unbind = "unbind")
+ private volatile ClusterService service;
+
+ public ClusterHealthCheckProvider() {
+ LOGGER.info("Building cluster health provider service...");
+ }
+
+ public void bind(ClusterService service) {
+ this.service = service;
+ }
+
+ public void unbind(ClusterService service) {
+ this.service = null;
+ }
+
+ @Override public String name() {
+ return NAME;
+ }
+
+ @Override public HealthCheckResponse execute() {
+ LOGGER.debug("Health check cluster");
+ if (cache.isStaled() || cache.getValue().isDown() ||
cache.getValue().isError()) {
+ cache.setValue(refresh());
+ }
+ return cache.getValue();
+ }
+
+ private HealthCheckResponse refresh() {
+ LOGGER.debug("Refresh");
+ HealthCheckResponse.Builder builder = new
HealthCheckResponse.Builder();
+ builder.name(NAME).down();
+ try {
+ if (service != null) {
+ builder.up();
+ List<ClusterNode> nodes = service.getClusterNodes();
+ builder.withData("cluster.size", nodes.size());
+ if (nodes.isEmpty()) {
+ builder.down();
+ }
+ int idx = 1;
+ for (ClusterNode node : nodes) {
+ if (!nodes.isEmpty() || node.isMaster()) {
+ builder.live();
+ }
+ builder.withData("cluster.node." + idx + ".uptime",
node.getUptime());
+ builder.withData("cluster.node." + idx + ".cpuload",
Double.toString(node.getCpuLoad()));
+ builder.withData("cluster.node." + idx + ".loadAverage",
Arrays.stream(node.getLoadAverage()).mapToObj(
+
Double::toString).collect(java.util.stream.Collectors.joining(",")));
+ builder.withData("cluster.node." + idx + ".public",
node.getPublicHostAddress());
+ builder.withData("cluster.node." + idx + ".internal",
node.getInternalHostAddress());
+ if (node.isData() || node.isMaster()) {
+ builder.withData("cluster.node." + idx + ".role",
+ (node.isMaster() ? "master" : "") +
(node.isData() ? "data" : ""));
+ }
+ idx++;
+ }
+ }
+ } catch (Exception e) {
+ builder.error().withData("error", e.getMessage());
+ LOGGER.error("Error checking cluster health", e);
+ }
+ return builder.build();
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
new file mode 100644
index 000000000..361e68df7
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java
@@ -0,0 +1,130 @@
+/*
+ * 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.unomi.healthcheck.provider;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.healthcheck.HealthCheckConfig;
+import org.apache.unomi.healthcheck.HealthCheckResponse;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
+import org.apache.unomi.healthcheck.util.CachedValue;
+import org.apache.unomi.shell.migration.utils.HttpUtils;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Health Check that checks the status of the ElasticSearch connectivity
according to the provided configuration.
+ * This connectivity should be LIVE before any try to start Unomi.
+ */
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class ElasticSearchHealthCheckProvider implements HealthCheckProvider {
+
+ public static final String NAME = "elasticsearch";
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ElasticSearchHealthCheckProvider.class.getName());
+ private final CachedValue<HealthCheckResponse> cache = new
CachedValue<>(10, TimeUnit.SECONDS);
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY)
+ private HealthCheckConfig config;
+
+ private CloseableHttpClient httpClient;
+
+ public ElasticSearchHealthCheckProvider() {
+ LOGGER.info("Building elasticsearch health provider service...");
+ }
+
+ @Activate
+ public void activate() {
+ LOGGER.info("Activating elasticsearch health provider service...");
+ CredentialsProvider credentialsProvider = null;
+ String login = config.get(HealthCheckConfig.CONFIG_ES_LOGIN);
+ if (StringUtils.isNotEmpty(login)) {
+ credentialsProvider = new BasicCredentialsProvider();
+ UsernamePasswordCredentials credentials
+ = new UsernamePasswordCredentials(login,
config.get(HealthCheckConfig.CONFIG_ES_PASSWORD));
+ credentialsProvider.setCredentials(AuthScope.ANY, credentials);
+ }
+ try {
+ httpClient = HttpUtils.initHttpClient(
+
Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_TRUST_ALL_CERTIFICATES)),
credentialsProvider);
+ } catch (IOException e) {
+ LOGGER.error("Unable to initialize http client", e);
+ }
+ }
+
+ public void setConfig(HealthCheckConfig config) {
+ this.config = config;
+ }
+
+ @Override public String name() {
+ return NAME;
+ }
+
+ @Override public HealthCheckResponse execute() {
+ LOGGER.debug("Health check elasticsearch");
+ if (cache.isStaled() || cache.getValue().isDown() ||
cache.getValue().isError()) {
+ cache.setValue(refresh());
+ }
+ return cache.getValue();
+ }
+
+ private HealthCheckResponse refresh() {
+ LOGGER.debug("Refresh");
+ HealthCheckResponse.Builder builder = new
HealthCheckResponse.Builder();
+ builder.name(NAME).down();
+ String url =
(config.get(HealthCheckConfig.CONFIG_ES_SSL_ENABLED).equals("true") ?
"https://" : "http://")
+
.concat(config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim())
+ .concat("/_cluster/health");
+ CloseableHttpResponse response = null;
+ try {
+ response = httpClient.execute(new HttpGet(url));
+ if (response != null && response.getStatusLine().getStatusCode()
== 200) {
+ builder.up();
+ HttpEntity entity = response.getEntity();
+ if (entity != null &&
EntityUtils.toString(entity).contains("\"status\":\"green\"")) {
+ builder.live();
+ //TODO parse and add cluster data
+ }
+ }
+ } catch (IOException e) {
+ builder.error().withData("error", e.getMessage());
+ LOGGER.error("Error while checking elasticsearch health", e);
+ } finally {
+ if (response != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ }
+ }
+ return builder.build();
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
new file mode 100644
index 000000000..b0d2725dc
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/PersistenceHealthCheckProvider.java
@@ -0,0 +1,91 @@
+/*
+ * 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.unomi.healthcheck.provider;
+
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.healthcheck.HealthCheckResponse;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
+import org.apache.unomi.healthcheck.util.CachedValue;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A health check that track the Unomi persistence layer availability. An
evolution would be to check the persistence migration status to
+ * ensure that running instance is aligned with the underlying persistence
migration status and structures.
+ */
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class PersistenceHealthCheckProvider implements HealthCheckProvider {
+
+ public static final String NAME = "persistence";
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(PersistenceHealthCheckProvider.class.getName());
+ private final CachedValue<HealthCheckResponse> cache = new
CachedValue<>(5, TimeUnit.MINUTES);
+
+ @Reference(service = PersistenceService.class, cardinality =
ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, bind = "bind",
unbind = "unbind")
+ private volatile PersistenceService service;
+
+ public PersistenceHealthCheckProvider() {
+ LOGGER.info("Building persistence health provider service...");
+ }
+
+ public void bind(PersistenceService service) {
+ this.service = service;
+ }
+
+ public void unbind(PersistenceService service) {
+ this.service = null;
+ }
+
+ @Override public String name() {
+ return NAME;
+ }
+
+ @Override public HealthCheckResponse execute() {
+ LOGGER.debug("Health check persistence");
+ if (cache.isStaled() || cache.getValue().isDown() ||
cache.getValue().isError()) {
+ cache.setValue(refresh());
+ }
+ return cache.getValue();
+ }
+
+ private HealthCheckResponse refresh() {
+ LOGGER.debug("Refresh value");
+ HealthCheckResponse.Builder builder = new
HealthCheckResponse.Builder();
+ builder.name(NAME).down();
+ try {
+ if (service != null) {
+ builder.up();
+ //TODO replace by the expected persistence version when
migrations steps will be stored in the persistence service
+ if (!service.query("target", "profiles", null,
PropertyType.class).isEmpty()) {
+ builder.live();
+ }
+ }
+ } catch (Exception e) {
+ builder.error().withData("error", e.getMessage());
+ LOGGER.error("Error while checking persistence health", e);
+ }
+ return builder.build();
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/UnomiBundlesHealthCheckProvider.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/UnomiBundlesHealthCheckProvider.java
new file mode 100644
index 000000000..1429330fa
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/UnomiBundlesHealthCheckProvider.java
@@ -0,0 +1,88 @@
+/*
+ * 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.unomi.healthcheck.provider;
+
+import org.apache.unomi.healthcheck.HealthCheckResponse;
+import org.apache.unomi.healthcheck.HealthCheckProvider;
+import org.apache.unomi.healthcheck.util.CachedValue;
+import org.apache.unomi.lifecycle.BundleWatcher;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A health check that track the Unomi bundles availability.
+ */
+@Component(service = HealthCheckProvider.class, immediate = true)
+public class UnomiBundlesHealthCheckProvider implements HealthCheckProvider {
+
+ public static final String NAME = "unomi";
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(UnomiBundlesHealthCheckProvider.class.getName());
+ private final CachedValue<HealthCheckResponse> cache = new
CachedValue<>(10, TimeUnit.SECONDS);
+
+ @Reference(service = BundleWatcher.class, cardinality =
ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC, bind = "bind",
unbind = "unbind")
+ private volatile BundleWatcher service;
+
+ public UnomiBundlesHealthCheckProvider() {
+ LOGGER.info("Building unomi bundles health provider service...");
+ }
+
+ public void bind(BundleWatcher service) {
+ this.service = service;
+ }
+
+ public void unbind(BundleWatcher service) {
+ this.service = null;
+ }
+
+ @Override public String name() {
+ return NAME;
+ }
+
+ @Override public HealthCheckResponse execute() {
+ LOGGER.debug("Health check unomi bundles");
+ if (cache.isStaled() || !cache.getValue().isLive()) {
+ cache.setValue(refresh());
+ }
+ return cache.getValue();
+ }
+
+ private HealthCheckResponse refresh() {
+ LOGGER.debug("Refresh");
+ HealthCheckResponse.Builder builder = new
HealthCheckResponse.Builder();
+ builder.name(NAME).down();
+ try {
+ if (service != null) {
+ builder.up();
+ if (service.isStartupComplete()) {
+ builder.live();
+ }
+ }
+ } catch (Exception e) {
+ builder.error().withData("error", e.getMessage());
+ LOGGER.error("Error while checking unomi bundles health", e);
+ }
+ return builder.build();
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckAuthorization.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckAuthorization.java
new file mode 100644
index 000000000..6a743bd14
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckAuthorization.java
@@ -0,0 +1,51 @@
+/*
+ * 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.unomi.healthcheck.servlet;
+
+import org.osgi.service.useradmin.Authorization;
+
+import java.util.Arrays;
+
+/**
+ * A simple implementation of the {@link Authorization} interface (usually
provided by UserAdmin).
+ */
+public class HealthCheckAuthorization implements Authorization {
+
+ private final String name;
+ private final String[] roles;
+
+ public HealthCheckAuthorization(String name, String[] roles) {
+ this.name = name;
+ this.roles = roles;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean hasRole(String role) {
+ return Arrays.asList(roles).contains(role);
+ }
+
+ @Override
+ public String[] getRoles() {
+ return roles;
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckHttpContext.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckHttpContext.java
new file mode 100644
index 000000000..8e9331a61
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckHttpContext.java
@@ -0,0 +1,117 @@
+/*
+ * 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.unomi.healthcheck.servlet;
+
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+import org.osgi.service.http.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Base64;
+
+/**
+ * A simple implementation of the {@link HttpContext} interface that provides
basic authentication for health checks.
+ */
+public class HealthCheckHttpContext implements HttpContext {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(HealthCheckHttpContext.class.getName());
+
+ private final String realm;
+
+ public HealthCheckHttpContext(String realm) {
+ this.realm = realm;
+ }
+
+ public boolean handleSecurity(HttpServletRequest req, HttpServletResponse
res) throws IOException {
+ if (req.getHeader("Authorization") == null) {
+ LOGGER.debug("No Authorization header found, sending 401");
+ res.addHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return false;
+ }
+ if (authenticated(req)) {
+ LOGGER.debug("User authenticated, allowing access");
+ return true;
+ } else {
+ LOGGER.debug("User not authenticated, sending 401");
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return false;
+ }
+ }
+
+ protected boolean authenticated(HttpServletRequest request) {
+ request.setAttribute(AUTHENTICATION_TYPE,
HttpServletRequest.BASIC_AUTH);
+
+ String authzHeader = request.getHeader("Authorization");
+ String usernameAndPassword = new
String(Base64.getDecoder().decode(authzHeader.substring(6).getBytes()));
+ String[] parts = usernameAndPassword.split(":");
+
+ LOGGER.debug("Authenticating user {}", parts[0]);
+ try {
+ //We use JAAS for authentication and authorization but it could be
done using UserAdmin OSGI service
+ LOGGER.debug("Creating Login Context for realm {}", realm);
+ LoginContext loginContext = new LoginContext(realm, callbacks -> {
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback) {
+ ((NameCallback) callback).setName(parts[0]);
+ } else if (callback instanceof PasswordCallback) {
+ ((PasswordCallback)
callback).setPassword(parts[1].toCharArray());
+ } else {
+ throw new UnsupportedCallbackException(callback);
+ }
+ }
+ });
+ LOGGER.debug("Login Context created");
+ loginContext.login();
+ LOGGER.debug("Login Context called");
+ if (loginContext.getSubject() != null) {
+ LOGGER.debug("User authenticated, subject is not null {}",
loginContext.getSubject());
+ String username =
loginContext.getSubject().getPrincipals(UserPrincipal.class).stream()
+
.map(UserPrincipal::getName).findFirst().orElse("unknown");
+ String[] roles =
loginContext.getSubject().getPrincipals(RolePrincipal.class).stream().map(RolePrincipal::getName)
+ .toArray(String[]::new);
+ LOGGER.debug("User {} authenticated with roles {}", username,
roles);
+ request.setAttribute(REMOTE_USER, username);
+ request.setAttribute(AUTHORIZATION, new
HealthCheckAuthorization(username, roles));
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error while authenticating user", e);
+ }
+ return false;
+ }
+
+ public URL getResource(String s) {
+ return null;
+ }
+
+ public String getMimeType(String s) {
+ return null;
+ }
+
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckServlet.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckServlet.java
new file mode 100644
index 000000000..e687b6c37
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/servlet/HealthCheckServlet.java
@@ -0,0 +1,79 @@
+/*
+ * 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.unomi.healthcheck.servlet;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.unomi.healthcheck.HealthCheckResponse;
+import org.apache.unomi.healthcheck.HealthCheckService;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.useradmin.Authorization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A simple servlet that provides a health check endpoint.
+ */
+public class HealthCheckServlet extends HttpServlet {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(HealthCheckServlet.class.getName());
+
+ private final HealthCheckService service;
+ private final ObjectMapper mapper;
+
+ public HealthCheckServlet(HealthCheckService service) {
+ LOGGER.info("Building healthcheck servlet...");
+ this.service = service;
+ mapper = new ObjectMapper();
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ LOGGER.info("Initializing healthcheck servlet...");
+ super.init(config);
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse
response) throws ServletException, IOException {
+ if (request.getAttribute(HttpContext.AUTHORIZATION) == null ||
+
!((Authorization)request.getAttribute(HttpContext.AUTHORIZATION)).hasRole("health"))
{
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ List<HealthCheckResponse> checks = service.check();
+ checks.sort(Comparator.comparing(HealthCheckResponse::getName));
+ response.getWriter().println(mapper.writeValueAsString(checks));
+ response.setContentType("application/json");
+ response.setHeader("Cache-Control", "no-cache");
+ if (checks.stream().allMatch(HealthCheckResponse::isLive)) {
+ response.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+ }
+ }
+}
diff --git
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/util/CachedValue.java
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/util/CachedValue.java
new file mode 100644
index 000000000..188e852e6
--- /dev/null
+++
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/util/CachedValue.java
@@ -0,0 +1,48 @@
+/*
+ * 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.unomi.healthcheck.util;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A Health Check response.
+ */
+public class CachedValue<T> {
+
+ private long ttl;
+ private long date;
+ private T value;
+
+ public CachedValue(long value, TimeUnit unit) {
+ this.ttl = TimeUnit.MILLISECONDS.convert(value, unit);
+ }
+
+ public boolean isStaled() {
+ return System.currentTimeMillis() - date > ttl;
+ }
+
+ public synchronized void setValue(T value) {
+ this.date = System.currentTimeMillis();
+ this.value = value;
+ }
+
+ public synchronized T getValue() {
+ return isStaled()?null:value;
+ }
+
+}
diff --git
a/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg
b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg
new file mode 100644
index 000000000..710876fe4
--- /dev/null
+++ b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg
@@ -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.
+#
+
+# Elasticsearch configuration
+esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200}
+esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false}
+esLogin = ${org.apache.unomi.elasticsearch.username:-}
+esPassword = ${org.apache.unomi.elasticsearch.password:-}
+httpClient.trustAllCertificates =
${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false}
+
+# Security configuration
+authentication.realm = ${org.apache.unomi.security.realm:-karaf}
+
+# Health check configuration
+healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-true}
+healthcheck.providers =
${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,unomi,persistence}
+healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index e160563cb..fc756fbd4 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -41,6 +41,7 @@
<module>groovy-actions</module>
<module>json-schema</module>
<module>log4j-extension</module>
+ <module>healthcheck</module>
</modules>
</project>
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index 60502de03..f415c3ab9 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -64,7 +64,8 @@ import org.junit.runners.Suite.SuiteClasses;
GraphQLWebSocketIT.class,
JSONSchemaIT.class,
GraphQLProfileAliasesIT.class,
- SendEventActionIT.class
+ SendEventActionIT.class,
+ HealthCheckIT.class,
})
public class AllITs {
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index 27d8d84df..e171cfc53 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -197,7 +197,7 @@ public abstract class BaseIT extends KarafTestSupport {
routerCamelContext = getOsgiService(IRouterCamelContext.class, 600000);
// init httpClient
- httpClient = initHttpClient();
+ httpClient = initHttpClient(getHttpClientCredentialProvider());
}
@After
@@ -555,12 +555,9 @@ public abstract class BaseIT extends KarafTestSupport {
}
}
- public static CloseableHttpClient initHttpClient() {
+ public static CloseableHttpClient initHttpClient(BasicCredentialsProvider
credentialsProvider) {
long requestStartTime = System.currentTimeMillis();
- BasicCredentialsProvider credsProvider = null;
- credsProvider = new BasicCredentialsProvider();
- credsProvider.setCredentials(AuthScope.ANY, new
UsernamePasswordCredentials(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
- HttpClientBuilder httpClientBuilder =
HttpClients.custom().useSystemProperties().setDefaultCredentialsProvider(credsProvider);
+ HttpClientBuilder httpClientBuilder =
HttpClients.custom().useSystemProperties().setDefaultCredentialsProvider(credentialsProvider);
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
@@ -613,4 +610,10 @@ public abstract class BaseIT extends KarafTestSupport {
LOGGER.error("Could not close httpClient: " + httpClient, e);
}
}
+
+ public BasicCredentialsProvider getHttpClientCredentialProvider() {
+ BasicCredentialsProvider credsProvider = new
BasicCredentialsProvider();
+ credsProvider.setCredentials(AuthScope.ANY, new
UsernamePasswordCredentials(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
+ return credsProvider;
+ }
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
new file mode 100644
index 000000000..2ef00efa8
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java
@@ -0,0 +1,206 @@
+/*
+ * 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.unomi.itests;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
+import org.apache.commons.io.IOUtils;
+import org.apache.cxf.interceptor.security.AccessDeniedException;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.karaf.itests.KarafTestSupport;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.lifecycle.BundleWatcher;
+import org.apache.unomi.persistence.spi.PersistenceService;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.CoreOptions;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel;
+import org.ops4j.pax.exam.options.MavenArtifactUrlReference;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.fail;
+import static org.ops4j.pax.exam.CoreOptions.systemProperty;
+import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.*;
+
+/**
+ * Health Check Integration Tests
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class HealthCheckIT extends BaseIT {
+
+ private final static Logger LOGGER =
LoggerFactory.getLogger(HealthCheckIT.class);
+
+ protected static final String HEALTHCHECK_AUTH_USER_NAME = "health";
+ protected static final String HEALTHCHECK_AUTH_PASSWORD = "health";
+ protected static final String HEALTHCHECK_ENDPOINT = "/health/check";
+
+ @Test
+ public void testHealthCheck() {
+ try {
+ List<HealthCheckResponse> response = get(HEALTHCHECK_ENDPOINT, new
TypeReference<>() {});
+ LOGGER.info("health check response: {}", response);
+ Assert.assertEquals(5, response.size());
+ Assert.assertTrue(response.stream().anyMatch(r ->
r.getName().equals("karaf") && r.getStatus() ==
HealthCheckResponse.Status.LIVE));
+ Assert.assertTrue(response.stream().anyMatch(r ->
r.getName().equals("elasticsearch") && r.getStatus() ==
HealthCheckResponse.Status.LIVE));
+ Assert.assertTrue(response.stream().anyMatch(r ->
r.getName().equals("unomi") && r.getStatus() ==
HealthCheckResponse.Status.LIVE));
+ Assert.assertTrue(response.stream().anyMatch(r ->
r.getName().equals("cluster") && r.getStatus() ==
HealthCheckResponse.Status.LIVE));
+ Assert.assertTrue(response.stream().anyMatch(r ->
r.getName().equals("persistence") && r.getStatus() ==
HealthCheckResponse.Status.LIVE));
+ } catch (Exception e) {
+ LOGGER.error("Error while executing health check", e);
+ fail("Error while executing health check" + e.getMessage());
+ }
+ }
+
+ protected <T> T get(final String url, TypeReference<T> typeReference) {
+ CloseableHttpResponse response = null;
+ try {
+ final HttpGet httpGet = new HttpGet(getFullUrl(url));
+ response = executeHttpRequest(httpGet);
+ if (response.getStatusLine().getStatusCode() == 200) {
+ return
objectMapper.readValue(response.getEntity().getContent(), typeReference);
+ } else {
+ return null;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (response != null) {
+ try {
+ response.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return null;
+ }
+
+ public BasicCredentialsProvider getHttpClientCredentialProvider() {
+ BasicCredentialsProvider credsProvider = new
BasicCredentialsProvider();
+ credsProvider.setCredentials(AuthScope.ANY, new
UsernamePasswordCredentials(HEALTHCHECK_AUTH_USER_NAME,
HEALTHCHECK_AUTH_PASSWORD));
+ return credsProvider;
+ }
+
+ public static class HealthCheckResponse {
+ private String name;
+ private Status status;
+ private long collectingTime;
+ private Map<String, Object> data;
+
+ public HealthCheckResponse() {
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public void setStatus(Status status) {
+ this.status = status;
+ }
+
+ public long getCollectingTime() {
+ return collectingTime;
+ }
+
+ public void setCollectingTime(long collectingTime) {
+ this.collectingTime = collectingTime;
+ }
+
+ public Map<String, Object> getData() {
+ return data;
+ }
+
+ public void setData(Map<String, Object> data) {
+ this.data = data;
+ }
+
+ @Override public String toString() {
+ return "HealthCheckResponse{" + "name='" + name + '\'' + ",
status=" + status + ", collectingTime=" + collectingTime + ", data="
+ + data + '}';
+ }
+
+ public enum Status {
+ DOWN, //Not available
+ UP, //Running or starting
+ LIVE, //Ready to serve requests
+ ERROR //Errors during check
+ }
+ }
+}
diff --git a/kar/src/main/feature/feature.xml b/kar/src/main/feature/feature.xml
index e16b43e0e..9de01623a 100644
--- a/kar/src/main/feature/feature.xml
+++ b/kar/src/main/feature/feature.xml
@@ -21,8 +21,7 @@
<repository>mvn:org.apache.cxf.karaf/apache-cxf/${cxf.version}/xml/features</repository>
<repository>mvn:org.apache.karaf.cellar/apache-karaf-cellar/${version.karaf.cellar}/xml/features</repository>
- <feature description="unomi-kar" version="${project.version}"
name="unomi-kar"
- start-level="70">
+ <feature description="unomi-kar" version="${project.version}"
name="unomi-kar" start-level="70">
<feature prerequisite="true">wrap</feature>
<feature prerequisite="true">aries-blueprint</feature>
<feature prerequisite="true">war</feature>
@@ -93,6 +92,8 @@
<configfile
finalname="/etc/org.apache.unomi.migration.cfg">mvn:org.apache.unomi/shell-commands/${project.version}/cfg/migration</configfile>
<bundle
start-level="99">mvn:org.apache.unomi/shell-commands/${project.version}</bundle>
+ <configfile
finalname="/etc/org.apache.unomi.healthcheck.cfg">mvn:org.apache.unomi/healthcheck/${project.version}/cfg/healthcheck</configfile>
+ <bundle
start-level="99">mvn:org.apache.unomi/healthcheck/${project.version}</bundle>
</feature>
<feature name="unomi-documentation" description="Documentation of Unomi in
HTML" version="${project.version}">
diff --git a/manual/src/main/asciidoc/configuration.adoc
b/manual/src/main/asciidoc/configuration.adoc
index b2d141ed3..56eb256b1 100644
--- a/manual/src/main/asciidoc/configuration.adoc
+++ b/manual/src/main/asciidoc/configuration.adoc
@@ -849,3 +849,92 @@ The following permissions are required by Unomi:
- required cluster privileges: `manage` OR `all`
- required index privileges on unomi indices: `write, manage, read` OR `all`
+
+=== Health Check Extension
+
+The Health Check extension provides a way to check is required Unomi
components are 'live'.
+
+It consists in a simple http endpoint that provide a JSON view of integrated
health checks. It can then be used to determine if the server
+is up and running and can serve requests.
+
+The health check endpoint is available at the following URL: /health/check and
returns a simple JSON response that includes all health check provider
responses.
+
+Basic Http Authentication is enabled by default for the health check endpoint
using the existing karaf realm. The user needs to have the specific role
**health**
+to access the endpoint. Users and roles can be configured in the
etc/users.properties file. By default, a login/pass health/health is configured.
+
+Specific configuration is located in : org.apache.unomi.healthcheck.cfg
Existing health checks are using configuration from that file, including
authentication realm.
+
+Existing health checks gives information about :
+- Karaf (as soon as the karaf container is started, that check is LIVE)
+- Elasticsearch (connection to elasticsearch cluster and its health)
+- Unomi (unomi bundles status)
+- Persistence (unomi to elasticsearch binding)
+- Cluster health (unomi cluster status and nodes information)
+
+All healthcheck can have a status :
+- DOWN (service is not available)
+- UP (service is up but does not respond to request (starting or
misconfigured))
+- LIVE (service is ready to serve request)
+- ERROR (an error occurred during service health check)
+
+Any subsystem health check have a timeout of 400ms where check is cancelled
and will be returned as error.
+
+Typical response to /health/check when unomi NOT started is :
+
+[source,json]
+----
+[
+ {
+ "name":"karaf",
+ "status":"LIVE",
+ "collectingTime":0
+ },
+ {
+ "name":"cluster",
+ "status":"DOWN",
+ "collectingTime":0
+ },
+ {
+ "name":"elasticsearch",
+ "status":"LIVE",
+ "collectingTime":6
+ },
+ {
+ "name":"persistence",
+ "status":"DOWN",
+ "collectingTime":0
+ },
+ {
+ "name":"unomi",
+ "status":"DOWN",
+ "collectingTime":0
+ }
+]
+----
+
+Existing health check can be extended by adding specific provider in the
dedicated extension. A provider is a class that implements the
HealthCheckProvider interface.
+
+[source,java]
+----
+package org.apache.unomi.healthcheck;
+
+public interface HealthCheckProvider {
+ String name();
+ HealthCheckResponse execute();
+}
+----
+
+Calls to provider are supposed to be done at a regular rate (every 15 seconds
for example) and should be fast to execute. Feel free to include any caching
strategy if needed.
+
+
+==== Configuration
+
+Healthcheck extension configuration is located in the file
etc/org.apache.unomi.healthcheck.cfg
+
+Extension can be disabled by setting the property `enabled` to `false`. An
environment variable can be used to set this property :
UNOMI_HEALTHCHECK_ENABLED.
+You must restart the bundle for that config to take effect.
+
+By default, all healthcheck providers are included but the list of those
included providers can be customized by setting the property `providers` with a
comma separated list of provider names. An environment variable can be used to
set this property : UNOMI_HEALTHCHECK_PROVIDERS.
+Karaf provider is the one needed by healthcheck (always LIVE), it cannot be
ignored.
+
+The timeout used for each health check can be set by setting the property
`timeout` to the desired value in milliseconds. An environment variable can be
used to set this property : UNOMI_HEALTHCHECK_TIMEOUT
diff --git a/package/src/main/resources/etc/custom.system.properties
b/package/src/main/resources/etc/custom.system.properties
index 72f42330e..5f5bf3713 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -443,4 +443,20 @@
org.apache.unomi.graphql.feature.activated=${env:UNOMI_GRAPHQL_FEATURE_ACTIVATED
#######################################################################################################################
## Settings for migration
##
#######################################################################################################################
-org.apache.unomi.migration.recoverFromHistory=${env:UNOMI_MIGRATION_RECOVER_FROM_HISTORY:-true}
\ No newline at end of file
+org.apache.unomi.migration.recoverFromHistory=${env:UNOMI_MIGRATION_RECOVER_FROM_HISTORY:-true}
+
+#######################################################################################################################
+## HealthCheck Settings
##
+#######################################################################################################################
+org.apache.unomi.healthcheck.enabled:${env:UNOMI_HEALTHCHECK_ENABLED:-true}
+org.apache.unomi.healthcheck.password=${env:UNOMI_HEALTHCHECK_PASSWORD:-health}
+#
+# Specify the list of health check providers (name) to use. The list is comma
separated. Other providers will be ignored.
+# As Karaf provider is the one needed by healthcheck (always LIVE), it cannot
be ignored.
+#
+org.apache.unomi.healthcheck.providers:${env:UNOMI_HEALTHCHECK_PROVIDERS:-cluster,elasticsearch,unomi,persistence}
+#
+# Specify the timeout in milliseconds for each healthcheck provider call. The
default value is 400ms.
+# If timeout is raised, the provider is marked in ERROR.
+#
+org.apache.unomi.healthcheck.timeout:${env:UNOMI_HEALTHCHECK_TIMEOUT:-400}
diff --git a/package/src/main/resources/etc/users.properties
b/package/src/main/resources/etc/users.properties
index 3848b1242..ee3acc547 100644
--- a/package/src/main/resources/etc/users.properties
+++ b/package/src/main/resources/etc/users.properties
@@ -30,4 +30,5 @@
# with the name "karaf".
#
karaf = ${org.apache.unomi.security.root.password:-karaf},_g_:admingroup
+health = ${org.apache.unomi.healthcheck.password:-health},health
_g_\:admingroup = group,admin,manager,viewer,systembundles,ssh,ROLE_UNOMI_ADMIN