You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2023/04/14 08:38:46 UTC

[camel] 01/03: CAMEL-19236: camel-jbang - Command to send a message.

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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3a26c35414c3b5536d6efa998128949a32fdaffe
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Apr 14 08:42:58 2023 +0200

    CAMEL-19236: camel-jbang - Command to send a message.
---
 .../org/apache/camel/support/MessageHelper.java    |  79 ++--
 .../camel/cli/connector/LocalCliConnector.java     | 143 ++++++++
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../core/commands/action/CamelSendAction.java      | 272 ++++++++++++++
 .../core/commands/action/CamelTraceAction.java     | 341 ++---------------
 .../core/commands/action/MessageTableHelper.java   | 408 +++++++++++++++++++++
 6 files changed, 902 insertions(+), 343 deletions(-)

diff --git a/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
index cbc6289d849..e903dbdb25a 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
@@ -808,7 +808,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message the message
      * @return         the JSon
@@ -818,7 +818,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message     the message
      * @param  includeBody whether or not to include the message body
@@ -829,7 +829,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message     the message
      * @param  includeBody whether or not to include the message body
@@ -841,7 +841,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message      the message
      * @param  includeBody  whether or not to include the message body
@@ -859,7 +859,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message                   the message
      * @param  includeExchangeProperties whether or not to include exchange properties
