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

Reply via email to