This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-1007-healthcheck-env-vars
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 1b4da699ce898dfb997ddf1a4f8b0f2e5b45890b
Author: Serge Huber <[email protected]>
AuthorDate: Fri Oct 30 16:53:39 2026 +0100

    UNOMI-1007: Restore health check environment variables
    
    Wire UNOMI_HEALTHCHECK_ENABLED, PROVIDERS and TIMEOUT through 
custom.system.properties
    again, validate values in HealthCheckConfig, and align the docs with the 
real cfg keys.
---
 docker/README.md                                   |   3 +
 extensions/healthcheck/README.md                   |  30 +++---
 .../unomi/healthcheck/HealthCheckConfig.java       | 103 +++++++++++++++++++--
 .../unomi/healthcheck/HealthCheckConfigTest.java   |  69 ++++++++++++++
 .../unomi/healthcheck/HealthCheckServiceTest.java  |  12 +++
 manual/src/main/asciidoc/configuration.adoc        |  26 ++++--
 manual/src/main/asciidoc/whats-new.adoc            |   1 +
 .../main/resources/etc/custom.system.properties    |   6 ++
 8 files changed, 219 insertions(+), 31 deletions(-)

diff --git a/docker/README.md b/docker/README.md
index 2334f3242..72fce9918 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -126,6 +126,9 @@ Note: Linux doesn't support the host.docker.internal DNS 
lookup method yet, it s
 ### Common Variables
 - `UNOMI_ROOT_PASSWORD`: Required admin (`karaf`) password — no known default
 - `UNOMI_HEALTHCHECK_PASSWORD`: Required health-check (`health`) password — no 
known default
+- `UNOMI_HEALTHCHECK_ENABLED`: Register `/health/check` (`true` by default). 
`false` turns the endpoint off; authentication cannot be disabled
+- `UNOMI_HEALTHCHECK_PROVIDERS`: Comma-separated provider list (defaults come 
from the cfg for Elasticsearch or OpenSearch)
+- `UNOMI_HEALTHCHECK_TIMEOUT`: Per-provider timeout in milliseconds (default 
`400`, range 1–60000)
 - `UNOMI_CLUSTER_NODEID`: Optional node id, unique per container. When unset, 
Unomi generates one on the first start and stores it inside the container; set 
this variable to keep the same id when the container is recreated
 - `UNOMI_CLUSTER_NODENAME`: Optional user friendly node name (defaults to the 
container host name); it is the prefix of a generated id
 - `UNOMI_AUTO_START`: Boolean to specify if unomi auto start with karaf 
(defaults to `true`)
diff --git a/extensions/healthcheck/README.md b/extensions/healthcheck/README.md
index 14ae4c62f..399589ef8 100644
--- a/extensions/healthcheck/README.md
+++ b/extensions/healthcheck/README.md
@@ -17,17 +17,17 @@
 
 # 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 