@@ -877,6 +877,35 @@ public final class MessageHelper {
             Message message, boolean includeExchangeProperties, boolean includeBody, int indent,
             boolean allowCachedStreams, boolean allowStreams, boolean allowFiles, int maxChars, boolean pretty) {
 
+        JsonObject jo = dumpAsJSonObject(message, includeExchangeProperties, includeBody, allowCachedStreams, allowStreams, allowFiles, maxChars);
+        String answer = jo.toJson();
+        if (pretty) {
+            if (indent > 0) {
+                answer = Jsoner.prettyPrint(answer, indent);
+            } else {
+                answer = Jsoner.prettyPrint(answer);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Dumps the message as a generic JSon Object.
+     *
+     * @param  message                   the message
+     * @param  includeExchangeProperties whether or not to include exchange properties
+     * @param  includeBody               whether or not to include the message body
+     * @param  allowCachedStreams        whether to include message body if they are stream cached based
+     * @param  allowStreams              whether to include message body if they are stream based
+     * @param  allowFiles                whether to include message body if they are file based
+     * @param  maxChars                  clip body after maximum chars (to avoid very big messages). Use 0 or negative
+     *                                   value to not limit at all.
+     * @return                           the JSon Object
+     */
+    public static JsonObject dumpAsJSonObject(
+            Message message, boolean includeExchangeProperties, boolean includeBody,
+            boolean allowCachedStreams, boolean allowStreams, boolean allowFiles, int maxChars) {
+
         JsonObject root = new JsonObject();
         JsonObject jo = new JsonObject();
         root.put("message", jo);
@@ -981,15 +1010,7 @@ public final class MessageHelper {
             }
         }
 
-        String answer = root.toJson();
-        if (pretty) {
-            if (indent > 0) {
-                answer = Jsoner.prettyPrint(answer, indent);
-            } else {
-                answer = Jsoner.prettyPrint(answer);
-            }
-        }
-        return answer;
+        return root;
     }
 
     /**
@@ -1029,13 +1050,31 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the exception as a generic JSon structure.
+     * Dumps the exception as a generic JSon structure as text.
      *
      * @param  indent number of spaces to indent
      * @param  pretty whether to pretty print JSon
      * @return        the JSon
      */
     public static String dumpExceptionAsJSon(Throwable exception, int indent, boolean pretty) {
+        JsonObject jo = dumpExceptionAsJSonObject(exception);
+        String answer = jo.toJson();
+        if (pretty) {
+            if (indent > 0) {
+                answer = Jsoner.prettyPrint(answer, indent);
+            } else {
+                answer = Jsoner.prettyPrint(answer);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Dumps the exception as a generic JSon object.
+     *
+     * @return        the JSon object
+     */
+    public static JsonObject dumpExceptionAsJSonObject(Throwable exception) {
         JsonObject root = new JsonObject();
         JsonObject jo = new JsonObject();
         root.put("exception", jo);
@@ -1058,15 +1097,7 @@ public final class MessageHelper {
         } catch (Throwable e) {
             // ignore as the body is for logging purpose
         }
-        String answer = root.toJson();
-        if (pretty) {
-            if (indent > 0) {
-                answer = Jsoner.prettyPrint(answer, indent);
-            } else {
-                answer = Jsoner.prettyPrint(answer);
-            }
-        }
-        return answer;
+        return root;
     }
 
 }
diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index b63e17482d9..6dd479e5bd0 100644
--- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -18,12 +18,15 @@ package org.apache.camel.cli.connector;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.InputStream;
 import java.lang.management.ClassLoadingMXBean;
 import java.lang.management.GarbageCollectorMXBean;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryMXBean;
 import java.lang.management.RuntimeMXBean;
 import java.lang.management.ThreadMXBean;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -37,7 +40,12 @@ import java.util.stream.Collectors;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelContextAware;
+import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.NoSuchEndpointException;
+import org.apache.camel.Processor;
+import org.apache.camel.ProducerTemplate;
 import org.apache.camel.Route;
 import org.apache.camel.api.management.ManagedCamelContext;
 import org.apache.camel.console.DevConsole;
@@ -46,12 +54,15 @@ import org.apache.camel.spi.CliConnector;
 import org.apache.camel.spi.CliConnectorFactory;
 import org.apache.camel.spi.ContextReloadStrategy;
 import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.EndpointHelper;
+import org.apache.camel.support.MessageHelper;
 import org.apache.camel.support.PatternHelper;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.concurrent.ThreadHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -66,6 +77,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
 
     private static final Logger LOG = LoggerFactory.getLogger(LocalCliConnector.class);
 
+    private static final int BODY_MAX_CHARS = 128 * 1024;
+
     private final CliConnectorFactory cliConnectorFactory;
     private CamelContext camelContext;
     private int delay = 2000;
@@ -75,6 +88,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
     private final AtomicBoolean terminating = new AtomicBoolean();
     private ScheduledExecutorService executor;
     private volatile ExecutorService terminateExecutor;
+    private ProducerTemplate producer;
     private File lockFile;
     private File statusFile;
     private File actionFile;
@@ -126,6 +140,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
             }
         }
         platformVersion = cliConnectorFactory.getRuntimeVersion();
+        producer = camelContext.createProducerTemplate();
 
         // create thread from JDK so it is not managed by Camel because we want the pool to be independent when
         // camel is being stopped which otherwise can lead to stopping the thread pool while the task is running
@@ -294,6 +309,133 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
                     LOG.trace("Updating output file: {}", outputFile);
                     IOHelper.writeText(json.toJson(), outputFile);
                 }
+            } else if ("send".equals(action)) {
+                StopWatch watch = new StopWatch();
+                long timestamp = System.currentTimeMillis();
+                String endpoint = root.getString("endpoint");
+                String body = root.getString("body");
+                String exchangePattern = root.getString("exchangePattern");
+                Collection<JsonObject> headers = root.getCollection("headers");
+                if (body != null) {
+                    InputStream is = null;
+                    Object b = body;
+                    Map<String, Object> map = null;
+                    if (body.startsWith("file:")) {
+                        File file = new File(body.substring(5));
+                        is = new FileInputStream(file);
+                        b = is;
+                    }
+                    if (headers != null) {
+                        map = new HashMap<>();
+                        for (JsonObject jo : headers) {
+                            map.put(jo.getString("key"), jo.getString("value"));
+                        }
+                    }
+                    final Object inputBody = b;
+                    final Map<String, Object> inputHeaders = map;
+                    Exchange out;
+                    Endpoint target = null;
+                    if (endpoint == null) {
+                        List<Route> routes = camelContext.getRoutes();
+                        if (!routes.isEmpty()) {
+                            // grab endpoint from 1st route
+                            target = routes.get(0).getEndpoint();
+                        }
+                    } else {
+                        // is the endpoint a pattern or route id
+                        boolean scheme = endpoint.contains(":");
+                        boolean pattern = endpoint.endsWith("*");
+                        if (!scheme || pattern) {
+                            if (!scheme) {
+                                endpoint = endpoint + "*";
+                            }
+                            for (Route route : camelContext.getRoutes()) {
+                                Endpoint e = route.getEndpoint();
+                                if (EndpointHelper.matchEndpoint(camelContext, e.getEndpointUri(), endpoint)) {
+                                    target = e;
+                                    break;
+                                }
+                            }
+                            if (target == null) {
+                                // okay it may refer to a route id
+                                for (Route route : camelContext.getRoutes()) {
+                                    String id = route.getRouteId();
+                                    Endpoint e = route.getEndpoint();
+                                    if (EndpointHelper.matchEndpoint(camelContext, id, endpoint)) {
+                                        target = e;
+                                        break;
+                                    }
+                                }
+                            }
+                        } else {
+                            target = camelContext.getEndpoint(endpoint);
+                        }
+                    }
+
+                    if (target != null) {
+                        out = producer.send(target, new Processor() {
+                            @Override
+                            public void process(Exchange exchange) throws Exception {
+                                exchange.getMessage().setBody(inputBody);
+                                if (inputHeaders != null) {
+                                    exchange.getMessage().setHeaders(inputHeaders);
+                                }
+                                exchange.setPattern(
+                                        "InOut".equals(exchangePattern) ? ExchangePattern.InOut : ExchangePattern.InOnly);
+                            }
+                        });
+                        IOHelper.close(is);
+                        LOG.trace("Updating output file: {}", outputFile);
+                        if (out.getException() != null) {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "failed");
+                            // avoid double wrap
+                            jo.put("exception",
+                                    MessageHelper.dumpExceptionAsJSonObject(out.getException()).getMap("exception"));
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        } else if ("InOut".equals(exchangePattern)) {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "success");
+                            // avoid double wrap
+                            jo.put("message", MessageHelper.dumpAsJSonObject(out.getMessage(), true, true, true, true, true,
+                                    BODY_MAX_CHARS).getMap("message"));
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        } else {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "success");
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        }
+                    } else {
+                        // there is no valid endpoint
+                        JsonObject jo = new JsonObject();
+                        jo.put("endpoint", root.getString("endpoint"));
+                        jo.put("exchangeId", "");
+                        jo.put("exchangePattern", exchangePattern);
+                        jo.put("timestamp", timestamp);
+                        jo.put("elapsed", watch.taken());
+                        jo.put("status", "failed");
+                        // avoid double wrap
+                        jo.put("exception",
+                                MessageHelper.dumpExceptionAsJSonObject(new NoSuchEndpointException(root.getString("endpoint")))
+                                        .getMap("exception"));
+                        IOHelper.writeText(jo.toJson(), outputFile);
+                    }
+                }
             }
 
             // action done so delete file
