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"); }
