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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new a37f972c0f45 CAMEL-25088: camel-health - Health checks: fix bugs found 
in a deep review (#26989)
a37f972c0f45 is described below

commit a37f972c0f453fe517d98fa1b0c190ae23a8e72f
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 08:11:34 2026 +0200

    CAMEL-25088: camel-health - Health checks: fix bugs found in a deep review 
(#26989)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/health/HealthCheckHelper.java |  33 ++-
 .../impl/health/HealthCheckEdgeCasesTest.java      | 265 +++++++++++++++++++++
 .../camel/impl/health/AbstractHealthCheck.java     |  10 +-
 .../health/ConsumersHealthCheckRepository.java     |  23 +-
 .../impl/health/DefaultHealthCheckRegistry.java    |   7 +-
 .../apache/camel/impl/health/RouteHealthCheck.java |   9 +-
 .../impl/health/RoutesHealthCheckRepository.java   |  17 +-
 .../org/apache/camel/main/BaseMainSupport.java     |   7 +-
 .../camel/main/MainHealthCheckDisabledTest.java    |  50 ++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  20 ++
 10 files changed, 416 insertions(+), 25 deletions(-)

diff --git 
a/core/camel-api/src/main/java/org/apache/camel/health/HealthCheckHelper.java 
b/core/camel-api/src/main/java/org/apache/camel/health/HealthCheckHelper.java
index 135cda654051..dc8e3d8fa286 100644
--- 
a/core/camel-api/src/main/java/org/apache/camel/health/HealthCheckHelper.java
+++ 
b/core/camel-api/src/main/java/org/apache/camel/health/HealthCheckHelper.java
@@ -184,11 +184,25 @@ public final class HealthCheckHelper {
                     } else {
                         return downs;
                     }
-                } else {
-                    // all up so grab first
-                    HealthCheck.Result up = result.iterator().next();
-                    return Collections.singleton(up);
                 }
+                // there may be results that are not up (such as unknown) 
which must be included, so readiness
+                // gives the same result regardless of the exposure level (a 
disabled check does not count)
+                Collection<HealthCheck.Result> notUps = result.stream()
+                        .filter(r -> 
!r.getState().equals(HealthCheck.State.UP) && !isDisabled(r))
+                        .collect(Collectors.toCollection(ArrayList::new));
+                if (!notUps.isEmpty()) {
+                    if ("oneline".equals(exposureLevel)) {
+                        return Collections.singleton(notUps.iterator().next());
+                    } else {
+                        return notUps;
+                    }
+                }
+                // all up so grab first (that is not disabled)
+                return result.stream()
+                        .filter(r -> !isDisabled(r))
+                        .findFirst()
+                        .map(Collections::singleton)
+                        .orElse(Collections.emptySet());
             }
         }
 
