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

davsclaus pushed a commit to branch fix/CAMEL-25296
in repository https://gitbox.apache.org/repos/asf/camel.git

commit bb54321b5f48b7bd7889ef6ae057a7d56b587485
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 3 22:22:27 2026 +0200

    CAMEL-25296: camel-jbang - camel_get_log leaves out stack traces by 
default, keeping the error line
    
    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.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../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");
     }
 

Reply via email to