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 3e9a8b3b5671 CAMEL-25296: camel-jbang - camel_get_log leaves out stack
traces by default, keeping the error line (#27317)
3e9a8b3b5671 is described below
commit 3e9a8b3b5671c7e7637003c871c2071a5078d3a9
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 4 08:30:37 2026 +0200
CAMEL-25296: camel-jbang - camel_get_log leaves out stack traces by
default, keeping the error line (#27317)
An error handler record is about forty lines of message history and stack
trace behind a first line that already names the route, the source line and
the exception; camel_get_errors has the stack trace of every failed
exchange.
Without details the record keeps that first line and level, detailLines says
how much was left out, cause names the exception when the first line does
not, and at is the first frame of the user's own code (a bean, a processor,
a script), looked for from the root cause up since a Caused by section can
end in "... N more". Only stack traces and message history are left out; a
pretty-printed body stays a detail block. The filter still searches the
whole record. details=true returns everything as before.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 9 +-
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 7 +-
.../dsl/jbang/core/commands/ai/LogFileReader.java | 159 ++++++++++++++++++++-
.../jbang/core/commands/ai/LogFileReaderTest.java | 144 +++++++++++++++++++
.../jbang/core/commands/mcp/AuthoringTools.java | 10 +-
.../core/commands/mcp/AuthoringToolsTest.java | 2 +-
6 files changed, 315 insertions(+), 16 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index e93680c9af0b..9ba4fd6371a6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -360,8 +360,13 @@ project `directory` as an argument, the runtime tools take
the integration `name
`stop-routes`, `start-routes`, `reset-stats`.
| `camel_get_log`
-| Recent log records of a running integration, newest first, filtered by level
or text; a stack trace comes
- as one record with a `detail` block.
+| Recent log records of a running integration, newest first, filtered by level
or text. An error keeps its
+ first line, which names the route, the source line and the exception; its
stack trace is left out, with the
+ number of lines left out in `detailLines`, the exception as `cause` when the
first line does not name it, and
+ as `at` the first frame of the user's own code, looked for from the root
cause up (a bean, a processor, a
+ script), so a `NullPointerException` without a message still says where it
happened.
+ `details=true` returns the stack traces; `camel_get_errors` has them for
every failed exchange. Other
+ multi-line text, such as a pretty-printed body, stays as a `detail` block.
| `camel_get_errors`
| The failed exchanges of a running integration: route, exchange, exception
with stack trace, body and headers.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index c529be10c390..ec5879904a42 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -287,17 +287,18 @@ public final class AuthoringTools {
}));
registry.accept(tool("camel_get_log",
- "Recent log records of a running integration, newest first,
with optional filtering; a stack trace "
- + "comes as one record with a
detail block.")
+ "Recent log records of a running integration, newest first,
with optional filtering; stack traces "
+ + "only with details.")
.param("name", "string", NAME_DESC, false)
.param("limit", "integer", "Maximum records to return (default
50)", false)
.param("filter", "string", "Case-insensitive substring filter
on the message", false)
.param("level", "string", "Only this log level (INFO, WARN,
ERROR, DEBUG, TRACE)", false)
+ .param("details", "boolean", "Include the stack traces
(default false)", false)
.core(true)
.executor((ctx, args) -> {
RuntimeHelper.ProcessInfo p = selectProcess(ctx, args);
return LogFileReader.read(ctx.pid(), p != null ? p.name()
: null, integer(args, "limit", 50),
- args.get("filter"), args.get("level")).toJson();
+ args.get("filter"), args.get("level"), bool(args,
"details", false)).toJson();
}));
registry.accept(tool("camel_get_errors",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
index d648b2f7f5f4..97deb8002518 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
@@ -53,6 +53,29 @@ public final class LogFileReader {
private static final Pattern ANSI = Pattern.compile("\\u001b\\[[;\\d]*[
-/]*[@-~]");
+ /** A line of a stack trace or of the error handler's message history:
what is left out without details. */
+ private static final Pattern TRACE_LINE = Pattern.compile("^\\s+at
\\S+\\(.*\\)$|^Caused by: |^\\s*\\.\\.\\. \\d+ more$"
+ + "|^Message
History|^Stacktrace$");
+
+ /** The line that names an exception and its message: {@code
java.net.ConnectException: supplier unreachable}. */
+ private static final Pattern EXCEPTION_LINE = Pattern.compile(
+ "^(?:Caused by:
)?((?:[a-zA-Z_$][\\w$]*\\.)+[\\w$]*(?:Exception|Error|Throwable)\\b.*)$");
+
+ /** A frame of a stack trace: {@code at
org.example.OrderBean.process(OrderBean.java:42)}. */
+ private static final Pattern FRAME = Pattern.compile("^\\tat
(\\S+\\(.*\\))$");
+
+ /** Frames of the runtime, not of the user's code: Camel, the JDK, Groovy,
Vert.x and the like. */
+ private static final List<String> RUNTIME_PACKAGES = List.of(
+ "org.apache.camel.", "java.", "javax.", "jakarta.", "jdk.",
"sun.", "com.sun.", "groovy.",
+ "org.codehaus.groovy.", "org.apache.groovy.", "io.vertx.",
"io.netty.", "io.smallrye.", "io.quarkus.",
+ "org.springframework.", "com.fasterxml.", "org.jboss.", "kotlin.");
+
+ /** Internal fields of a record, removed before it is returned. */
+ private static final String TRACE = "_trace";
+ private static final String CAUSE = "_cause";
+ private static final String ORIGIN = "_origin";
+ private static final String DETAIL_LINES = "_detailLines";
+
private LogFileReader() {
}
@@ -78,6 +101,20 @@ public final class LogFileReader {
* @return lines (newest first), totalLines and returnedLines, or
an error when there is no log file
*/
public static JsonObject read(long pid, String name, int limit, String
filter, String level) {
+ return read(pid, name, limit, filter, level, true);
+ }
+
+ /**
+ * @param pid the process
+ * @param name the integration name, for the log file fallback
+ * @param limit maximum records to return
+ * @param filter case-insensitive substring the message or detail must
contain; null for all
+ * @param level only records of this level (INFO, WARN, ERROR, DEBUG,
TRACE); null for all
+ * @param details whether a stack trace stays in its record; without, an
error keeps its first line, the number of
+ * lines left out and the exception as {@code cause} when
the first line does not name it
+ * @return lines (newest first), totalLines and returnedLines, or
an error when there is no log file
+ */
+ public static JsonObject read(long pid, String name, int limit, String
filter, String level, boolean details) {
Path file = logFile(pid, name);
JsonObject result = new JsonObject();
result.put("file", file.toString());
@@ -93,11 +130,22 @@ public final class LogFileReader {
result.put("error", "Cannot read " + file + ": " + e.getMessage());
return result;
}
- return build(lines, limit, filter, level, result);
+ return build(lines, limit, filter, level, details, result);
}
- /** Groups raw lines into records and filters them, newest first; visible
for tests. */
+ /** Groups raw lines into records and filters them, newest first, stack
traces included; visible for tests. */
static JsonObject build(List<String> lines, int limit, String filter,
String level, JsonObject result) {
+ return build(lines, limit, filter, level, true, result);
+ }
+
+ /**
+ * Groups raw lines into records and filters them, newest first; visible
for tests. Without details a stack trace is
+ * left out of its record (CAMEL-25296): an error handler record is about
forty lines of message history and stack
+ * trace behind a first line that already names the route, the source line
and the exception, and camel_get_errors
+ * has the stack trace of every failed exchange. Other multi-line text,
such as a pretty-printed body, is kept.
+ */
+ static JsonObject build(
+ List<String> lines, int limit, String filter, String level,
boolean details, JsonObject result) {
List<JsonObject> records = fold(toRecords(lines));
String needle = filter == null || filter.isBlank() ? null :
filter.toLowerCase();
JsonArray rows = new JsonArray();
@@ -113,14 +161,45 @@ public final class LogFileReader {
continue;
}
}
- rows.add(r);
+ rows.add(details ? r : withoutTrace(r));
+ }
+ int left = 0;
+ for (Object o : rows) {
+ JsonObject r = (JsonObject) o;
+ if (r.containsKey("detailLines")) {
+ left++;
+ }
+ r.remove(TRACE);
+ r.remove(CAUSE);
+ r.remove(ORIGIN);
+ r.remove(DETAIL_LINES);
}
result.put("lines", rows);
result.put("totalLines", lines.size());
result.put("returnedLines", rows.size());
+ if (left > 0) {
+ result.put("note", "The stack traces of " + left + " record(s) are
left out (detailLines says how many lines):"
+ + " details=true returns them, camel_get_errors
has the failed exchanges.");
+ }
return result;
}
+ private static JsonObject withoutTrace(JsonObject r) {
+ if (!Boolean.TRUE.equals(r.get(TRACE))) {
+ return r;
+ }
+ r.remove("detail");
+ r.put("detailLines", r.get(DETAIL_LINES));
+ Object cause = r.get(CAUSE);
+ if (cause instanceof String c && !r.getStringOrDefault("message",
"").contains(c)) {
+ r.put("cause", c);
+ }
+ if (r.get(ORIGIN) instanceof String origin) {
+ r.put("at", origin);
+ }
+ return r;
+ }
+
private static List<String> tail(Path file) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "r")) {
long length = raf.length();
@@ -149,11 +228,32 @@ public final class LogFileReader {
JsonObject head = null;
List<String> detail = null;
int hidden = 0;
+ boolean trace = false;
+ String cause = null;
+ // per exception of the trace, outermost first: the first frame of the
user's code in it, or null
+ List<String> origins = new ArrayList<>();
for (String raw : lines) {
String line = ANSI.matcher(raw).replaceAll("");
Matcher m = LOG_PATTERN.matcher(line);
boolean matches = m.matches();
if (!matches && head != null) {
+ // the whole block counts for what it is and what threw, also
past the lines that are kept
+ trace |= TRACE_LINE.matcher(line).find();
+ Matcher ex = EXCEPTION_LINE.matcher(line);
+ if (ex.matches() && (cause == null || line.startsWith("Caused
by: "))) {
+ // the first exception line, unless a Caused by follows:
the last one is the root cause
+ cause = ex.group(1);
+ origins.add(null);
+ } else if (!origins.isEmpty() && origins.get(origins.size() -
1) == null) {
+ // a frame directly under its exception (a Suppressed
block is indented further and skipped)
+ Matcher frame = FRAME.matcher(line);
+ if (frame.matches()) {
+ String f = withoutModule(frame.group(1));
+ if (isUserCode(f)) {
+ origins.set(origins.size() - 1, f);
+ }
+ }
+ }
if (detail.size() < MAX_DETAIL_LINES) {
detail.add(line);
} else {
@@ -162,7 +262,7 @@ public final class LogFileReader {
continue;
}
if (head != null) {
- records.add(finish(head, detail, hidden));
+ records.add(finish(head, detail, hidden, trace, cause,
origin(origins)));
}
head = new JsonObject();
if (matches) {
@@ -180,9 +280,12 @@ public final class LogFileReader {
}
detail = new ArrayList<>();
hidden = 0;
+ trace = false;
+ cause = null;
+ origins = new ArrayList<>();
}
if (head != null) {
- records.add(finish(head, detail, hidden));
+ records.add(finish(head, detail, hidden, trace, cause,
origin(origins)));
}
return records;
}
@@ -217,8 +320,52 @@ public final class LogFileReader {
&& Objects.equals(a.get("detail"), b.get("detail"));
}
- private static JsonObject finish(JsonObject head, List<String> detail, int
hidden) {
+ /**
+ * Where it went wrong in the user's code: the root cause is printed last,
so walk from the bottom. A Caused by
+ * section ends with "... N more" for the frames it shares with the
exception above it, which may be the very frame
+ * of the user's code, so a section without one hands over to the one
above.
+ */
+ private static String origin(List<String> origins) {
+ for (int i = origins.size() - 1; i >= 0; i--) {
+ if (origins.get(i) != null) {
+ return origins.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * A frame without its module or class loader: {@code
java.base/java.lang.Thread.run(..)},
+ * {@code app//org.example..}.
+ */
+ private static String withoutModule(String frame) {
+ int paren = frame.indexOf('(');
+ int slash = frame.lastIndexOf('/', paren < 0 ? frame.length() : paren);
+ return slash < 0 ? frame : frame.substring(slash + 1);
+ }
+
+ private static boolean isUserCode(String frame) {
+ for (String p : RUNTIME_PACKAGES) {
+ if (frame.startsWith(p)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static JsonObject finish(
+ JsonObject head, List<String> detail, int hidden, boolean trace,
String cause, String origin) {
if (!detail.isEmpty()) {
+ if (trace) {
+ head.put(TRACE, Boolean.TRUE);
+ head.put(DETAIL_LINES, detail.size() + hidden);
+ if (cause != null) {
+ head.put(CAUSE, cause);
+ }
+ if (origin != null) {
+ head.put(ORIGIN, origin);
+ }
+ }
String text = String.join("\n", detail);
if (hidden > 0) {
text += "\n... " + hidden + " more lines";
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
index 543023809ad7..2958961f458c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
@@ -83,4 +83,148 @@ class LogFileReaderTest {
assertEquals(2, LogFileReader.build(LINES, 2, null, null, new
JsonObject()).getInteger("returnedLines"));
assertEquals(0, LogFileReader.build(List.of(), 2, null, null, new
JsonObject()).getInteger("returnedLines"));
}
+
+ @Test
+ void withoutDetailsAnErrorKeepsItsFirstLineAndTheCause() {
+ JsonObject result = LogFileReader.build(LINES, 50, null, null, false,
new JsonObject());
+ List<JsonObject> rows = List.copyOf(result.getCollection("lines"));
+ JsonObject error = rows.get(1);
+ assertEquals("ERROR", error.getString("level"));
+ assertEquals("Failed delivery", error.getString("message"));
+ assertNull(error.get("detail"), "the stack trace is left out");
+ assertEquals(3, error.getInteger("detailLines"));
+ assertEquals("java.lang.IllegalStateException: boom",
error.getString("cause"),
+ "the first line does not name the exception, so the cause
does");
+ assertTrue(result.getString("note").contains("details=true"));
+ for (JsonObject row : rows) {
+ assertTrue(row.keySet().stream().noneMatch(k ->
k.startsWith("_")), "no internal field leaks: " + row);
+ }
+ }
+
+ @Test
+ void theErrorHandlerLineAlreadyNamesTheExceptionSoThereIsNoCause() {
+ List<String> lines = List.of(
+ "2026-10-03 08:29:42.988 ERROR 42 --- [- timer://stock]
or.errorhandler.DefaultErrorHandler : Failed delivery"
+ + " for (MessageId: A on ExchangeId: A)
at stock-check[throwException1] cb.camel.yaml:27."
+ + " Exhausted after delivery attempt: 1
caught: java.net.ConnectException: supplier"
+ + " unreachable",
+ "Message History (source location and message history is
enabled)",
+
"---------------------------------------------------------------------------------------------------",
+ "Source ID
Processor Elapsed (ms)",
+ "cb.camel.yaml:4 stock-check/stock-check
from[timer://stock?period=1000] 0",
+ "cb.camel.yaml:27 stock-check/throwException1
throwException[java.net.ConnectException] 0",
+ "Stacktrace",
+
"---------------------------------------------------------------------------------------------------",
+ "java.net.ConnectException: supplier unreachable",
+ "\tat
org.apache.camel.processor.ThrowExceptionProcessor.process(ThrowExceptionProcessor.java:68)",
+ "\tat
org.apache.camel.processor.Pipeline.process(Pipeline.java:163)");
+
+ JsonObject error = (JsonObject) LogFileReader.build(lines, 50, null,
null, false, new JsonObject())
+ .getCollection("lines").iterator().next();
+ assertTrue(error.getString("message").endsWith("caught:
java.net.ConnectException: supplier unreachable"));
+ assertNull(error.get("detail"));
+ assertNull(error.get("cause"), "the first line has it already");
+ assertNull(error.get("at"), "a YAML route has no frame of the user's
code; the first line has the source line");
+ assertEquals(10, error.getInteger("detailLines"));
+ }
+
+ @Test
+ void theCauseIsTheLastCausedByAlsoPastTheLinesThatAreKept() {
+ List<String> lines = new java.util.ArrayList<>();
+ lines.add("2026-10-03 08:00:00.000 ERROR 42 --- [ main] route1 :
Failed to start");
+ lines.add("org.apache.camel.RuntimeCamelException: wrapped");
+ for (int i = 0; i < 30; i++) {
+ lines.add("\tat org.example.Frame.m" + i + "(Frame.java:" + i +
")");
+ }
+ lines.add("Caused by: java.io.IOException: disk full");
+ lines.add("\tat org.example.Disk.write(Disk.java:1)");
+
+ JsonObject error = (JsonObject) LogFileReader.build(lines, 50, null,
null, false, new JsonObject())
+ .getCollection("lines").iterator().next();
+ assertEquals("java.io.IOException: disk full",
error.getString("cause"));
+ assertEquals(33, error.getInteger("detailLines"));
+ }
+
+ @Test
+ void aPrettyPrintedBodyIsNotAStackTraceAndStays() {
+ List<String> lines = List.of(
+ "2026-10-03 08:00:00.000 INFO 42 --- [ timer://order] route1
: Order ORD-1001: {",
+ " \"orderId\" : \"ORD-1001\",",
+ " \"country\" : \"DK\"",
+ "}");
+
+ JsonObject result = LogFileReader.build(lines, 50, null, null, false,
new JsonObject());
+ JsonObject row = (JsonObject)
result.getCollection("lines").iterator().next();
+ assertTrue(row.getString("detail").contains("\"orderId\" :
\"ORD-1001\""));
+ assertNull(row.get("detailLines"));
+ assertNull(result.get("note"), "nothing was left out");
+ }
+
+ @Test
+ void withDetailsTheRecordIsAsBeforeAndTheFilterSearchesTheLeftOutTrace() {
+ JsonObject error = (JsonObject) LogFileReader.build(LINES, 50, null,
"error", true, new JsonObject())
+ .getCollection("lines").iterator().next();
+
assertTrue(error.getString("detail").startsWith("java.lang.IllegalStateException:
boom"));
+ assertNull(error.get("detailLines"));
+ assertNull(error.get("cause"));
+
+ JsonObject boom = LogFileReader.build(LINES, 50, "boom", null, false,
new JsonObject());
+ assertEquals(1, boom.getInteger("returnedLines"), "the filter matches
the trace even when it is left out");
+ }
+
+ @Test
+ void theOriginIsTheUsersFrameOfTheRootCauseUnderTheWrapping() {
+ List<String> lines = List.of(
+ "2026-10-03 09:00:00.000 ERROR 42 --- [ timer://tick] route1 :
Failed to invoke the bean",
+ "org.apache.camel.RuntimeCamelException: Error invoking
method",
+ "\tat
org.apache.camel.util.ObjectHelper.wrapRuntimeCamelException(ObjectHelper.java:21)",
+ "\tat
org.apache.camel.component.bean.BeanProcessor.process(BeanProcessor.java:81)",
+ "Caused by: java.lang.reflect.InvocationTargetException",
+ "\tat
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118)",
+ "\t... 2 more",
+ "Caused by: java.lang.NullPointerException",
+ "\tat app//camel.example.OrderBean.total(OrderBean.java:42)",
+ "\tat app//camel.example.OrderBean.process(OrderBean.java:17)",
+ "\t... 4 more");
+
+ JsonObject error = (JsonObject) LogFileReader.build(lines, 50, null,
null, false, new JsonObject())
+ .getCollection("lines").iterator().next();
+ assertEquals("java.lang.NullPointerException",
error.getString("cause"));
+ assertEquals("camel.example.OrderBean.total(OrderBean.java:42)",
error.getString("at"),
+ "the top frame of the root cause, without its class loader
prefix");
+ }
+
+ @Test
+ void aRootCauseCutShortByMoreHandsOverToTheExceptionAboveIt() {
+ List<String> lines = List.of(
+ "2026-10-03 09:00:00.000 ERROR 42 --- [ timer://tick] route1 :
Order failed",
+ "java.lang.IllegalStateException: order failed",
+ "\tat camel.example.OrderBean.process(OrderBean.java:17)",
+ "\tat
org.apache.camel.component.bean.BeanProcessor.process(BeanProcessor.java:81)",
+ "Caused by: java.lang.NumberFormatException: For input string:
\"x\"",
+ "\tat java.base/java.lang.Integer.parseInt(Integer.java:661)",
+ "\t... 2 more");
+
+ JsonObject error = (JsonObject) LogFileReader.build(lines, 50, null,
null, false, new JsonObject())
+ .getCollection("lines").iterator().next();
+ assertEquals("java.lang.NumberFormatException: For input string:
\"x\"", error.getString("cause"));
+ assertEquals("camel.example.OrderBean.process(OrderBean.java:17)",
error.getString("at"),
+ "the JDK frame of the root cause is not the user's code, and
the frame that called it is above");
+ }
+
+ @Test
+ void aGroovyScriptIsTheUsersCode() {
+ List<String> lines = List.of(
+ "2026-10-03 09:00:00.000 WARN 42 --- [ timer://orders]
TimerConsumer : Error processing exchange",
+ "groovy.lang.MissingPropertyException: No such property:
header for class: Script1",
+ "\tat
org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:65)",
+ "\tat Script1.run(Script1.groovy:3)",
+ "\tat
org.apache.camel.language.groovy.GroovyExpression.evaluate(GroovyExpression.java:72)");
+
+ JsonObject warn = (JsonObject) LogFileReader.build(lines, 50, null,
null, false, new JsonObject())
+ .getCollection("lines").iterator().next();
+ assertEquals("Script1.run(Script1.groovy:3)", warn.getString("at"));
+ assertEquals("groovy.lang.MissingPropertyException: No such property:
header for class: Script1",
+ warn.getString("cause"));
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index 5423c7d2ad9e..e6fc218efa89 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -206,14 +206,16 @@ public class AuthoringTools {
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
- description = "Recent log records of a running integration, newest
first, with optional filtering; a "
- + "stack trace comes as one record with a detail
block.")
+ description = "Recent log records of a running integration, newest
first, with optional filtering; stack "
+ + "traces only with details.")
public JsonObject camel_get_log(
@ToolArg(description = NAME_DESC, required = false) String name,
@ToolArg(description = "Maximum records to return (default 50)",
required = false) Integer limit,
@ToolArg(description = "Case-insensitive substring filter on the
message", required = false) String filter,
- @ToolArg(description = "Only this log level (INFO, WARN, ERROR,
DEBUG, TRACE)", required = false) String level) {
- return call("camel_get_log", args("name", name, "limit", limit,
"filter", filter, "level", level));
+ @ToolArg(description = "Only this log level (INFO, WARN, ERROR,
DEBUG, TRACE)", required = false) String level,
+ @ToolArg(description = "Include the stack traces (default false)",
required = false) Boolean details) {
+ return call("camel_get_log", args("name", name, "limit", limit,
"filter", filter, "level", level,
+ "details", details));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 07613b2420a9..6ddf954e7b9c 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -113,7 +113,7 @@ class AuthoringToolsTest {
void anUnknownIntegrationNameIsAnError() {
assertThatThrownBy(() -> tools.camel_eval_expression("${body}", null,
"camel", "no-such-app-xyz-1"))
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
- assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1",
null, null, null))
+ assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1",
null, null, null, null))
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
}