@@ -341,8 +355,8 @@ public final class HealthCheckHelper {
         Objects.requireNonNull(results, "results");
         boolean up;
         if (readiness) {
-            // readiness requires that all are UP
-            up = results.stream().allMatch(r -> 
r.getState().equals(HealthCheck.State.UP));
+            // readiness requires that all are UP (a disabled check does not 
count)
+            up = results.stream().filter(r -> !isDisabled(r)).allMatch(r -> 
r.getState().equals(HealthCheck.State.UP));
         } else {
             // liveness will fail if there is any down
             up = results.stream().noneMatch(r -> 
r.getState().equals(HealthCheck.State.DOWN));
@@ -350,6 +364,13 @@ public final class HealthCheckHelper {
         return up;
     }
 
+    /**
+     * Whether the result is of a check that is disabled (which does not 
influence the outcome).
+     */
+    private static boolean isDisabled(HealthCheck.Result result) {
+        return 
Boolean.FALSE.equals(result.getDetails().get(HealthCheck.CHECK_ENABLED));
+    }
+
     /**
      * Get the group of the given check or an empty string if the group is not 
set.
      *
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/health/HealthCheckEdgeCasesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/health/HealthCheckEdgeCasesTest.java
new file mode 100644
index 000000000000..d26d0f51bdb5
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/health/HealthCheckEdgeCasesTest.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.impl.health;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Ordered;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.health.HealthCheck;
+import org.apache.camel.health.HealthCheckHelper;
+import org.apache.camel.health.HealthCheckRegistry;
+import org.apache.camel.health.HealthCheckResultBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class HealthCheckEdgeCasesTest {
+
+    private static class MyCheck extends AbstractHealthCheck {
+        private final RuntimeException error;
+
+        MyCheck(String id, RuntimeException error) {
+            super("my", id);
+            this.error = error;
+        }
+
+        @Override
+        protected void doCall(HealthCheckResultBuilder builder, Map<String, 
Object> options) {
+            if (error != null) {
+                throw error;
+            }
+            builder.up();
+        }
+    }
+
+    private static CamelContext createContext(HealthCheckRegistry registry) 
throws Exception {
+        CamelContext context = new DefaultCamelContext();
+        registry.setCamelContext(context);
+        
context.getCamelContextExtension().addContextPlugin(HealthCheckRegistry.class, 
registry);
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:foo").routeId("foo").to("mock:foo");
+            }
+        });
+        return context;
+    }
+
+    @Test
+    public void testExposureLevelDoesNotChangeReadiness() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        // an up check that comes first (such as the context check)
+        registry.register(new MyCheck("first", null) {
+            @Override
+            public int getOrder() {
+                return Ordered.HIGHEST;
+            }
+        });
+        registry.register(new MyCheck("unknown", null) {
+            @Override
+            protected void doCall(HealthCheckResultBuilder builder, 
Map<String, Object> options) {
+                builder.unknown();
+            }
+        });
+
+        Collection<HealthCheck.Result> full = 
HealthCheckHelper.invokeReadiness(context, "full");
+        Collection<HealthCheck.Result> def = 
HealthCheckHelper.invokeReadiness(context, "default");
+        Collection<HealthCheck.Result> oneline = 
HealthCheckHelper.invokeReadiness(context, "oneline");
+        assertFalse(HealthCheckHelper.isResultsUp(full, true));
+        assertFalse(HealthCheckHelper.isResultsUp(def, true));
+        assertFalse(HealthCheckHelper.isResultsUp(oneline, true));
+
+        context.stop();
+    }
+
+    @Test
+    public void testDisabledCheckDoesNotChangeReadiness() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        // a disabled check that comes first
+        MyCheck check = new MyCheck("disabled", null) {
+            @Override
+            public int getOrder() {
+                return Ordered.HIGHEST;
+            }
+        };
+        check.setEnabled(false);
+        registry.register(check);
+        registry.register(new MyCheck("up", null));
+
+        for (String level : new String[] { "full", "default", "oneline" }) {
+            Collection<HealthCheck.Result> results = 
HealthCheckHelper.invokeReadiness(context, level);
+            assertTrue(HealthCheckHelper.isResultsUp(results, true), level);
+            if (!"full".equals(level)) {
+                assertTrue(results.stream().noneMatch(r -> 
r.getCheck().getId().equals("disabled")), level);
+            }
+        }
+
+        context.stop();
+    }
+
+    @Test
+    public void testThrowingCheckIsDown() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        registry.register(new MyCheck("boom", new 
IllegalStateException("boom")));
+
+        Collection<HealthCheck.Result> results = 
HealthCheckHelper.invokeReadiness(context, "full");
+        HealthCheck.Result result = results.stream().filter(r -> 
"boom".equals(r.getCheck().getId())).findFirst().get();
+        assertEquals(HealthCheck.State.DOWN, result.getState());
+        assertTrue(result.getError().isPresent());
+
+        context.stop();
+    }
+
+    @Test
+    public void testRegisterCheckWithSameNameAsRoute() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        assertTrue(registry.register(new MyCheck("foo", null)));
+        assertTrue(registry.register(new MyCheck("x-health-check", null)));
+        assertTrue(registry.register(new MyCheck("x", null)));
+        assertFalse(registry.register(new MyCheck("x", null)));
+
+        context.stop();
+    }
+
+    @Test
+    public void testExcludePatternWithSpaces() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        registry.setExcludePattern("foo, bar");
+        context.start();
+
+        assertTrue(registry.isExcluded(new MyCheck("bar", null)));
+        assertTrue(registry.isExcluded(new MyCheck("foo", null)));
+
+        context.stop();
+    }
+
+    @Test
+    public void testRouteCheckOfRemovedRoute() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        Route route = context.getRoute("foo");
+        RouteHealthCheck check = new RouteHealthCheck(route);
+        context.getRouteController().stopRoute("foo");
+        context.removeRoute("foo");
+
+        HealthCheckResultBuilder builder = HealthCheckResultBuilder.on(check);
+        check.doCall(builder, Collections.emptyMap());
+        assertEquals(HealthCheck.State.UNKNOWN, builder.build().getState());
+
+        context.stop();
+    }
+
+    @Test
+    public void testSuspendedRouteIsDown() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        registry.setInitialState(HealthCheck.State.UP);
+        CamelContext context = createContext(registry);
+        context.start();
+
+        context.getRouteController().suspendRoute("foo");
+        RouteHealthCheck check = new RouteHealthCheck(context.getRoute("foo"));
+        check.setCamelContext(context);
+        assertEquals(HealthCheck.State.DOWN, check.call().getState());
+
+        context.stop();
+    }
+
+    @Test
+    public void testRemovedRoutesAreNotKept() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        RoutesHealthCheckRepository routes = new RoutesHealthCheckRepository();
+        routes.setCamelContext(context);
+        ConsumersHealthCheckRepository consumers = new 
ConsumersHealthCheckRepository();
+        consumers.setCamelContext(context);
+
+        for (int i = 0; i < 5; i++) {
+            String id = "r" + i;
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:" + id).routeId(id).to("mock:" + id);
+                }
+            });
+            routes.stream().forEach(c -> c.call());
+            consumers.stream().forEach(c -> c.call());
+            context.getRouteController().stopRoute(id);
+            context.removeRoute(id);
+        }
+        routes.stream().forEach(c -> c.call());
+        consumers.stream().forEach(c -> c.call());
+
+        assertEquals(1, checksOf(routes).size());
+        assertEquals(1, checksOf(consumers).size());
+
+        context.stop();
+    }
+
+    @Test
+    public void testRestartedRouteConsumersAreNotKept() throws Exception {
+        DefaultHealthCheckRegistry registry = new DefaultHealthCheckRegistry();
+        CamelContext context = createContext(registry);
+        context.start();
+
+        ConsumersHealthCheckRepository consumers = new 
ConsumersHealthCheckRepository();
+        consumers.setCamelContext(context);
+
+        // a restarted route has a new consumer
+        for (int i = 0; i < 5; i++) {
+            consumers.stream().forEach(c -> c.call());
+            context.getRouteController().stopRoute("foo");
+            context.getRouteController().startRoute("foo");
+        }
+        consumers.stream().forEach(c -> c.call());
+
+        assertEquals(1, checksOf(consumers).size());
+
+        context.stop();
+    }
+
+    private static Map<?, ?> checksOf(Object repository) throws Exception {
+        Field field = repository.getClass().getDeclaredField("checks");
+        field.setAccessible(true);
+        return (Map<?, ?>) field.get(repository);
+    }
+}
diff --git 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java
 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java
index 63548839454f..36c155baa492 100644
--- 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java
+++ 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/AbstractHealthCheck.java
@@ -176,7 +176,15 @@ public abstract class AbstractHealthCheck implements 
HealthCheck, CamelContextAw
         }
 
         LOG.debug("Invoke health-check ({}) {}/{}", kind, getGroup(), getId());
-        doCall(builder, options);
+        try {
+            doCall(builder, options);
+        } catch (Exception e) {
+            // a failing check is down (and must not fail the other checks)
+            LOG.debug("health-check ({}) {}/{} failed due to: {}", kind, 
getGroup(), getId(), e.getMessage(), e);
+            builder.down();
+            builder.error(e);
+            builder.message("Health check failed due to: " + e.getMessage());
+        }
 
         if (builder.state() == null) {
             builder.unknown();
diff --git 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumersHealthCheckRepository.java
 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumersHealthCheckRepository.java
index b4244d23c357..624b502e5423 100644
--- 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumersHealthCheckRepository.java
+++ 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumersHealthCheckRepository.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.impl.health;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Stream;
@@ -85,12 +88,20 @@ public class ConsumersHealthCheckRepository extends 
ServiceSupport
         //     void forEachRoute(Consumer<Route> consumer);
         // }
         //
-        return this.context != null && enabled
-                ? this.context.getRoutes()
-                        .stream()
-                        .filter(route -> route.getId() != null)
-                        .map(this::toConsumerHealthCheck)
-                : Stream.empty();
+        if (this.context == null || !enabled) {
+            return Stream.empty();
+        }
+        List<Route> routes = this.context.getRoutes();
+        // remove the checks of the routes that have been removed
+        Set<Consumer> consumers = new HashSet<>();
+        for (Route route : routes) {
+            consumers.add(route.getConsumer());
+        }
+        checks.keySet().retainAll(consumers);
+        return routes
+                .stream()
+                .filter(route -> route.getId() != null)
+                .map(this::toConsumerHealthCheck);
     }
 
     // *****************************
diff --git 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java
 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java
index e8d36777c1ba..91aa2697c432 100644
--- 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java
+++ 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/DefaultHealthCheckRegistry.java
@@ -206,8 +206,8 @@ public class DefaultHealthCheckRegistry extends 
ServiceSupport implements Health
         CamelContextAware.trySetCamelContext(obj, camelContext);
 
         if (obj instanceof HealthCheck healthCheck) {
-            // do we have this already
-            if (getCheck(healthCheck.getId()).isPresent()) {
+            // do we have this already (with the exact same id)
+            if (checks.stream().anyMatch(c -> 
c.getId().equals(healthCheck.getId()))) {
                 return false;
             }
             result = checks.add(healthCheck);
@@ -307,7 +307,8 @@ public class DefaultHealthCheckRegistry extends 
ServiceSupport implements Health
     @Override
     public boolean isExcluded(HealthCheck healthCheck) {
         if (excludePattern != null) {
-            String[] s = excludePattern.split(",");
+            // allow spaces around the comma (such as foo, bar)
+            String[] s = excludePattern.trim().split("\\s*,\\s*");
 
             String id = healthCheck.getId();
             if (PatternHelper.matchPatterns(id, s)) {
diff --git 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/RouteHealthCheck.java
 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/RouteHealthCheck.java
index df01bf5ef7e3..82eabef8b9bf 100644
--- 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/RouteHealthCheck.java
+++ 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/RouteHealthCheck.java
@@ -46,12 +46,19 @@ public class RouteHealthCheck extends AbstractHealthCheck {
             final ServiceStatus status = 
context.getRouteController().getRouteStatus(route.getId());
 
             builder.detail("route.id", route.getId());
+            if (status == null) {
+                // the route has been removed
+                builder.unknown();
+                builder.message(String.format("Route %s is removed", 
route.getId()));
+                return;
+            }
             builder.detail("route.status", status.name());
 
             if (route.getRouteController() != null || route.isAutoStartup()) {
                 if (status.isStarted()) {
                     builder.up();
-                } else if (status.isStopped()) {
+                } else {
+                    // stopped, suspended, starting or stopping
                     builder.down();
                     builder.message(String.format("Route %s has status %s", 
route.getId(), status.name()));
                 }
diff --git 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/RoutesHealthCheckRepository.java
 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/RoutesHealthCheckRepository.java
index f7cd2fb7ef83..1ccc76f01bd1 100644
--- 
a/core/camel-health/src/main/java/org/apache/camel/impl/health/RoutesHealthCheckRepository.java
+++ 
b/core/camel-health/src/main/java/org/apache/camel/impl/health/RoutesHealthCheckRepository.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.impl.health;
 
+import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.stream.Stream;
@@ -84,12 +85,16 @@ public class RoutesHealthCheckRepository extends 
ServiceSupport
         //     void forEachRoute(Consumer<Route> consumer);
         // }
         //
-        return this.context != null && enabled
-                ? this.context.getRoutes()
-                        .stream()
-                        .filter(route -> route.getId() != null)
-                        .map(this::toRouteHealthCheck)
-                : Stream.empty();
+        if (this.context == null || !enabled) {
+            return Stream.empty();
+        }
+        List<Route> routes = this.context.getRoutes();
+        // remove the checks of the routes that have been removed
+        checks.keySet().retainAll(routes);
+        return routes
+                .stream()
+                .filter(route -> route.getId() != null)
+                .map(this::toRouteHealthCheck);
     }
 
     // *****************************
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java 
b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
index 9af1ceb78a3e..7a094b567152 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
@@ -1891,13 +1891,16 @@ public abstract class BaseMainSupport extends 
BaseService {
         setPropertiesOnTarget(camelContext, health, healthCheckProperties, 
"camel.health.",
                 mainConfigurationProperties.isAutoConfigurationFailFast(), 
true, autoConfiguredProperties);
 
+        // auto-detect camel-health on classpath
+        HealthCheckRegistry hcr = 
camelContext.getCamelContextExtension().getContextPlugin(HealthCheckRegistry.class);
         if (health.getEnabled() != null && !health.getEnabled()) {
             // health-check is disabled
+            if (hcr != null) {
+                hcr.setEnabled(false);
+            }
             return;
         }
 
-        // auto-detect camel-health on classpath
-        HealthCheckRegistry hcr = 
camelContext.getCamelContextExtension().getContextPlugin(HealthCheckRegistry.class);
         if (hcr == null) {
             if (health.getEnabled() != null && health.getEnabled()) {
                 LOG.warn("Cannot find HealthCheckRegistry from classpath. Add 
camel-health to classpath.");
diff --git 
a/core/camel-main/src/test/java/org/apache/camel/main/MainHealthCheckDisabledTest.java
 
b/core/camel-main/src/test/java/org/apache/camel/main/MainHealthCheckDisabledTest.java
new file mode 100644
index 000000000000..72a323950eb3
--- /dev/null
+++ 
b/core/camel-main/src/test/java/org/apache/camel/main/MainHealthCheckDisabledTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.main;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.health.HealthCheckRegistry;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class MainHealthCheckDisabledTest {
+
+    @Test
+    public void testMainHealthCheckDisabled() {
+        Main main = new Main();
+        main.configure().addRoutesBuilder(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("foo").to("log:foo");
+            }
+        });
+        main.addInitialProperty("camel.health.enabled", "false");
+
+        main.start();
+        try {
+            CamelContext camelContext = main.getCamelContext();
+            HealthCheckRegistry registry = 
camelContext.getCamelContextExtension().getContextPlugin(HealthCheckRegistry.class);
+            assertNotNull(registry);
+            assertFalse(registry.isEnabled());
+        } finally {
+            main.stop();
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index b705cfa69400..c679b2715009 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -3614,6 +3614,26 @@ producer were never stopped.
 With `parallelProcessing`, a recipient whose task had not started yet when the 
Recipient List completed is now
 skipped instead of being sent to afterwards. As before, recipients that had 
already started keep running.
 
+=== camel-health - readiness is the same for every exposure level
+
+The readiness of Camel is now the same regardless of the health check exposure 
level (`full`, `default` or `oneline`).
+Previously with the `default` and `oneline` exposure levels, health checks in 
the `UNKNOWN` state were not included in
+the result, and Camel could report ready, while the `full` exposure level 
reported not ready for the same state. These
+health checks are now included, so Camel is not ready in all exposure levels.
+
+A health check that is disabled (such as a consumer health check turned off 
with `healthCheckConsumerEnabled=false`)
+no longer influences the readiness, in any exposure level. Previously a 
disabled health check made Camel not ready
+with the `full` exposure level.
+
+A route health check now reports `DOWN` when the route is suspended (or is 
starting or stopping), the same as when the
+route is stopped. Previously it reported the initial state of the health check 
registry.
+
+A health check that fails with an exception now reports `DOWN` (with the 
exception as the error), instead of failing
+the whole health check.
+
+Setting `camel.health.enabled=false` now disables the health check registry. 
Previously the registry was still
+enabled, and health checks (such as the context health check) were still 
loaded and invoked.
+
 === camel-core - properties component locations
 
 A properties location with an unknown (or misspelled) resolver, such as 
`Classpath:app.properties` or

Reply via email to