This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-23631-route-diagram-highlight-error-path in repository https://gitbox.apache.org/repos/asf/camel.git
commit 4c9c2f43508ed7f5b785e648100c4cbdec64cae0 Author: Claus Ibsen <[email protected]> AuthorDate: Wed May 27 22:15:36 2026 +0200 CAMEL-23631: Add --diagram option to camel get history and default theme to unicode Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../camel-jbang-cmd-route-diagram.adoc | 2 +- .../jbang-commands/camel-jbang-get-history.adoc | 2 + .../META-INF/camel-jbang-commands-metadata.json | 4 +- .../core/commands/action/CamelHistoryAction.java | 169 +++++++++++++++++++++ .../commands/action/CamelRouteDiagramAction.java | 2 +- 5 files changed, 175 insertions(+), 4 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc index eaa8d056e71f..dbcb7566d452 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc @@ -28,7 +28,7 @@ camel cmd route-diagram [options] | `--metric` | Whether to include live metrics (only possible for running Camel application) | true | boolean | `--node-label` | What text to display in diagram nodes: code, description, or both (default) | both | String | `--output` | Save diagram to a file (PNG for image themes, text for ascii theme) | | String -| `--theme` | Color theme preset (dark, light, transparent, ascii, unicode) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Use ascii/unicode for plain text output. Can also be set via DIAGRAM_COLORS env var. | transparent | String +| `--theme` | Color theme preset (dark, light, transparent, ascii, unicode) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Use ascii/unicode for plain text output. Can also be set via DIAGRAM_COLORS env var. | unicode | String | `--watch` | Execute periodically and showing output fullscreen | | boolean | `--width` | Image width in pixels (0 = auto) | 0 | int | `-h,--help` | Display the help and sub-commands | | boolean diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-history.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-history.adoc index 3aba5a683694..a3114809ca3d 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-history.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-history.adoc @@ -21,6 +21,7 @@ camel get history [options] | Option | Description | Default | Type | `--ago` | Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp. | | boolean | `--depth` | Depth of tracing. 0=Created Completed. 1=All events on 1st route, 2=All events on 1st 2nd depth, and so on. 9 = all events on every depth. | 9 | int +| `--diagram` | Display a route diagram with the message path highlighted | | boolean | `--it` | Interactive mode for enhanced history information | | boolean | `--limit-split` | Limit Split to a maximum number of entries to be displayed | | int | `--logging-color` | Use colored logging | true | boolean @@ -32,6 +33,7 @@ camel get history [options] | `--show-exchange-variables` | Show exchange variables in debug messages | true | boolean | `--show-headers` | Show message headers in debug messages | true | boolean | `--source` | Prefer to display source filename/code instead of IDs | | boolean +| `--theme` | Diagram color theme (ascii, unicode, dark, light, transparent, or custom) | unicode | String | `--timestamp` | Print timestamp. | true | boolean | `--watch` | Execute periodically and showing output fullscreen | | boolean | `-h,--help` | Display the help and sub-commands | | boolean diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index eebf916394be..afafb3cab311 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" [...] { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...] @@ -14,7 +14,7 @@ { "name": "eval", "fullName": "eval", "description": "Evaluate Camel expressions and scripts", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", "fullName": "eval expression", "description": "Evaluates Camel expression", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...] { "name": "explain", "fullName": "explain", "description": "Explain what a Camel route does using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai' (OpenAI-compatible), or 'anthropic' (A [...] { "name": "export", "fullName": "export", "description": "Export to other runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": "--build-property", "description": "Maven build properties, ex. --build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { "names": "--camel-spring-boot-version", "description": "Camel version to use with Spring Boot", "javaType": "java.lang.String", "ty [...] - { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean", "fullName": "get [...] + { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean", "fullName": "get [...] { "name": "harden", "fullName": "harden", "description": "Suggest security hardening for Camel routes using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...] { "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio web console", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ { "names": "--host", "description": "Hostname to bind the Hawtio web console to", "defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string" }, { "names": "--openUrl", "description": "To automatic open Hawtio web console in the web browser", "defaultValue": "true", "javaType": "boolean", "type": [...] { "name": "infra", "fullName": "infra", "description": "List and Run external services for testing and prototyping", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "infra [...] 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 5d9b64cd3c78..715e980656cb 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,6 +16,7 @@ */ package org.apache.camel.dsl.jbang.core.commands.action; +import java.awt.image.BufferedImage; import java.io.LineNumberReader; import java.nio.file.Files; import java.nio.file.Path; @@ -24,8 +25,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -36,7 +39,16 @@ import com.github.freva.asciitable.HorizontalAlign; import com.github.freva.asciitable.OverflowBehaviour; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.diagram.RouteDiagramAsciiRenderer; +import org.apache.camel.diagram.RouteDiagramHelper; +import org.apache.camel.diagram.RouteDiagramLayoutEngine; +import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute; +import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeLabelMode; +import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo; +import org.apache.camel.diagram.RouteDiagramRenderer; +import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.PathUtils; import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper; import org.apache.camel.support.LoggerHelper; import org.apache.camel.tooling.model.ComponentModel; @@ -52,6 +64,10 @@ import org.apache.camel.util.json.Jsoner; import org.fusesource.jansi.Ansi; import org.jline.keymap.KeyMap; import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.impl.TerminalGraphics; +import org.jline.terminal.impl.TerminalGraphicsManager; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStyle; import org.jline.utils.InfoCmp; @@ -121,6 +137,14 @@ public class CamelHistoryAction extends ActionWatchCommand { @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", description = "Use colored logging") boolean loggingColor = true; + @CommandLine.Option(names = { "--diagram" }, + description = "Display a route diagram with the message path highlighted") + boolean diagram; + + @CommandLine.Option(names = { "--theme" }, defaultValue = "unicode", + description = "Diagram color theme (ascii, unicode, dark, light, transparent, or custom)") + String theme = "unicode"; + private MessageTableHelper tableHelper; private final CamelCatalog camelCatalog = new DefaultCamelCatalog(true); @@ -146,6 +170,15 @@ public class CamelHistoryAction extends ActionWatchCommand { List<List<Row>> pids = loadRows(); if (!pids.isEmpty()) { + if (diagram) { + if (pids.size() > 1) { + printer().println("Diagram mode only operate on a single Camel application"); + return 1; + } + List<Row> rows = pids.get(0); + long pid = rows.get(0).pid; + return doDiagramCall(pid, rows); + } if (it) { if (pids.size() > 1) { printer().println("Interactive mode only operate on a single Camel application"); @@ -302,6 +335,142 @@ public class CamelHistoryAction extends ActionWatchCommand { return 0; } + private Integer doDiagramCall(long pid, List<Row> rows) throws Exception { + System.setProperty("java.awt.headless", "true"); + + // extract node IDs and route order from history + Set<String> nodeIds = new LinkedHashSet<>(); + Set<String> routeOrderSet = new LinkedHashSet<>(); + for (Row r : rows) { + if (r.nodeId != null) { + nodeIds.add(r.nodeId); + } + if (r.routeId != null) { + routeOrderSet.add(r.routeId); + } + } + + if (nodeIds.isEmpty()) { + printer().println("No node IDs found in message history"); + return 1; + } + + // auto-detect style from exchange status + Row last = rows.get(rows.size() - 1); + RouteDiagramHelper.HighlightStyle hlStyle = last.failed + ? RouteDiagramHelper.HighlightStyle.FAIL + : RouteDiagramHelper.HighlightStyle.SUCCESS; + + // request route structure from the running process + String pidStr = Long.toString(pid); + Path outputFile = getOutputFile(pidStr); + PathUtils.deleteFile(outputFile); + + JsonObject action = new JsonObject(); + action.put("action", "route-structure"); + action.put("filter", "*"); + action.put("brief", false); + action.put("metric", false); + Path actionFile = getActionFile(pidStr); + try { + Files.writeString(actionFile, action.toJson()); + } catch (Exception e) { + // ignore + } + + JsonObject structureJson = getJsonObject(outputFile); + if (structureJson == null) { + printer().println("Response from running Camel with PID " + pid + " not received within 5 seconds"); + return 1; + } + + try { + List<RouteInfo> routes = RouteDiagramHelper.parseRoutes(structureJson); + if (routes.isEmpty()) { + printer().println("No routes found"); + return 1; + } + + // add route and from node IDs for each route that has highlighted nodes + // (the history trace only has processor nodeIds, not the structural route/from nodes) + for (RouteInfo route : routes) { + boolean routeHasHighlight = route.nodes.stream().anyMatch(n -> n.id != null && nodeIds.contains(n.id)); + if (routeHasHighlight) { + for (RouteDiagramLayoutEngine.NodeInfo node : route.nodes) { + if (node.id != null && ("route".equals(node.type) || "from".equals(node.type))) { + nodeIds.add(node.id); + } + } + } + } + + // filter and order routes by highlighted path + RouteDiagramHelper.HighlightInfo highlightInfo + = new RouteDiagramHelper.HighlightInfo(nodeIds, new ArrayList<>(routeOrderSet), hlStyle); + routes = RouteDiagramHelper.filterAndOrderRoutes(routes, highlightInfo); + if (routes.isEmpty()) { + printer().println("No routes contain highlighted nodes from message history"); + return 1; + } + + // layout + RouteDiagramLayoutEngine engine + = new RouteDiagramLayoutEngine(180, 12, NodeLabelMode.BOTH); + + List<LayoutRoute> layoutRoutes = new ArrayList<>(); + int currentY = RouteDiagramLayoutEngine.PADDING; + for (RouteInfo route : routes) { + LayoutRoute lr = engine.layoutRoute(route, currentY); + layoutRoutes.add(lr); + currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP; + } + + boolean textMode = isTextTheme(); + if (textMode) { + RouteDiagramAsciiRenderer asciiRenderer + = new RouteDiagramAsciiRenderer(engine.getNodeWidth(), isUnicodeTheme()); + String ascii = asciiRenderer.renderDiagramAnsi(layoutRoutes, currentY, nodeIds, hlStyle); + printer().println(ascii); + } else { + DiagramColors colors = DiagramColors.parse(theme); + RouteDiagramRenderer renderer = new RouteDiagramRenderer( + engine.getNodeWidth(), 12 * RouteDiagramLayoutEngine.SCALE, engine.getNodeTextPadding(), false); + + BufferedImage image; + try { + image = renderer.renderDiagram(layoutRoutes, currentY, colors, nodeIds, hlStyle); + } catch (IllegalStateException e) { + printer().println(e.getMessage()); + return 1; + } + + try (Terminal terminal = TerminalBuilder.builder().system(true).build()) { + TerminalGraphics tg = TerminalGraphicsManager.getBestProtocol(terminal).orElse(null); + if (tg == null) { + printer().println( + "Terminal does not support graphics. Use --theme=ascii or --theme=unicode for text output."); + return 1; + } + TerminalGraphics.ImageOptions opts = new TerminalGraphics.ImageOptions().preserveAspectRatio(true); + tg.displayImage(terminal, image, opts); + terminal.writer().println(); + terminal.flush(); + } + } + return 0; + } finally { + PathUtils.deleteFile(outputFile); + } + } + + private boolean isTextTheme() { + return "ascii".equalsIgnoreCase(theme) || "unicode".equalsIgnoreCase(theme); + } + + private boolean isUnicodeTheme() { + return "unicode".equalsIgnoreCase(theme); + } + private List<AttributedString> interactiveContent( List<Row> rows, AtomicInteger rowIndex, AtomicInteger pageIndex, Size size) { List<AttributedString> answer = new ArrayList<>(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java index fa89ddca2317..5b5454f867f7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java @@ -81,7 +81,7 @@ public class CamelRouteDiagramAction extends ActionWatchCommand { + "ANSI color names (e.g. from=seagreen:to=steelblue). " + "Use bg= for transparent. Use ascii/unicode for plain text output. " + "Can also be set via DIAGRAM_COLORS env var.", - defaultValue = "transparent") + defaultValue = "unicode") String theme; @CommandLine.Option(names = { "--font-size" },