+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.  
+```
+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. The shipped `health` 
user has no default password: its password comes from the 
`UNOMI_HEALTHCHECK_PASSWORD` environment variable, which must be set before 
starting. An empty password is never accepted. 
+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. The shipped `health` 
user has no default password: its password comes from the 
`UNOMI_HEALTHCHECK_PASSWORD` environment variable, which must be set before 
starting. An empty password is never accepted.
 
-The healthcheck is available even if unomi is not started. It gives health 
information about :   
+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 or OpenSearch (connection to ElasticSearch or OpenSearch 
cluster and its health)
   - Unomi (unomi bundles status)
@@ -40,11 +40,11 @@ All healthcheck can have a status :
   - LIVE (service is ready to serve requests)
   - 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.
+Any subsystem health check uses the configured timeout (default 400 ms); a 
check that does not answer in time is cancelled and returned as an error.
 
-Typical response to /health/check when unomi NOT started is : 
+Typical response to /health/check when unomi NOT started is :
 
-```json 
+```json
 [
   {
     "name":"karaf",
@@ -76,10 +76,12 @@ Typical response to /health/check when unomi NOT started is 
:
 
 ## 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
+Configuration is located in the file `etc/org.apache.unomi.healthcheck.cfg` 
(installed by the Elasticsearch or OpenSearch healthcheck feature).
 
-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
+| Property | Environment variable | Default | Notes |
+|---|---|---|---|
+| `healthcheck.enabled` | `UNOMI_HEALTHCHECK_ENABLED` | `true` | `false` 
unregisters the endpoint. Never disables authentication. |
+| `healthcheck.providers` | `UNOMI_HEALTHCHECK_PROVIDERS` | 
distribution-specific (see cfg) | Comma-separated: `cluster`, `elasticsearch` 
or `opensearch`, `unomi`, `persistence`. Unknown names are ignored. `karaf` is 
always reported. |
+| `healthcheck.timeout` | `UNOMI_HEALTHCHECK_TIMEOUT` | `400` | Milliseconds, 
allowed range 1–60000. |
 
-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 
+These environment variables map through `custom.system.properties` to 
`org.apache.unomi.healthcheck.*`, which the cfg file resolves. Invalid values 
fall back to the safe defaults above with a warning.
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
index 19fce3170..5045530ca 100644
--- 
a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java
+++ 
b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java
@@ -25,11 +25,19 @@ import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 /**
- * Health check configuration.
+ * Health check configuration (UNOMI-1007). Values usually come from
+ * {@code etc/org.apache.unomi.healthcheck.cfg}, which resolves
+ * {@code org.apache.unomi.healthcheck.*} system properties — those can be set 
from
+ * {@code UNOMI_HEALTHCHECK_ENABLED}, {@code UNOMI_HEALTHCHECK_PROVIDERS} and
+ * {@code UNOMI_HEALTHCHECK_TIMEOUT}. Invalid values fall back to safe 
defaults with a warning.
+ * None of these settings can turn authentication off.
  */
 @Component(immediate = true, service = HealthCheckConfig.class, 
configurationPid = {"org.apache.unomi.healthcheck"})
 public class HealthCheckConfig {
@@ -53,19 +61,98 @@ public class HealthCheckConfig {
     public static final String PROVIDERS = "healthcheck.providers";
     public static final String TIMEOUT = "healthcheck.timeout";
 
+    /** Default when enabled is missing or invalid. */
+    public static final boolean DEFAULT_ENABLED = true;
+    /** Default provider timeout in milliseconds. */
+    public static final int DEFAULT_TIMEOUT_MS = 400;
+    /** Lowest accepted timeout (milliseconds). */
+    public static final int MIN_TIMEOUT_MS = 1;
+    /** Highest accepted timeout (milliseconds). */
+    public static final int MAX_TIMEOUT_MS = 60_000;
+
+    /**
+     * Names that may appear in {@link #PROVIDERS}. {@code karaf} is listed 
for documentation
+     * compatibility; the service always reports it. Unknown names are dropped 
with a warning.
+     */
+    public static final Set<String> KNOWN_PROVIDERS = Set.of(
+            "karaf", "cluster", "elasticsearch", "opensearch", "unomi", 
"persistence");
+
     private Map<String, String> config = new HashMap<>();
-    private boolean enabled = true;
+    private boolean enabled = DEFAULT_ENABLED;
     private List<String> enabledProviders = new ArrayList<>();
-    private int timeout = 400;
+    private int timeout = DEFAULT_TIMEOUT_MS;
 
     @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")));
+        LOGGER.info("Updating healthcheck configuration, config size: {}", 
config == null ? 0 : config.size());
+        apply(config == null ? Map.of() : config);
+    }
+
+    /**
+     * Applies a configuration map with validation. Package-visible for unit 
tests.
+     *
+     * @param config OSGi configuration entries; may be empty but not {@code 
null}
+     */
+    void apply(Map<String, String> config) {
+        this.setConfig(new HashMap<>(config));
+        this.setEnabled(parseEnabled(config.get(ENABLED)));
+        this.setEnabledProviders(parseProviders(config.get(PROVIDERS)));
+        this.setTimeout(parseTimeout(config.get(TIMEOUT)));
+    }
+
+    static boolean parseEnabled(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return DEFAULT_ENABLED;
+        }
+        String value = raw.trim().toLowerCase(Locale.ROOT);
+        if ("true".equals(value)) {
+            return true;
+        }
+        if ("false".equals(value)) {
+            return false;
+        }
+        LOGGER.warn("Ignoring invalid healthcheck.enabled value '{}'; using 
{}", raw, DEFAULT_ENABLED);
+        return DEFAULT_ENABLED;
+    }
+
+    static List<String> parseProviders(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return new ArrayList<>();
+        }
+        LinkedHashSet<String> providers = new LinkedHashSet<>();
+        for (String part : raw.split(",")) {
+            String name = part.trim();
+            if (name.isEmpty()) {
+                continue;
+            }
+            String key = name.toLowerCase(Locale.ROOT);
+            if (!KNOWN_PROVIDERS.contains(key)) {
+                LOGGER.warn("Ignoring unknown healthcheck provider '{}'", 
name);
+                continue;
+            }
+            providers.add(key);
+        }
+        return new ArrayList<>(providers);
+    }
+
+    static int parseTimeout(String raw) {
+        if (raw == null || raw.trim().isEmpty()) {
+            return DEFAULT_TIMEOUT_MS;
+        }
+        final int parsed;
+        try {
+            parsed = Integer.parseInt(raw.trim());
+        } catch (NumberFormatException e) {
+            LOGGER.warn("Ignoring invalid healthcheck.timeout value '{}'; 
using {}", raw, DEFAULT_TIMEOUT_MS);
+            return DEFAULT_TIMEOUT_MS;
+        }
+        if (parsed < MIN_TIMEOUT_MS || parsed > MAX_TIMEOUT_MS) {
+            LOGGER.warn("Ignoring healthcheck.timeout {} outside {}-{} ms; 
using {}",
+                    parsed, MIN_TIMEOUT_MS, MAX_TIMEOUT_MS, 
DEFAULT_TIMEOUT_MS);
+            return DEFAULT_TIMEOUT_MS;
+        }
+        return parsed;
     }
 
     public String get(String configKey) {
diff --git 
a/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckConfigTest.java
 
b/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckConfigTest.java
new file mode 100644
index 000000000..eb8313b98
--- /dev/null
+++ 
b/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckConfigTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class HealthCheckConfigTest {
+
+    @Test
+    void enabledParsesTrueAndFalse() {
+        assertTrue(HealthCheckConfig.parseEnabled("true"));
+        assertTrue(HealthCheckConfig.parseEnabled("TRUE"));
+        assertFalse(HealthCheckConfig.parseEnabled("false"));
+        assertTrue(HealthCheckConfig.parseEnabled(null));
+        assertTrue(HealthCheckConfig.parseEnabled("maybe"));
+    }
+
+    @Test
+    void providersKeepKnownNamesAndDropUnknown() {
+        assertEquals(List.of("cluster", "elasticsearch", "unomi"),
+                HealthCheckConfig.parseProviders("cluster, elasticsearch, 
unknown, unomi"));
+        assertEquals(List.of(), HealthCheckConfig.parseProviders(""));
+        assertEquals(List.of("karaf", "opensearch"),
+                HealthCheckConfig.parseProviders("karaf,opensearch"));
+    }
+
+    @Test
+    void timeoutRejectsOutOfBoundsAndInvalid() {
+        assertEquals(400, HealthCheckConfig.parseTimeout(null));
+        assertEquals(250, HealthCheckConfig.parseTimeout("250"));
+        assertEquals(400, HealthCheckConfig.parseTimeout("0"));
+        assertEquals(400, HealthCheckConfig.parseTimeout("999999"));
+        assertEquals(400, HealthCheckConfig.parseTimeout("nope"));
+    }
+
+    @Test
+    void applyMapsEnvironmentStyleKeys() {
+        HealthCheckConfig config = new HealthCheckConfig();
+        config.apply(Map.of(
+                HealthCheckConfig.ENABLED, "false",
+                HealthCheckConfig.PROVIDERS, "cluster,bogus,persistence",
+                HealthCheckConfig.TIMEOUT, "1500"));
+
+        assertFalse(config.isEnabled());
+        assertEquals(List.of("cluster", "persistence"), 
config.getEnabledProviders());
+        assertEquals(1500, config.getTimeout());
+    }
+}
diff --git 
a/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckServiceTest.java
 
b/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckServiceTest.java
index c4ebdfc9b..9698d5a29 100644
--- 
a/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckServiceTest.java
+++ 
b/extensions/healthcheck/src/test/java/org/apache/unomi/healthcheck/HealthCheckServiceTest.java
@@ -71,6 +71,18 @@ class HealthCheckServiceTest {
         assertEquals("timeout", blocked.getData().get("error.cause"));
     }
 
+    @Test
+    void disabledServiceReturnsNoResults() throws Exception {
+        HealthCheckConfig config = new HealthCheckConfig();
+        config.setEnabled(false);
+        config.setEnabledProviders(List.of("fast"));
+        config.setTimeout(TIMEOUT);
+        service.setConfig(config);
+        service.bind(new TestProvider("fast", () -> 
HealthCheckResponse.live("fast")));
+
+        assertEquals(List.of(), service.check());
+    }
+
     @Test
     void secondCallWithinOneSecondReturnsTheCachedResults() {
         TestProvider fast = new TestProvider("fast", () -> 
HealthCheckResponse.live("fast"));
diff --git a/manual/src/main/asciidoc/configuration.adoc 
b/manual/src/main/asciidoc/configuration.adoc
index c96978bdd..8220dbefe 100644
--- a/manual/src/main/asciidoc/configuration.adoc
+++ b/manual/src/main/asciidoc/configuration.adoc
@@ -1987,7 +1987,7 @@ Existing health checks give information about:
 * Persistence (Unomi binding to the search/persistence backend)
 * Cluster health (unomi cluster status and nodes information)
 
-Set `UNOMI_HEALTHCHECK_PROVIDERS` (or `providers` in the cfg file) to the 
comma-separated list that matches your deployment, for example 
`karaf,elasticsearch,unomi,persistence,cluster` or 
`karaf,opensearch,unomi,persistence,cluster`.
+Set `UNOMI_HEALTHCHECK_PROVIDERS` (or `healthcheck.providers` in the cfg file) 
to the comma-separated list that matches your deployment, for example 
`cluster,elasticsearch,unomi,persistence` or 
`cluster,opensearch,unomi,persistence`. The `karaf` probe is always reported 
and does not need to be listed.
 
 All healthcheck can have a status :
 - DOWN (service is not available)
@@ -1995,7 +1995,7 @@ All healthcheck can have a status :
 - LIVE (service is ready to serve requests)
 - 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.
+Any subsystem health check uses the configured timeout (default 400 ms); a 
check that does not answer in time is cancelled and returned as an error.
 
 Typical response to /health/check when unomi NOT started is :
 
@@ -2049,10 +2049,18 @@ Calls to provider are supposed to be done at a regular 
rate (every 15 seconds fo
 
 Healthcheck extension configuration is located in the file 
etc/org.apache.unomi.healthcheck.cfg
 
-Extension can be enabled by setting the property `enabled` to `true`. 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
+Extension can be enabled by setting the property `healthcheck.enabled` to 
`true` (default).
+Environment variable: `UNOMI_HEALTHCHECK_ENABLED` (maps to 
`org.apache.unomi.healthcheck.enabled`).
+Setting it to `false` unregisters `/health/check`. It never makes the endpoint 
answer without authentication.
+Invalid values fall back to `true` with a warning.
+
+By default the cfg file lists the providers for the active distribution
+(`cluster,elasticsearch,unomi,persistence` or 
`cluster,opensearch,unomi,persistence`).
+Override with `healthcheck.providers` or `UNOMI_HEALTHCHECK_PROVIDERS`
+(maps to `org.apache.unomi.healthcheck.providers`). Unknown names are ignored 
with a warning.
+The `karaf` probe is always reported as LIVE and does not need to be listed.
+
+The per-provider timeout is `healthcheck.timeout` in milliseconds (default 
`400`, allowed range 1–60000).
+Environment variable: `UNOMI_HEALTHCHECK_TIMEOUT` (maps to 
`org.apache.unomi.healthcheck.timeout`).
+Out-of-range or invalid values fall back to `400` with a warning.
+None of these settings can turn authentication off; a blank health-check 
password is still refused.
diff --git a/manual/src/main/asciidoc/whats-new.adoc 
b/manual/src/main/asciidoc/whats-new.adoc
index a511248d2..df268e6fa 100644
--- a/manual/src/main/asciidoc/whats-new.adoc
+++ b/manual/src/main/asciidoc/whats-new.adoc
@@ -25,6 +25,7 @@ Complete tenant isolation for profiles, events, segments, 
rules, and schemas. Pu
 * Migrating from Unomi 2.x: <<_v2_compatibility_mode,single-tenant 
compatibility mode>> (`singletenantcompatibility.enabled` in 
`org.apache.unomi.rest.authentication.cfg`)
 * Public and GraphQL API key checks no longer read every tenant on each 
request, and refuse revoked and expired keys 
(https://issues.apache.org/jira/browse/UNOMI-1016[UNOMI-1016])
 * JAAS admin requests without `X-Unomi-Tenant-Id` use a configurable default 
tenant (default `default`), so 3.0 admin scripts keep seeing migrated data 
(https://issues.apache.org/jira/browse/UNOMI-1008[UNOMI-1008])
+* Health check environment variables `UNOMI_HEALTHCHECK_ENABLED`, 
`UNOMI_HEALTHCHECK_PROVIDERS` and `UNOMI_HEALTHCHECK_TIMEOUT` work again 
(https://issues.apache.org/jira/browse/UNOMI-1007[UNOMI-1007])
 
 ==== Security hardening (credentials, profile binding, privileged APIs)
 
diff --git a/package/src/main/resources/etc/custom.system.properties 
b/package/src/main/resources/etc/custom.system.properties
index 524aaaf8c..7960fa1ea 100644
--- a/package/src/main/resources/etc/custom.system.properties
+++ b/package/src/main/resources/etc/custom.system.properties
@@ -29,6 +29,12 @@ ${optionals}=unomi.custom.system.properties
 # if you set the matching property another way.
 org.apache.unomi.security.root.password=${env:UNOMI_ROOT_PASSWORD}
 org.apache.unomi.healthcheck.password=${env:UNOMI_HEALTHCHECK_PASSWORD}
+# Health check endpoint (UNOMI-1007). Resolved by 
etc/org.apache.unomi.healthcheck.cfg.
+# Disabling the endpoint never turns authentication off. Empty PROVIDERS keeps 
the cfg default
+# for the active distribution (Elasticsearch or OpenSearch).
+org.apache.unomi.healthcheck.enabled=${env:UNOMI_HEALTHCHECK_ENABLED:-true}
+org.apache.unomi.healthcheck.providers=${env:UNOMI_HEALTHCHECK_PROVIDERS:-}
+org.apache.unomi.healthcheck.timeout=${env:UNOMI_HEALTHCHECK_TIMEOUT:-400}
 
 # These parameters control the list of classes that are allowed or forbidden 
when executing expressions.
 # The forbid list is applied first. No org.mvel2 class is on the allow list, 
so expressions cannot name

Reply via email to