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 d65b06d9f962 CAMEL-24851: when a YAML route file does not load, camel
run logs the schema validator's report before the loader's error (#26636)
d65b06d9f962 is described below
commit d65b06d9f962bb27f9f68dcfd0676a054fe5e819
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 21:07:50 2026 +0200
CAMEL-24851: when a YAML route file does not load, camel run logs the
schema validator's report before the loader's error (#26636)
* CAMEL-24851: when a YAML route file does not load, camel run logs the
schema validator's report (what to write) before the loader's error, on a
failed start and on a failed reload in dev mode
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24851: the upgrade note is migration-only and does not split the
runtime paragraphs, the running docs show the report, a Windows path is not a
scheme, no duplicate files, and swallowed exceptions are logged at debug
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
.../support/FileWatcherResourceReloadStrategy.java | 2 +
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 5 +
.../modules/ROOT/pages/camel-jbang-running.adoc | 11 ++
.../apache/camel/dsl/jbang/core/commands/Run.java | 13 ++
.../core/commands/ai/YamlLoadFailureReport.java | 198 +++++++++++++++++++++
.../commands/ai/YamlLoadFailureReportTest.java | 108 +++++++++++
.../java/org/apache/camel/main/KameletMain.java | 19 ++
7 files changed, 356 insertions(+)
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 59b38793aa7a..c0f3e9da8c4a 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -314,6 +314,8 @@ public class FileWatcherResourceReloadStrategy extends
ResourceReloadStrategySup
} catch (Exception e) {
setLastError(e);
incFailedCounter();
+ // the same event a failed context reload
emits, so a listener can act on the file
+
EventHelper.notifyContextReloadFailure(getCamelContext(), name, e);
String msg = e.getMessage();
if (msg.endsWith(".")) {
msg = msg.substring(0, msg.length() - 1);
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 2f627f9065a9..4bef77ec5004 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
@@ -786,6 +786,11 @@ Running an existing Maven project (`camel run pom.xml`) is
unchanged, and still
from the `pom.xml`. The other commands that take `--runtime` (such as `camel
export`,
`camel dependency list` and `camel version list`) accept `jbang` as an alias
for `main`.
+The file watcher reload strategy (camel-support) now emits the
`CamelContextReloadFailure` event when a single
+file fails to reload, with the file name as the event's action; before, only a
failed reload of the whole context
+emitted it. A listener of that event fires for these failures too. `camel run`
uses it to print the report of
+`camel validate yaml` when a YAML route file does not load, at start and on a
reload in `--dev` mode.
+
The YAML that `camel init` writes (`camel init foo.yaml`, and the Integration
and Kamelet templates), the
bundled examples (`camel init --example`) and the routes the
`camel_ai_pipeline_scaffold` and
`camel_openapi_scaffold` MCP tools generate are now written in the canonical
YAML DSL format, with an
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index faf644de04ca..9af930c01171 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -20,6 +20,17 @@ This works for all DSLs (YAML, Java, XML).
NOTE: Live reload is for development purposes. If you encounter JVM class
loading issues,
restart the integration. Java files are not live-reloadable in Spring Boot
runtime.
+When a YAML route file does not load, at start or after a save in dev mode,
`camel run` prints the report of
+`camel validate yaml` for the file before the loader's error, so the message
says what to write:
+
+[source,text]
+----
+The route file did not load. camel validate yaml says what to write:
+ orders.camel.yaml:
+ pollEnrich: property 'uri' is not defined in the schema ... (the endpoint
of pollEnrich is an expression: write pollEnrich: {expression: {constant:
{expression: "file:./order.json"}}})
+ (camel validate yaml <file> for the full report; the loader's error follows)
+----
+
=== Source directory
Use `--source-dir` for more flexibility — Camel watches the entire directory
(including subfolders)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index 37da002093fe..4ba4f527fe11 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -47,6 +47,7 @@ import jdk.jfr.Configuration;
import jdk.jfr.Recording;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.YamlLoadFailureReport;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
@@ -73,6 +74,7 @@ import
org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.main.BaseMainSupport;
import org.apache.camel.main.KameletMain;
+import org.apache.camel.main.MainListenerSupport;
import org.apache.camel.main.download.DownloadListener;
import org.apache.camel.main.util.SuggestSimilarHelper;
import org.apache.camel.spi.BacklogDebugger;
@@ -1421,6 +1423,17 @@ public class Run extends CamelCommand {
} else {
// run default in current JVM with same camel version
try {
+ // a YAML route file that did not load: the validator's report
(what to write) before the loader's
+ // error, on a failed start and on a failed reload in dev mode
(CAMEL-24851)
+ final List<String> runFiles = new ArrayList<>(files);
+ main.setStartFailureListener(ex ->
YamlLoadFailureReport.logIfYamlLoadFailure(ex, runFiles));
+ main.addMainListener(new MainListenerSupport() {
+ @Override
+ public void afterConfigure(BaseMainSupport main) {
+ main.getCamelContext().getManagementStrategy()
+ .addEventNotifier(new
YamlLoadFailureReport.ReloadFailureNotifier());
+ }
+ });
return runKameletMain(main);
} catch (Exception ex) {
if (ignoreLoadingError) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
new file mode 100644
index 000000000000..f7a8c96bb53b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
@@ -0,0 +1,198 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.SimpleEventNotifierSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * When a YAML route file fails to load, the loader's message says which node
is wrong but not what to write; the schema
+ * validator has the message that does, with its hints. This runs the
validator on the route files of a failed start (or
+ * of a failed reload in dev mode) and prints its report, so the hints reach
everyone who runs, whether they validated
+ * first or not (CAMEL-24851).
+ */
+public final class YamlLoadFailureReport {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(YamlLoadFailureReport.class);
+
+ private static volatile CamelCatalog catalog;
+
+ private YamlLoadFailureReport() {
+ }
+
+ /** Whether the failure comes from loading a YAML route file: a
deserialization error or a pre-parse error. */
+ public static boolean isYamlLoadFailure(Throwable failure) {
+ for (Throwable t = failure; t != null; t = t.getCause() == t ? null :
t.getCause()) {
+ if (t instanceof YamlDeserializationException) {
+ return true;
+ }
+ String msg = t.getMessage();
+ if (msg != null && (msg.startsWith("Error pre-parsing resource")
|| msg.contains("Error constructing YAML node")
+ || msg.contains("Error parsing YAML"))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Logs the validator's report for the YAML files of a run whose start
failed on loading one of them; nothing for
+ * any other failure. Logged (not printed) so that it is in the console
and in the run's log file alike, where the
+ * camel-jbang-mcp tools read it, and before the loader's own error.
+ */
+ public static void logIfYamlLoadFailure(Throwable failure, List<String>
files) {
+ if (!isYamlLoadFailure(failure)) {
+ return;
+ }
+ List<String> lines = report(yamlFiles(files));
+ if (!lines.isEmpty()) {
+ LOG.error(String.join(System.lineSeparator(), lines));
+ }
+ }
+
+ /**
+ * The validator's report for the YAML files, as lines to print: a header,
then per file with problems its name and
+ * the messages, then a footer; empty when the validator finds nothing
(then the loader's message is all there is).
+ */
+ public static List<String> report(List<Path> yamlFiles) {
+ List<String> lines = new ArrayList<>();
+ for (Path file : yamlFiles) {
+ if (!Files.isRegularFile(file)) {
+ continue;
+ }
+ List<String> errors;
+ try {
+ String content = Files.readString(file);
+ Path dir = file.toAbsolutePath().getParent();
+ errors =
SourceValidator.validate(file.getFileName().toString(), content, catalog(),
null, dir);
+ } catch (Exception e) {
+ // the validator must never hide the loader's error
+ LOG.debug("Cannot validate {}", file, e);
+ continue;
+ }
+ if (!errors.isEmpty()) {
+ if (lines.isEmpty()) {
+ lines.add("The route file did not load. camel validate
yaml says what to write:");
+ }
+ lines.add(" " + file.getFileName() + ":");
+ for (String error : errors) {
+ lines.add(" " + error);
+ }
+ }
+ }
+ if (!lines.isEmpty()) {
+ lines.add(" (camel validate yaml <file> for the full report; the
loader's error follows)");
+ }
+ return lines;
+ }
+
+ /** The YAML route files among the files of a run: local files ending in
.yaml or .yml. */
+ public static List<Path> yamlFiles(List<String> files) {
+ List<Path> answer = new ArrayList<>();
+ if (files == null) {
+ return answer;
+ }
+ for (String f : files) {
+ if (f == null) {
+ continue;
+ }
+ if (f.startsWith("file:")) {
+ f = f.substring(5);
+ }
+ if (ResourceHelper.hasScheme(f) || f.startsWith("github:")) {
+ continue; // github:, https:, classpath: (the check Run makes;
a Windows drive letter is not a scheme)
+ }
+ String lower = f.toLowerCase();
+ Path path = Path.of(f);
+ if ((lower.endsWith(".yaml") || lower.endsWith(".yml")) &&
Files.isRegularFile(path) && !answer.contains(path)) {
+ answer.add(path);
+ }
+ }
+ return answer;
+ }
+
+ private static CamelCatalog catalog() {
+ CamelCatalog answer = catalog;
+ if (answer == null) {
+ synchronized (YamlLoadFailureReport.class) {
+ if (catalog == null) {
+ catalog = new DefaultCamelCatalog();
+ }
+ answer = catalog;
+ }
+ }
+ return answer;
+ }
+
+ /**
+ * Prints the report for a route file whose reload failed in dev mode: the
file watcher emits the reload failure
+ * event with the file name as the action.
+ */
+ public static final class ReloadFailureNotifier extends
SimpleEventNotifierSupport {
+
+ private final Consumer<String> printer;
+
+ /** Logs the report, as {@link #logIfYamlLoadFailure(Throwable, List)}
does. */
+ public ReloadFailureNotifier() {
+ this(null);
+ }
+
+ public ReloadFailureNotifier(Consumer<String> printer) {
+ this.printer = printer;
+ setIgnoreCamelContextEvents(false);
+ setIgnoreExchangeEvents(true);
+ // the reload events are dispatched under the route events flag:
it must stay off
+ setIgnoreRouteEvents(false);
+ setIgnoreServiceEvents(true);
+ }
+
+ @Override
+ public boolean isEnabled(CamelEvent event) {
+ return event instanceof CamelEvent.CamelContextReloadFailureEvent;
+ }
+
+ @Override
+ public void notify(CamelEvent event) {
+ // the file watcher emits the event with the file name as the
action (the API's getSource() is the context)
+ if (event instanceof CamelContextReloadFailureEvent failure
+ && failure.getAction() instanceof String name &&
isYamlLoadFailure(failure.getCause())) {
+ List<String> lines = report(yamlFiles(List.of(name)));
+ if (lines.isEmpty()) {
+ return;
+ }
+ if (printer != null) {
+ lines.forEach(printer);
+ } else {
+ LOG.error(String.join(System.lineSeparator(), lines));
+ }
+ }
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
new file mode 100644
index 000000000000..a06a76854a49
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.engine.SimpleCamelContext;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** CAMEL-24851: a YAML file that did not load gets the validator's report,
which says what to write. */
+class YamlLoadFailureReportTest {
+
+ @TempDir
+ Path dir;
+
+ private static final String BAD_ROUTE = """
+ - route:
+ from:
+ uri: timer:tick
+ steps:
+ - pollEnrich:
+ uri: file:./order.json
+ - to:
+ uri: log:done
+ """;
+
+ @Test
+ void aDeserializationErrorIsAYamlLoadFailure() {
+ Exception loader
+ = new YamlDeserializationException("Error constructing YAML
node id: pollEnrich: unsupported field: uri");
+ assertThat(YamlLoadFailureReport.isYamlLoadFailure(new
RuntimeCamelException("Error starting Camel", loader))).isTrue();
+ assertThat(
+ YamlLoadFailureReport.isYamlLoadFailure(new
RuntimeCamelException("Error pre-parsing resource: file:x.yaml")))
+ .isTrue();
+ assertThat(YamlLoadFailureReport.isYamlLoadFailure(new
IllegalArgumentException("Invalid directory: archived/${x}")))
+ .isFalse();
+ }
+
+ @Test
+ void theReportSaysWhatToWrite() throws Exception {
+ Path route = dir.resolve("orders.camel.yaml");
+ Files.writeString(route, BAD_ROUTE);
+ List<String> lines = YamlLoadFailureReport.report(List.of(route));
+ assertThat(lines).hasSize(4);
+ assertThat(lines.get(0)).isEqualTo("The route file did not load. camel
validate yaml says what to write:");
+ assertThat(lines.get(1)).isEqualTo(" orders.camel.yaml:");
+ // the schema message (with the CAMEL-24850 hint once it is in: "write
pollEnrich: {expression: ...}")
+ assertThat(lines.get(2)).startsWith("
").contains("pollEnrich").contains("'uri'");
+ assertThat(lines.get(3)).startsWith(" (camel validate yaml <file> for
the full report");
+ }
+
+ @Test
+ void aFileTheValidatorAcceptsGivesNoReport() throws Exception {
+ Path route = dir.resolve("ok.camel.yaml");
+ Files.writeString(route, BAD_ROUTE.replace("uri: file:./order.json",
+ "expression:\n constant:\n
expression: \"file:./order.json\""));
+ assertThat(YamlLoadFailureReport.report(List.of(route))).isEmpty();
+ }
+
+ @Test
+ void theRunFilesAreFilteredToLocalYaml() throws Exception {
+ Path route = dir.resolve("a.yaml");
+ Files.writeString(route, BAD_ROUTE);
+ List<Path> files
+ = YamlLoadFailureReport.yamlFiles(List.of("file:" + route,
route.toString(), dir.resolve("Foo.java").toString(),
+ "github:apache:camel:x.yaml",
dir.resolve("missing.yaml").toString()));
+ assertThat(files).containsExactly(route);
+ }
+
+ @Test
+ void theReloadNotifierPrintsTheReportForTheFileThatFailed() throws
Exception {
+ Path route = dir.resolve("orders.camel.yaml");
+ Files.writeString(route, BAD_ROUTE);
+ List<String> printed = new ArrayList<>();
+ YamlLoadFailureReport.ReloadFailureNotifier notifier = new
YamlLoadFailureReport.ReloadFailureNotifier(printed::add);
+ // the event the file watcher emits: the file name is the action, the
loader's exception the cause
+ CamelContextReloadFailureEvent event = new
CamelContextReloadFailureEvent(
+ new SimpleCamelContext(), route.toString(),
+ new YamlDeserializationException("Error constructing YAML node
id: pollEnrich: unsupported field: uri"));
+ assertThat(notifier.isEnabled(event)).isTrue();
+ notifier.notify(event);
+ assertThat(printed).hasSize(4);
+ assertThat(printed.get(2)).contains("pollEnrich");
+ }
+}
diff --git
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
index 0da255df653a..8be7dadd91e6 100644
---
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
+++
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
@@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
+import java.util.function.Consumer;
import java.util.stream.Stream;
import org.w3c.dom.Document;
@@ -166,6 +167,8 @@ public class KameletMain extends MainCommandLineSupport {
configureInitialProperties(locations);
}
+ private Consumer<Exception> startFailureListener;
+
public static void main(String... args) throws Exception {
KameletMain main = new KameletMain();
int code = main.run(args);
@@ -174,8 +177,24 @@ public class KameletMain extends MainCommandLineSupport {
System.exit(code);
}
+ /**
+ * Called with the exception when Camel fails to start, before the error
is logged: the CLI uses it to report what
+ * the schema validator says about a YAML route file that did not load
(CAMEL-24851).
+ */
+ public void setStartFailureListener(Consumer<Exception>
startFailureListener) {
+ this.startFailureListener = startFailureListener;
+ }
+
@Override
protected void doFail(Exception e) {
+ if (startFailureListener != null) {
+ try {
+ startFailureListener.accept(e);
+ } catch (Exception listenerFailure) {
+ // the listener must not hide the failure
+ LOG.debug("Start failure listener failed: {}",
listenerFailure.getMessage(), listenerFailure);
+ }
+ }
// ensure any unhandled fatal errors are also logged before
terminating process
LOG.error("Error starting Camel: {}", e, e);
super.doFail(e);