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