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
