This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch it in repository https://gitbox.apache.org/repos/asf/camel.git
commit 37872879bb7635794c5b562dde50b5a90c09fa17 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Dec 7 08:37:10 2025 +0100 CAMEL-22703: camel-jbang - history to have it mode to show message data like debugger --- .../impl/console/MessageHistoryDevConsole.java | 73 ++- .../camel/dsl/jbang/core/commands/Debug.java | 5 +- .../core/commands/action/CamelHistoryAction.java | 507 ++++++++++++++++++++- 3 files changed, 580 insertions(+), 5 deletions(-) diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/MessageHistoryDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/MessageHistoryDevConsole.java index f2d0c6656190..86cbad472945 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/MessageHistoryDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/MessageHistoryDevConsole.java @@ -16,21 +16,31 @@ */ package org.apache.camel.impl.console; +import java.io.LineNumberReader; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map; +import org.apache.camel.Route; import org.apache.camel.spi.BacklogTracer; import org.apache.camel.spi.BacklogTracerEventMessage; import org.apache.camel.spi.Configurer; +import org.apache.camel.spi.Resource; import org.apache.camel.spi.annotations.DevConsole; import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.IOHelper; +import org.apache.camel.util.StringHelper; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; @DevConsole(name = "message-history", displayName = "Message History", description = "History of latest completed exchange") @Configurer(extended = true) public class MessageHistoryDevConsole extends AbstractDevConsole { + public static final String CODE_LIMIT = "codeLimit"; + public MessageHistoryDevConsole() { super("camel", "message-history", "Message History", "History of latest completed exchange"); } @@ -53,14 +63,30 @@ public class MessageHistoryDevConsole extends AbstractDevConsole { protected JsonObject doCallJson(Map<String, Object> options) { JsonObject root = new JsonObject(); + String codeLimit = (String) options.getOrDefault(CODE_LIMIT, "5"); + BacklogTracer tracer = getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class); if (tracer != null) { JsonArray arr = new JsonArray(); Collection<BacklogTracerEventMessage> queue = tracer.getLatestMessageHistory(); for (BacklogTracerEventMessage t : queue) { - JsonObject jo = (JsonObject) t.asJSon(); - arr.add(jo); + JsonObject to = (JsonObject) t.asJSon(); + + // enrich with source code +/- lines around location + int limit = Integer.parseInt(codeLimit); + if (limit > 0) { + String rid = to.getString("routeId"); + String loc = to.getString("location"); + if (rid != null) { + List<JsonObject> code = enrichSourceCode(rid, loc, limit); + if (code != null && !code.isEmpty()) { + to.put("code", code); + } + } + } + + arr.add(to); } root.put("name", getCamelContext().getName()); root.put("traces", arr); @@ -69,4 +95,47 @@ public class MessageHistoryDevConsole extends AbstractDevConsole { return root; } + private List<JsonObject> enrichSourceCode(String routeId, String location, int lines) { + Route route = getCamelContext().getRoute(routeId); + if (route == null) { + return null; + } + Resource resource = route.getSourceResource(); + if (resource == null) { + return null; + } + + List<JsonObject> code = new ArrayList<>(); + + location = StringHelper.afterLast(location, ":"); + int line = 0; + try { + if (location != null) { + line = Integer.parseInt(location); + } + LineNumberReader reader = new LineNumberReader(resource.getReader()); + for (int i = 1; i <= line + lines; i++) { + String t = reader.readLine(); + if (t != null) { + int low = line - lines + 2; // grab more of the following code than previous code (+2) + int high = line + lines + 1 + 2; + if (i >= low && i <= high) { + JsonObject c = new JsonObject(); + c.put("line", i); + if (line == i) { + c.put("match", true); + } + c.put("code", Jsoner.escape(t)); + code.add(c); + } + } + } + IOHelper.close(reader); + } catch (Exception e) { + // ignore + } + + return code; + } + } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java index c05836ee7457..45b3bed57c55 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java @@ -962,9 +962,12 @@ public class Debug extends Run { c = Jsoner.unescape(h.code); c = c.trim(); } else { - c = Jsoner.unescape(h.nodeLabel); + c = Jsoner.escape(h.nodeLabel); c = c.trim(); } + // pad with level + String pad = StringHelper.padString(h.level); + c = pad + c; String fids = String.format("%-30.30s", ids); String msg; diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java index fc1eec3285f4..f7eea0727e29 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHistoryAction.java @@ -16,15 +16,20 @@ */ package org.apache.camel.dsl.jbang.core.commands.action; +import java.io.Console; import java.io.LineNumberReader; import java.nio.file.Files; import java.nio.file.Path; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import com.github.freva.asciitable.AsciiTable; import com.github.freva.asciitable.Column; @@ -35,6 +40,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.tooling.model.ComponentModel; import org.apache.camel.tooling.model.EipModel; +import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.camel.util.StringHelper; import org.apache.camel.util.TimeUtils; @@ -43,6 +49,7 @@ import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiConsole; import picocli.CommandLine; @CommandLine.Command(name = "history", @@ -52,6 +59,10 @@ public class CamelHistoryAction extends ActionWatchCommand { @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") String name = "*"; + @CommandLine.Option(names = { "--it" }, + description = "Interactive mode for enhanced history information") + boolean it; + @CommandLine.Option(names = { "--source" }, description = "Prefer to display source filename/code instead of IDs") boolean source; @@ -68,12 +79,59 @@ public class CamelHistoryAction extends ActionWatchCommand { description = "Limit Split to a maximum number of entries to be displayed") int limitSplit; + @CommandLine.Option(names = { "--timestamp" }, defaultValue = "true", + description = "Print timestamp.") + boolean timestamp = true; + + @CommandLine.Option(names = { "--ago" }, + description = "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.") + boolean ago; + + @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue = "false", + description = "Show exchange properties in debug messages") + boolean showExchangeProperties; + + @CommandLine.Option(names = { "--show-exchange-variables" }, defaultValue = "true", + description = "Show exchange variables in debug messages") + boolean showExchangeVariables = true; + + @CommandLine.Option(names = { "--show-headers" }, defaultValue = "true", + description = "Show message headers in debug messages") + boolean showHeaders = true; + + @CommandLine.Option(names = { "--show-body" }, defaultValue = "true", + description = "Show message body in debug messages") + boolean showBody = true; + + @CommandLine.Option(names = { "--show-exception" }, defaultValue = "true", + description = "Show exception and stacktrace for failed messages") + boolean showException = true; + + @CommandLine.Option(names = { "--pretty" }, + description = "Pretty print message body when using JSon or XML format") + boolean pretty; + + @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", description = "Use colored logging") + boolean loggingColor = true; + + private MessageTableHelper tableHelper; + private final AtomicBoolean quit = new AtomicBoolean(); + private final AtomicBoolean waitForUser = new AtomicBoolean(); private final CamelCatalog camelCatalog = new DefaultCamelCatalog(true); public CamelHistoryAction(CamelJBangMain main) { super(main); } + @Override + public Integer doCall() throws Exception { + if (it) { + // cannot run in watch mode for interactive + watch = false; + } + return super.doCall(); + } + @Override public Integer doWatchCall() throws Exception { if (name == null) { @@ -83,6 +141,13 @@ public class CamelHistoryAction extends ActionWatchCommand { List<List<Row>> pids = loadRows(); if (!pids.isEmpty()) { + if (it) { + if (pids.size() > 1) { + printer().println("Interactive mode only operate on a single Camel application"); + return 0; + } + return doInteractiveCall(pids.get(0)); + } if (watch) { clearScreen(); } @@ -137,6 +202,345 @@ public class CamelHistoryAction extends ActionWatchCommand { return 0; } + private void doRead(Console c, AtomicBoolean quit, AtomicInteger index) { + do { + String line = c.readLine(); + if (line != null) { + line = line.trim(); + if ("q".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { + quit.set(true); + } + // user have pressed ENTER so continue + index.incrementAndGet(); + waitForUser.set(false); + } + } while (!quit.get()); + } + + private Integer doInteractiveCall(List<Row> rows) throws Exception { + // read CLI input from user + final AtomicInteger index = new AtomicInteger(); + final Console c = System.console(); + Thread t2 = new Thread(() -> doRead(c, quit, index), "ReadCommand"); + t2.start(); + + tableHelper = new MessageTableHelper(); + tableHelper.setPretty(pretty); + tableHelper.setLoggingColor(loggingColor); + tableHelper.setShowExchangeProperties(showExchangeProperties); + tableHelper.setShowExchangeVariables(showExchangeVariables); + + do { + if (!waitForUser.get()) { + clearScreen(); + Row first = rows.get(0); + String ago = TimeUtils.printSince(first.timestamp); + Row last = rows.get(rows.size() - 1); + String status = last.failed ? "failed" : "success"; + String s = String.format("Message History of last completed (id:%s status:%s ago:%s pid:%d name:%s)", + first.exchangeId, status, ago, first.pid, first.name); + printer().println(s); + + int i = index.get(); + if (i < rows.size()) { + Row r = rows.get(i); + printSourceAndHistory(r); + printCurrentRow(r); + } + waitForUser.set(true); + } + } while (!quit.get() || waitForUser.get()); + + return 0; + } + + private static int lineIsNumber(String line) { + try { + return Integer.parseInt(line); + } catch (Exception e) { + return 0; + } + } + + private String getDataAsTable(Row r) { + return tableHelper.getDataAsTable(r.exchangeId, r.exchangePattern, null, r.endpoint, r.endpointService, + r.message, + r.exception); + } + + private void printSourceAndHistory(Row row) { + List<Panel> panel = new ArrayList<>(); + if (!row.code.isEmpty()) { + String loc = StringHelper.beforeLast(row.location, ":", row.location); + if (loc != null && loc.length() < 72) { + loc = loc + " ".repeat(72 - loc.length()); + } else { + loc = ""; + } + panel.add(Panel.withCode("Source: " + loc).andHistory("History")); + panel.add(Panel.withCode("-".repeat(80)) + .andHistory("-".repeat(90))); + + for (int i = 0; i < row.code.size(); i++) { + Code code = row.code.get(i); + String c = Jsoner.unescape(code.code); + String arrow = " "; + if (code.match) { + if (row.first) { + arrow = "*-->"; + } else if (row.last) { + arrow = "<--*"; + } else { + arrow = "--->"; + } + } + String msg = String.format("%4d: %s %s", code.line, arrow, c); + if (msg.length() > 80) { + msg = msg.substring(0, 80); + } + int length = msg.length(); + if (loggingColor && code.match) { + Ansi.Color col = Ansi.Color.BLUE; + Ansi.Attribute it = Ansi.Attribute.INTENSITY_BOLD; + if (row.failed && row.last) { + col = Ansi.Color.RED; + } else if (row.last) { + col = Ansi.Color.GREEN; + } + // need to fill out entire line, so fill in spaces + if (length < 80) { + String extra = " ".repeat(80 - length); + msg = msg + extra; + length = 80; + } + msg = Ansi.ansi().bg(col).a(it).a(msg).reset().toString(); + } else { + // need to fill out entire line, so fill in spaces + if (length < 80) { + String extra = " ".repeat(80 - length); + msg = msg + extra; + length = 80; + } + } + panel.add(Panel.withCode(msg, length)); + } + for (int i = row.code.size(); i < 11; i++) { + // empty lines so source code has same height + panel.add(Panel.withCode(" ".repeat(80))); + } + } + + if (!row.history.isEmpty()) { + if (row.history.size() > (panel.size() - 4)) { + // cut to only what we can display + int pos = row.history.size() - (panel.size() - 4); + if (row.history.size() > pos) { + row.history = row.history.subList(pos, row.history.size()); + } + } + for (int i = 2; panel.size() > 2 && i < 11; i++) { + Panel p = panel.get(i); + if (row.history.size() > (i - 2)) { + History h = row.history.get(i - 2); + boolean top = h == row.history.get(row.history.size() - 1); + + String ids; + if (source) { + ids = locationAndLine(h.location, h.line); + } else { + ids = h.routeId + "/" + h.nodeId; + } + if (ids.length() > 30) { + ids = ids.substring(ids.length() - 30); + } + + ids = String.format("%-30.30s", ids); + if (loggingColor) { + ids = Ansi.ansi().fgCyan().a(ids).reset().toString(); + } + long e = i == 2 ? 0 : h.elapsed; // the pseudo from should have 0 as elapsed + String elapsed = "(" + e + "ms)"; + + String c = ""; + if (source && h.code != null) { + c = Jsoner.unescape(h.code); + c = c.trim(); + } else if (h.nodeLabel != null) { + c = Jsoner.escape(h.nodeLabel); + c = c.trim(); + } + // pad with level + String pad = StringHelper.padString(h.level); + c = pad + c; + + String fids = String.format("%-30.30s", ids); + String msg; + if (top && !row.last) { + msg = String.format("%2d %10.10s %s %4d: %s", h.index, "--->", fids, h.line, c); + } else { + msg = String.format("%2d %10.10s %s %4d: %s", h.index, elapsed, fids, h.line, c); + } + int len = msg.length(); + if (loggingColor) { + fids = String.format("%-30.30s", ids); + fids = Ansi.ansi().fgCyan().a(fids).reset().toString(); + if (top && !row.last) { + msg = String.format("%2d %10.10s %s %4d: %s", h.index, "--->", fids, h.line, c); + } else { + msg = String.format("%2d %10.10s %s %4d: %s", h.index, elapsed, fids, h.line, c); + } + } + + p.history = msg; + p.historyLength = len; + } + } + } + // the ascii-table does not work well with color cells (https://github.com/freva/ascii-table/issues/26) + for (Panel p : panel) { + String c = p.code; + String h = p.history; + int len = p.historyLength; + if (len > 90) { + h = h.substring(0, 90); + } + String line = c + " " + h; + printer().println(line); + } + } + + private void printCurrentRow(Row row) { + if (timestamp) { + String ts; + if (ago) { + ts = String.format("%12s", TimeUtils.printSince(row.timestamp) + " ago"); + } else { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + ts = sdf.format(new Date(row.timestamp)); + } + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset()); + } else { + printer().print(ts); + } + printer().print(" "); + } + // pid + String p = String.format("%5.5s", row.pid); + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(p).reset()); + AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(" --- ").reset()); + } else { + printer().print(p); + printer().print(" --- "); + } + // thread name + String tn = row.threadName; + if (tn.length() > 25) { + tn = tn.substring(tn.length() - 25); + } + tn = String.format("[%25.25s]", tn); + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(tn).reset()); + } else { + printer().print(tn); + } + printer().print(" "); + // node ids or source location + String ids; + if (source) { + ids = locationAndLine(row.location, -1); + } else { + ids = row.routeId + "/" + getId(row); + } + if (ids.length() > 40) { + ids = ids.substring(ids.length() - 40); + } + ids = String.format("%40.40s", ids); + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgCyan().a(ids).reset()); + } else { + printer().print(ids); + } + printer().print(" : "); + // uuid + String u = String.format("%5.5s", row.uid); + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(u).reset()); + } else { + printer().print(u); + } + printer().print(" - "); + // status + printer().print(getStatus(row)); + // elapsed + String e = getElapsed(row); + if (e != null) { + if (loggingColor) { + AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" + e + ")").reset()); + } else { + printer().print("(" + e + ")"); + } + } + printer().println(); + printer().println(getDataAsTable(row)); + printer().println(); + } + + private String getElapsed(Row r) { + if (!r.first) { + return TimeUtils.printDuration(r.elapsed, true); + } + return null; + } + + private String getStatus(Row r) { + boolean remote = r.endpoint != null && r.endpoint.getBooleanOrDefault("remote", false); + + if (r.first) { + String s = "Created"; + if (loggingColor) { + return Ansi.ansi().fg(Ansi.Color.GREEN).a(s).reset().toString(); + } else { + return s; + } + } else if (r.last) { + String done = r.exception != null ? "Completed (exception)" : "Completed (success)"; + if (loggingColor) { + return Ansi.ansi().fg(r.failed ? Ansi.Color.RED : Ansi.Color.GREEN).a(done).reset().toString(); + } else { + return done; + } + } + if (!r.done) { + if (loggingColor) { + return Ansi.ansi().fg(Ansi.Color.BLUE).a("Breakpoint").reset().toString(); + } else { + return "Breakpoint"; + } + } else if (r.failed) { + String fail = r.exception != null ? "Exception" : "Failed"; + if (loggingColor) { + return Ansi.ansi().fg(Ansi.Color.RED).a(fail).reset().toString(); + } else { + return fail; + } + } else { + String s = remote ? "Sent" : "Processed"; + if (loggingColor) { + return Ansi.ansi().fg(Ansi.Color.GREEN).a(s).reset().toString(); + } else { + return s; + } + } + } + + private static String locationAndLine(String loc, int line) { + // shorten path as there is no much space + loc = FileUtil.stripPath(loc); + return line == -1 ? loc : loc + ":" + line; + } + private boolean filterDepth(Row r) { if (depth >= 9) { return true; @@ -241,8 +645,6 @@ public class CamelHistoryAction extends ActionWatchCommand { answer.add(rows); } } catch (Exception e) { - // TODO: remove me - e.printStackTrace(); // ignore } finally { IOHelper.close(reader); @@ -284,6 +686,10 @@ public class CamelHistoryAction extends ActionWatchCommand { row.nodeLabel = URISupport.sanitizeUri(row.nodeLabel); } row.nodeLevel = jo.getIntegerOrDefault("nodeLevel", 0); + if ("aggregate".equals(jo.getString("nodeShortName"))) { + row.aggregate = new JsonObject(); + row.aggregate.put("nodeLabel", jo.getString("nodeLabel")); + } String uri = jo.getString("endpointUri"); if (uri != null) { row.endpoint = new JsonObject(); @@ -313,6 +719,31 @@ public class CamelHistoryAction extends ActionWatchCommand { // we should exchangeId/pattern elsewhere row.message.remove("exchangeId"); row.message.remove("exchangePattern"); + if (!showExchangeVariables) { + row.message.remove("exchangeVariables"); + } + if (!showExchangeProperties) { + row.message.remove("exchangeProperties"); + } + if (!showHeaders) { + row.message.remove("headers"); + } + if (!showBody) { + row.message.remove("body"); + } + if (!showException) { + row.exception = null; + } + List<JsonObject> codeLines = jo.getCollection("code"); + if (codeLines != null) { + for (JsonObject cl : codeLines) { + Code code = new Code(); + code.line = cl.getInteger("line"); + code.match = cl.getBooleanOrDefault("match", false); + code.code = cl.getString("code"); + row.code.add(code); + } + } rows.add(row); } @@ -385,6 +816,27 @@ public class CamelHistoryAction extends ActionWatchCommand { } } } + for (int i = 1; i < rows.size() - 1; i++) { + Row cur = rows.get(i); + + cur.history = new ArrayList<>(); + for (int j = 0; j < i; j++) { + Row r = rows.get(j); + History h = new History(); + h.index = j; + h.nodeId = r.nodeId; + h.routeId = r.routeId; + h.nodeShortName = r.nodeShortName; + h.nodeLabel = r.nodeLabel; + h.location = r.location; + h.elapsed = r.elapsed; + h.level = r.nodeLevel; + // TODO + h.code = "some code here"; + h.line = 123; + cur.history.add(h); + } + } } private Map<String, String> extractComponentModel(String uri, Row r) { @@ -470,11 +922,62 @@ public class CamelHistoryAction extends ActionWatchCommand { long elapsed; boolean done; boolean failed; + JsonObject aggregate; JsonObject endpoint; JsonObject endpointService; JsonObject message; JsonObject exception; String summary; + List<Code> code = new ArrayList<>(); + List<History> history = new ArrayList<>(); + } + + private static class History { + int index; + String routeId; + String nodeId; + String nodeShortName; + String nodeLabel; + int level; + long elapsed; + String location; + int line; + String code; + } + + private static class Code { + int line; + String code; + boolean match; + } + + private static class Panel { + String code = ""; + String history = ""; + int codeLength; + int historyLength; + + static Panel withCode(String code) { + return withCode(code, code.length()); + } + + static Panel withCode(String code, int length) { + Panel p = new Panel(); + p.code = code; + p.codeLength = length; + return p; + } + + Panel andHistory(String history) { + return andHistory(history, history.length()); + } + + Panel andHistory(String history, int length) { + this.history = history; + this.historyLength = length; + return this; + } + } }