@@ -629,6 +771,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
             camelContext.getExecutorServiceManager().shutdown(executor);
             executor = null;
         }
+        ServiceHelper.stopService(producer);
     }
 
     private static String getPid() {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index d0b9bfa7e9f..f536129e90d 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -27,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.CamelReloadAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelResetStatsAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStartAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStopAction;
+import org.apache.camel.dsl.jbang.core.commands.action.CamelSendAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceTop;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump;
@@ -114,6 +115,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("stop-route", new CommandLine(new CamelRouteStopAction(main)))
                         .addSubcommand("reset-stats", new CommandLine(new CamelResetStatsAction(main)))
                         .addSubcommand("reload", new CommandLine(new CamelReloadAction(main)))
+                        .addSubcommand("send", new CommandLine(new CamelSendAction(main)))
                         .addSubcommand("thread-dump", new CommandLine(new CamelThreadDump(main)))
                         .addSubcommand("logger", new CommandLine(new LoggerAction(main)))
                         .addSubcommand("gc", new CommandLine(new CamelGCAction(main))))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
new file mode 100644
index 00000000000..a8cdf8d650e
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.TimeUtils;
+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 = "send",
+                     description = "Sends a message to a system via an existing running Camel integration")
+public class CamelSendAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "1")
+    String name;
+
+    @CommandLine.Option(names = { "--endpoint" },
+                        description = "Endpoint where to send the message (can be uri, pattern, or refer to a route id)")
+    String endpoint;
+
+    @CommandLine.Option(names = { "--reply" },
+                        description = "Whether to expect a reply message (InOut vs InOut messaging style)")
+    boolean reply;
+
+    @CommandLine.Option(names = { "--reply-file" },
+                        description = "Saves reply message to the file with the given name (override if exists)")
+    String replyFile;
+
+    @CommandLine.Option(names = { "--body" }, required = true,
+                        description = "Message body to send (prefix with file: to refer to loading message body from file)")
+    String body;
+
+    @CommandLine.Option(names = { "--header" },
+                        description = "Message header (key=value)")
+    List<String> headers;
+
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
+                        description = "Timeout in millis waiting for message to be sent (and reply message if InOut messaging)")
+    long timeout = 20000;
+
+    @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue = "false",
+                        description = "Show exchange properties in traced messages")
+    boolean showExchangeProperties;
+
+    @CommandLine.Option(names = { "--show-headers" }, defaultValue = "true",
+                        description = "Show message headers in traced messages")
+    boolean showHeaders = true;
+
+    @CommandLine.Option(names = { "--show-body" }, defaultValue = "true",
+                        description = "Show message body in traced 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 = { "--logging-color" }, defaultValue = "true", description = "Use colored logging")
+    boolean loggingColor = true;
+
+    @CommandLine.Option(names = { "--pretty" },
+                        description = "Pretty print message body when using JSon or XML format")
+    boolean pretty;
+
+    private volatile long pid;
+
+    private MessageTableHelper tableHelper;
+
+    public CamelSendAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            return 0;
+        } else if (pids.size() > 1) {
+            System.out.println("Name or pid " + name + " matches " + pids.size()
+                               + " running Camel integrations. Specify a name or PID that matches exactly one.");
+            return 0;
+        }
+
+        this.pid = pids.get(0);
+
+        // ensure output file is deleted before executing action
+        File outputFile = getOutputFile(Long.toString(pid));
+        FileUtil.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "send");
+        root.put("endpoint", endpoint);
+        String mep = (reply || replyFile != null) ? "InOut" : "InOnly";
+        root.put("exchangePattern", mep);
+        root.put("body", body);
+        if (headers != null) {
+            JsonArray arr = new JsonArray();
+            for (String h : headers) {
+                JsonObject jo = new JsonObject();
+                if (!h.contains("=")) {
+                    System.out.println("Header must be in key=value format, was: " + h);
+                    return 0;
+                }
+                jo.put("key", StringHelper.before(h, "="));
+                jo.put("value", StringHelper.after(h, "="));
+                arr.add(jo);
+            }
+            root.put("headers", arr);
+        }
+        File f = getActionFile(Long.toString(pid));
+        try {
+            IOHelper.writeText(root.toJson(), f);
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            printStatusLine(jo);
+            String exchangeId = jo.getString("exchangeId");
+            JsonObject message = jo.getMap("message");
+            JsonObject cause = jo.getMap("exception");
+            if (message != null || cause != null) {
+                if (replyFile != null) {
+                    File target = new File(replyFile);
+                    String json = jo.toJson();
+                    if (pretty) {
+                        json = Jsoner.prettyPrint(json, 2);
+                    }
+                    IOHelper.writeText(json, target);
+                }
+                if (!showExchangeProperties && message != null) {
+                    message.remove("exchangeProperties");
+                }
+                if (!showHeaders && message != null) {
+                    message.remove("headers");
+                }
+                if (!showBody && message != null) {
+                    message.remove("body");
+                }
+                if (!showException && cause != null) {
+                    cause = null;
+                }
+                if (replyFile == null) {
+                    tableHelper = new MessageTableHelper();
+                    tableHelper.setPretty(pretty);
+                    tableHelper.setLoggingColor(loggingColor);
+                    tableHelper.setShowExchangeProperties(showExchangeProperties);
+                    String table = tableHelper.getDataAsTable(exchangeId, mep, jo, message, cause);
+                    System.out.println(table);
+                }
+            }
+        }
+
+        // delete output file after use
+        FileUtil.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    private void printStatusLine(JsonObject jo) {
+        // timstamp
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        String ts = sdf.format(new Date(jo.getLong("timestamp")));
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
+        } else {
+            System.out.print(ts);
+        }
+        // pid
+        System.out.print("  ");
+        String p = String.format("%5.5s", this.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 {
+            System.out.print(p);
+            System.out.print(" --- ");
+        }
+        // endpoint
+        String ids = jo.getString("endpoint");
+        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 {
+            System.out.print(ids);
+        }
+        System.out.print(" : ");
+        // status
+        System.out.print(getStatus(jo));
+        // elapsed
+        String e = TimeUtils.printDuration(jo.getLong("elapsed"), true);
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" + e + ")").reset());
+        } else {
+            System.out.print("(" + e + ")");
+        }
+        System.out.println();
+    }
+
+    private String getStatus(JsonObject r) {
+        boolean failed = "failed".equals(r.getString("status"));
+        boolean reply = r.containsKey("message");
+        String status;
+        if (failed) {
+            status = "Failed (exception)";
+        } else if (replyFile != null) {
+            status = "Reply saved to file (success)";
+        } else if (reply) {
+            status = "Reply received (success)";
+        } else {
+            status = "Sent (success)";
+        }
+        if (loggingColor) {
+            return Ansi.ansi().fg(failed ? Ansi.Color.RED : Ansi.Color.GREEN).a(status).reset().toString();
+        } else {
+            return status;
+        }
+    }
+
+    protected JsonObject waitForOutputFile(File outputFile) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < timeout) {
+            try {
+                // give time for response to be ready
+                Thread.sleep(20);
+
+                if (outputFile.exists()) {
+                    FileInputStream fis = new FileInputStream(outputFile);
+                    String text = IOHelper.loadText(fis);
+                    IOHelper.close(fis);
+                    return (JsonObject) Jsoner.deserialize(text);
+                }
+
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
index 97de403abc0..6b449764e43 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
@@ -23,7 +23,6 @@ import java.io.LineNumberReader;
 import java.text.SimpleDateFormat;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -36,15 +35,9 @@ import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.regex.Pattern;
 
-import com.github.freva.asciitable.AsciiTable;
-import com.github.freva.asciitable.Column;
-import com.github.freva.asciitable.HorizontalAlign;
-import com.github.freva.asciitable.OverflowBehaviour;
 import org.apache.camel.catalog.impl.TimePatternConverter;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.dsl.jbang.core.common.JSonHelper;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
-import org.apache.camel.dsl.jbang.core.common.XmlHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.TimeUtils;
@@ -152,6 +145,8 @@ public class CamelTraceAction extends ActionBaseCommand {
     private int nameMaxWidth;
     private boolean prefixShown;
 
+    private MessageTableHelper tableHelper;
+
     private final Map<String, Ansi.Color> nameColors = new HashMap<>();
     private final Map<String, Ansi.Color> exchangeIdColors = new HashMap<>();
     private int exchangeIdColorsIndex = 1;
@@ -162,6 +157,25 @@ public class CamelTraceAction extends ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
+        // setup table helper
+        tableHelper = new MessageTableHelper();
+        tableHelper.setPretty(pretty);
+        tableHelper.setLoggingColor(loggingColor);
+        tableHelper.setShowExchangeProperties(showExchangeProperties);
+        tableHelper.setExchangeIdColorChooser(value -> {
+            Ansi.Color color = exchangeIdColors.get(value);
+            if (color == null) {
+                // grab a new color
+                exchangeIdColorsIndex++;
+                if (exchangeIdColorsIndex > 6) {
+                    exchangeIdColorsIndex = 2;
+                }
+                color = Ansi.Color.values()[exchangeIdColorsIndex];
+                exchangeIdColors.put(value, color);
+            }
+            return color;
+        });
+
         Map<Long, Pid> pids = new LinkedHashMap<>();
 
         if (latest) {
@@ -675,130 +689,7 @@ public class CamelTraceAction extends ActionBaseCommand {
     }
 
     private String getDataAsTable(Row r) {
-        List<TableRow> rows = new ArrayList<>();
-
-        TableRow eRow = new TableRow("Exchange", r.message.getString("exchangeType"), r.exchangePattern, r.exchangeId);
-        String tab1 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
-        String tab1b = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.CENTER)
-                        .minWidth(18).maxWidth(18).with(TableRow::mepAsKey),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .maxWidth(80).with(TableRow::exchangeIdAsValue)));
-        // exchange properties
-        JsonArray arr = r.message.getCollection("exchangeProperties");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        // internal exchange properties
-        arr = r.message.getCollection("internalExchangeProperties");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        String tab2 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
-        rows.clear();
-
-        // message type before headers
-        TableRow msgRow = new TableRow("Message", r.message.getString("messageType"), null, null);
-        String tab3 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(msgRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
-        arr = r.message.getCollection("headers");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Header", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        // headers
-        String tab4 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
-
-        // body and type
-        JsonObject jo = r.message.getMap("body");
-        TableRow bodyRow = new TableRow("Body", jo.getString("type"), null, jo.get("value"), jo.getLong("position"));
-        String tab5 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAndLengthAsString)));
-        // body value only (span)
-        String tab6 = null;
-        if (bodyRow.value != null) {
-            tab6 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
-                            .with(b -> pretty ? bodyRow.valueAsStringPretty() : bodyRow.valueAsString())));
-        }
-        String tab7 = null;
-        jo = r.exception;
-        if (jo != null) {
-            eRow = new TableRow("Exception", jo.getString("type"), null, jo.get("message"));
-            tab7 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .minWidth(showExchangeProperties ? 12 : 10)
-                            .with(TableRow::kindAsStringRed),
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .maxWidth(40, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsStringRed)));
-        }
-        // stacktrace only (span)
-        String tab8 = null;
-        if (jo != null) {
-            eRow = new TableRow("Stacktrace", null, null, jo.get("stackTrace"));
-            tab8 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
-                            .with(TableRow::valueAsStringRed)));
-        }
-        String answer = "";
-        if (tab1 != null && tab1b != null && !tab1.isEmpty()) {
-            answer = answer + tab1 + tab1b + System.lineSeparator();
-        }
-        if (tab2 != null && !tab2.isEmpty()) {
-            answer = answer + tab2 + System.lineSeparator();
-        }
-        if (tab3 != null && !tab3.isEmpty()) {
-            answer = answer + tab3 + System.lineSeparator();
-        }
-        if (tab4 != null && !tab4.isEmpty()) {
-            answer = answer + tab4 + System.lineSeparator();
-        }
-        if (tab5 != null && !tab5.isEmpty()) {
-            answer = answer + tab5 + System.lineSeparator();
-        }
-        if (tab6 != null && !tab6.isEmpty()) {
-            answer = answer + tab6 + System.lineSeparator();
-        }
-        if (tab7 != null && !tab7.isEmpty()) {
-            answer = answer + tab7 + System.lineSeparator();
-        }
-        if (tab8 != null && !tab8.isEmpty()) {
-            answer = answer + tab8 + System.lineSeparator();
-        }
-        return answer;
+        return tableHelper.getDataAsTable(r.exchangeId, r.exchangePattern, null, r.message, r.exception);
     }
 
     private String getElapsed(Row r) {
@@ -891,192 +782,4 @@ public class CamelTraceAction extends ActionBaseCommand {
 
     }
 
-    private class TableRow {
-        String kind;
-        String type;
-        String key;
-        Object value;
-        Long position;
-
-        TableRow(String kind, String type, String key, Object value) {
-            this(kind, type, key, value, null);
-        }
-
-        TableRow(String kind, String type, String key, Object value, Long position) {
-            this.kind = kind;
-            this.type = type;
-            this.key = key;
-            this.value = value;
-            this.position = position;
-        }
-
-        String valueAsString() {
-            return value != null ? value.toString() : "null";
-        }
-
-        String valueAsStringPretty() {
-            if (value == null) {
-                return "null";
-            }
-            boolean json = false;
-            String s = value.toString();
-            if (!s.isEmpty()) {
-                try {
-                    s = Jsoner.unescape(s);
-                    if (loggingColor) {
-                        s = JSonHelper.colorPrint(s, 2, true);
-                    } else {
-                        s = JSonHelper.prettyPrint(s, 2);
-                    }
-                    if (s != null && !s.isEmpty()) {
-                        json = true;
-                    }
-                } catch (Throwable e) {
-                    // ignore as not json
-                }
-                if (s == null || s.isEmpty()) {
-                    s = value.toString();
-                }
-                if (!json) {
-                    // try with xml
-                    try {
-                        s = Jsoner.unescape(s);
-                        if (loggingColor) {
-                            s = XmlHelper.colorPrint(s, 2, true);
-                        } else {
-                            s = XmlHelper.prettyPrint(s, 2);
-                        }
-                    } catch (Throwable e) {
-                        // ignore as not xml
-                    }
-                }
-                if (s == null || s.isEmpty()) {
-                    s = value.toString();
-                }
-            }
-            if (s == null) {
-                return "null";
-            }
-            return s;
-        }
-
-        String valueAsStringRed() {
-            if (value != null) {
-                if (loggingColor) {
-                    return Ansi.ansi().fgRed().a(value).reset().toString();
-                } else {
-                    return value.toString();
-                }
-            }
-            return "";
-        }
-
-        String keyAsString() {
-            if (key == null) {
-                return "";
-            }
-            return key;
-        }
-
-        String kindAsString() {
-            return kind;
-        }
-
-        String kindAsStringRed() {
-            if (loggingColor) {
-                return Ansi.ansi().fgRed().a(kind).reset().toString();
-            } else {
-                return kind;
-            }
-        }
-
-        String typeAsString() {
-            String s;
-            if (type == null) {
-                s = "null";
-            } else if (type.startsWith("java.util.concurrent")) {
-                s = type.substring(21);
-            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
-                s = type.substring(10);
-            } else if (type.startsWith("org.apache.camel.support.")) {
-                s = type.substring(25);
-            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
-                s = type.substring(34);
-            } else {
-                s = type;
-            }
-            s = "(" + s + ")";
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String typeAndLengthAsString() {
-            String s;
-            if (type == null) {
-                s = "null";
-            } else if (type.startsWith("java.util.concurrent")) {
-                s = type.substring(21);
-            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
-                s = type.substring(10);
-            } else if (type.startsWith("org.apache.camel.support.")) {
-                s = type.substring(25);
-            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
-                s = type.substring(34);
-            } else {
-                s = type;
-            }
-            s = "(" + s + ")";
-            int l = valueLength();
-            long p = position != null ? position : -1;
-            if (l != -1 & p != -1) {
-                s = s + " (pos: " + p + " length: " + l + ")";
-            } else if (l != -1) {
-                s = s + " (length: " + l + ")";
-            } else if (p != -1) {
-                s = s + " (pos: " + p + ")";
-            }
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String mepAsKey() {
-            String s = key;
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightMagenta().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String exchangeIdAsValue() {
-            String s = value.toString();
-            if (loggingColor) {
-                Ansi.Color color = exchangeIdColors.get(s);
-                if (color == null) {
-                    // grab a new color
-                    exchangeIdColorsIndex++;
-                    if (exchangeIdColorsIndex > 6) {
-                        exchangeIdColorsIndex = 2;
-                    }
-                    color = Ansi.Color.values()[exchangeIdColorsIndex];
-                    exchangeIdColors.put(s, color);
-                }
-                s = Ansi.ansi().fg(color).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        int valueLength() {
-            if (value == null) {
-                return -1;
-            } else {
-                return valueAsString().length();
-            }
-        }
-
-    }
-
 }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
new file mode 100644
index 00000000000..cf7282e899a
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.common.JSonHelper;
+import org.apache.camel.dsl.jbang.core.common.XmlHelper;
+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;
+
+/**
+ * Helper to output message details (headers, body) in a table like structure with pretty and color supported.
+ */
+public class MessageTableHelper {
+
+    @FunctionalInterface
+    interface ColorChooser {
+        Ansi.Color color(String value);
+    }
+
+    private boolean loggingColor;
+    private boolean pretty;
+    private boolean showExchangeProperties;
+    private ColorChooser exchangeIdColorChooser;
+
+    public boolean isLoggingColor() {
+        return loggingColor;
+    }
+
+    public void setLoggingColor(boolean loggingColor) {
+        this.loggingColor = loggingColor;
+    }
+
+    public boolean isPretty() {
+        return pretty;
+    }
+
+    public void setPretty(boolean pretty) {
+        this.pretty = pretty;
+    }
+
+    public boolean isShowExchangeProperties() {
+        return showExchangeProperties;
+    }
+
+    public void setShowExchangeProperties(boolean showExchangeProperties) {
+        this.showExchangeProperties = showExchangeProperties;
+    }
+
+    public ColorChooser getExchangeIdColorChooser() {
+        return exchangeIdColorChooser;
+    }
+
+    public void setExchangeIdColorChooser(ColorChooser exchangeIdColorChooser) {
+        this.exchangeIdColorChooser = exchangeIdColorChooser;
+    }
+
+    public String getDataAsTable(
+            String exchangeId, String exchangePattern,
+            JsonObject endpoint, JsonObject root, JsonObject cause) {
+
+        List<TableRow> rows = new ArrayList<>();
+        TableRow eRow;
+        String tab0 = null, tab1 = null, tab1b = null, tab2 = null, tab3 = null, tab4 = null, tab5 = null, tab6 = null;
+
+        if (endpoint != null) {
+            eRow = new TableRow("Endpoint", endpoint.getString("endpoint"), null, null);
+            tab0 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+        }
+
+        if (root != null) {
+            eRow = new TableRow("Exchange", root.getString("exchangeType"), exchangePattern, exchangeId);
+            tab1 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+            tab1b = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.CENTER)
+                            .minWidth(18).maxWidth(18).with(TableRow::mepAsKey),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .maxWidth(80).with(TableRow::exchangeIdAsValue)));
+            // exchange properties
+            JsonArray arr = root.getCollection("exchangeProperties");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            // internal exchange properties
+            arr = root.getCollection("internalExchangeProperties");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            tab2 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
+            rows.clear();
+
+            // message type before headers
+            TableRow msgRow = new TableRow("Message", root.getString("messageType"), null, null);
+            tab3 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(msgRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+            arr = root.getCollection("headers");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Header", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            // headers
+            tab4 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
+
+            // body and type
+            JsonObject jo = root.getMap("body");
+            if (jo != null) {
+                TableRow bodyRow = new TableRow("Body", jo.getString("type"), null, jo.get("value"), jo.getLong("position"));
+                tab5 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
+                        new Column().dataAlign(HorizontalAlign.LEFT)
+                                .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                        new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAndLengthAsString)));
+                // body value only (span)
+                if (bodyRow.value != null) {
+                    tab6 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
+                            new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
+                                    .with(b -> pretty ? bodyRow.valueAsStringPretty() : bodyRow.valueAsString())));
+                }
+            }
+        }
+
+        String tab7 = null;
+        if (cause != null) {
+            eRow = new TableRow("Exception", cause.getString("type"), null, cause.get("message"));
+            tab7 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10)
+                            .with(TableRow::kindAsStringRed),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(40, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsStringRed)));
+        }
+        // stacktrace only (span)
+        String tab8 = null;
+        if (cause != null) {
+            String value = cause.getString("stackTrace");
+            value = Jsoner.unescape(value);
+            eRow = new TableRow("Stacktrace", null, null, value);
+            tab8 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
+                            .with(TableRow::valueAsStringRed)));
+        }
+        String answer = "";
+        if (tab0 != null && !tab0.isEmpty()) {
+            answer = answer + tab0 + System.lineSeparator();
+        }
+        if (tab1 != null && tab1b != null && !tab1.isEmpty()) {
+            answer = answer + tab1 + tab1b + System.lineSeparator();
+        }
+        if (tab2 != null && !tab2.isEmpty()) {
+            answer = answer + tab2 + System.lineSeparator();
+        }
+        if (tab3 != null && !tab3.isEmpty()) {
+            answer = answer + tab3 + System.lineSeparator();
+        }
+        if (tab4 != null && !tab4.isEmpty()) {
+            answer = answer + tab4 + System.lineSeparator();
+        }
+        if (tab5 != null && !tab5.isEmpty()) {
+            answer = answer + tab5 + System.lineSeparator();
+        }
+        if (tab6 != null && !tab6.isEmpty()) {
+            answer = answer + tab6 + System.lineSeparator();
+        }
+        if (tab7 != null && !tab7.isEmpty()) {
+            answer = answer + tab7 + System.lineSeparator();
+        }
+        if (tab8 != null && !tab8.isEmpty()) {
+            answer = answer + tab8 + System.lineSeparator();
+        }
+        return answer;
+    }
+
+    private class TableRow {
+        String kind;
+        String type;
+        String key;
+        Object value;
+        Long position;
+
+        TableRow(String kind, String type, String key, Object value) {
+            this(kind, type, key, value, null);
+        }
+
+        TableRow(String kind, String type, String key, Object value, Long position) {
+            this.kind = kind;
+            this.type = type;
+            this.key = key;
+            this.value = value;
+            this.position = position;
+        }
+
+        String valueAsString() {
+            return value != null ? value.toString() : "null";
+        }
+
+        String valueAsStringPretty() {
+            if (value == null) {
+                return "null";
+            }
+            boolean json = false;
+            String s = value.toString();
+            if (!s.isEmpty()) {
+                try {
+                    s = Jsoner.unescape(s);
+                    if (loggingColor) {
+                        s = JSonHelper.colorPrint(s, 2, true);
+                    } else {
+                        s = JSonHelper.prettyPrint(s, 2);
+                    }
+                    if (s != null && !s.isEmpty()) {
+                        json = true;
+                    }
+                } catch (Throwable e) {
+                    // ignore as not json
+                }
+                if (s == null || s.isEmpty()) {
+                    s = value.toString();
+                }
+                if (!json) {
+                    // try with xml
+                    try {
+                        s = Jsoner.unescape(s);
+                        if (loggingColor) {
+                            s = XmlHelper.colorPrint(s, 2, true);
+                        } else {
+                            s = XmlHelper.prettyPrint(s, 2);
+                        }
+                    } catch (Throwable e) {
+                        // ignore as not xml
+                    }
+                }
+                if (s == null || s.isEmpty()) {
+                    s = value.toString();
+                }
+            }
+            if (s == null) {
+                return "null";
+            }
+            return s;
+        }
+
+        String valueAsStringRed() {
+            if (value != null) {
+                if (loggingColor) {
+                    return Ansi.ansi().fgRed().a(value).reset().toString();
+                } else {
+                    return value.toString();
+                }
+            }
+            return "";
+        }
+
+        String keyAsString() {
+            if (key == null) {
+                return "";
+            }
+            return key;
+        }
+
+        String kindAsString() {
+            return kind;
+        }
+
+        String kindAsStringRed() {
+            if (loggingColor) {
+                return Ansi.ansi().fgRed().a(kind).reset().toString();
+            } else {
+                return kind;
+            }
+        }
+
+        String typeAsString() {
+            String s;
+            if (type == null) {
+                s = "null";
+            } else if (type.startsWith("java.util.concurrent")) {
+                s = type.substring(21);
+            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
+                s = type.substring(10);
+            } else if (type.startsWith("org.apache.camel.support.")) {
+                s = type.substring(25);
+            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
+                s = type.substring(34);
+            } else {
+                s = type;
+            }
+            s = "(" + s + ")";
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String typeAndLengthAsString() {
+            String s;
+            if (type == null) {
+                s = "null";
+            } else if (type.startsWith("java.util.concurrent")) {
+                s = type.substring(21);
+            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
+                s = type.substring(10);
+            } else if (type.startsWith("org.apache.camel.support.")) {
+                s = type.substring(25);
+            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
+                s = type.substring(34);
+            } else {
+                s = type;
+            }
+            s = "(" + s + ")";
+            int l = valueLength();
+            long p = position != null ? position : -1;
+            if (l != -1 & p != -1) {
+                s = s + " (pos: " + p + " length: " + l + ")";
+            } else if (l != -1) {
+                s = s + " (length: " + l + ")";
+            } else if (p != -1) {
+                s = s + " (pos: " + p + ")";
+            }
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String mepAsKey() {
+            String s = key;
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightMagenta().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String exchangeIdAsValue() {
+            String s = value.toString();
+            if (loggingColor) {
+                Ansi.Color color = exchangeIdColorChooser != null ? exchangeIdColorChooser.color(s) : Ansi.Color.DEFAULT;
+                s = Ansi.ansi().fg(color).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        int valueLength() {
+            if (value == null) {
+                return -1;
+            } else {
+                return valueAsString().length();
+            }
+        }
+
+    }
+
+}