From 73c798e8e686f5bcdd035d0800ccfb5143bd7874 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 1 Sep 2024 07:51:20 +0200 Subject: [PATCH] CAMEL-21150: camel-jbang - Make tracer in standby as default (#15383) * CAMEL-21150: camel-jbang - Make tracer in standby as default --- .../camel/impl/console/TraceDevConsole.java | 77 +++++++-- .../apache/camel/main/ProfileConfigurer.java | 8 +- .../camel/management/BacklogDebuggerTest.java | 2 +- .../apache/camel/support/MessageHelper.java | 6 +- .../pages/camel-4x-upgrade-guide-4_8.adoc | 4 + .../modules/ROOT/pages/camel-jbang.adoc | 37 ++++- .../cli/connector/LocalCliConnector.java | 35 +++- .../commands/action/CamelTraceAction.java | 150 ++++++++++++++++++ 8 files changed, 290 insertions(+), 29 deletions(-) diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java index c5de210a50d97..f34766bb255e5 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/TraceDevConsole.java @@ -42,6 +42,11 @@ public class TraceDevConsole extends AbstractDevConsole { */ public static final String ENABLED = "enabled"; + /** + * Whether to dump trace messages + */ + public static final String DUMP = "dump"; + private Queue queue; public TraceDevConsole() { @@ -67,17 +72,11 @@ protected void doInit() throws Exception { protected String doCallText(Map options) { StringBuilder sb = new StringBuilder(); String enabled = (String) options.get(ENABLED); + String dump = (String) options.get(DUMP); BacklogTracer tracer = getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class); if (tracer != null) { - if ("true".equals(enabled)) { - tracer.setEnabled(true); - sb.append("Enabled: ").append(tracer.isEnabled()).append("\n"); - } else if ("false".equals(enabled)) { - tracer.setEnabled(false); - sb.append("Enabled: ").append(tracer.isEnabled()).append("\n"); - } else { - sb.append("Enabled: ").append(tracer.isEnabled()).append("\n"); + if (dump != null) { for (BacklogTracerEventMessage t : tracer.dumpAllTracedMessages()) { addMessage(t); } @@ -85,6 +84,32 @@ protected String doCallText(Map options) { String json = t.toJSon(0); sb.append(json).append("\n"); } + } else { + if ("true".equals(enabled)) { + tracer.setEnabled(true); + } else if ("false".equals(enabled)) { + tracer.setEnabled(false); + } + sb.append("Enabled: ").append(tracer.isEnabled()).append("\n"); + sb.append("Standby: ").append(tracer.isStandby()).append("\n"); + sb.append("Trace Counter: ").append(tracer.getTraceCounter()).append("\n"); + sb.append("Backlog Size: ").append(tracer.getBacklogSize()).append("\n"); + sb.append("Queue Size: ").append(tracer.getQueueSize()).append("\n"); + sb.append("Remove On Dump: ").append(tracer.isRemoveOnDump()).append("\n"); + if (tracer.getTraceFilter() != null) { + sb.append("Trace Filter: ").append(tracer.getTraceFilter()).append("\n"); + } + if (tracer.getTracePattern() != null) { + sb.append("Trace Pattern: ").append(tracer.getTracePattern()).append("\n"); + } + sb.append("Trace Rests: ").append(tracer.isTraceRests()).append("\n"); + sb.append("Trace Templates: ").append(tracer.isTraceTemplates()).append("\n"); + sb.append("Body Max Chars: ").append(tracer.getBodyMaxChars()).append("\n"); + sb.append("Body Include Files: ").append(tracer.isBodyIncludeFiles()).append("\n"); + sb.append("Body Include Streams: ").append(tracer.isBodyIncludeStreams()).append("\n"); + sb.append("Include Exchange Properties: ").append(tracer.isIncludeExchangeProperties()).append("\n"); + sb.append("Include Exchange Variables: ").append(tracer.isIncludeExchangeVariables()).append("\n"); + sb.append("Include Exception: ").append(tracer.isIncludeException()).append("\n"); } } @@ -105,20 +130,14 @@ private void addMessage(BacklogTracerEventMessage message) { protected JsonObject doCallJson(Map options) { JsonObject root = new JsonObject(); String enabled = (String) options.get(ENABLED); + String dump = (String) options.get(DUMP); BacklogTracer tracer = getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class); if (tracer != null) { - if ("true".equals(enabled)) { - tracer.setEnabled(true); - root.put("enabled", tracer.isEnabled()); - } else if ("false".equals(enabled)) { - tracer.setEnabled(false); - root.put("enabled", tracer.isEnabled()); - } else { + if (dump != null) { for (BacklogTracerEventMessage t : tracer.dumpAllTracedMessages()) { addMessage(t); } - JsonArray arr = new JsonArray(); root.put("enabled", tracer.isEnabled()); root.put("traces", arr); @@ -126,6 +145,32 @@ protected JsonObject doCallJson(Map options) { JsonObject jo = (JsonObject) t.asJSon(); arr.add(jo); } + } else { + if ("true".equals(enabled)) { + tracer.setEnabled(true); + } else if ("false".equals(enabled)) { + tracer.setEnabled(false); + } + root.put("enabled", tracer.isEnabled()); + root.put("standby", tracer.isStandby()); + root.put("counter", tracer.getTraceCounter()); + root.put("backlogSize", tracer.getBacklogSize()); + root.put("queueSize", tracer.getQueueSize()); + root.put("removeOnDump", tracer.isRemoveOnDump()); + if (tracer.getTraceFilter() != null) { + root.put("traceFilter", tracer.getTraceFilter()); + } + if (tracer.getTracePattern() != null) { + root.put("tracePattern", tracer.getTracePattern()); + } + root.put("traceRests", tracer.isTraceRests()); + root.put("traceTemplates", tracer.isTraceTemplates()); + root.put("bodyMaxChars", tracer.getBodyMaxChars()); + root.put("bodyIncludeFiles", tracer.isBodyIncludeFiles()); + root.put("bodyIncludeStreams", tracer.isBodyIncludeStreams()); + root.put("includeExchangeProperties", tracer.isIncludeExchangeProperties()); + root.put("includeExchangeVariables", tracer.isIncludeExchangeVariables()); + root.put("includeException", tracer.isIncludeException()); } } diff --git a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java index 3973098923e3e..aa0b128ac532d 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/ProfileConfigurer.java @@ -44,10 +44,10 @@ public static void configureMain(CamelContext camelContext, String profile, Main } if ("dev".equals(profile)) { - boolean standby = config.tracerConfig().isStandby(); - if (!standby) { - // make tracing enabled (if not configured to be standby) and limit to not capture too much data - config.tracerConfig().withEnabled(true); + // make tracing at least standby so we can use it in dev-mode + boolean enabled = config.tracerConfig().isEnabled(); + if (!enabled) { + config.tracerConfig().withStandby(true); } } diff --git a/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java b/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java index 5af09dd12a478..458ff9ab4da5a 100644 --- a/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java +++ b/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java @@ -349,10 +349,10 @@ public void testBacklogDebuggerRemoveBodyAndHeader() throws Exception { assertNotNull(xml); log.info(xml); - assertTrue(xml.contains("[Body is null]"), "Should not contain our body"); assertTrue(xml.contains("bar"), "Should contain bar node"); assertFalse(xml.contains(""), "Should not contain our body"); resetMocks(); mock.expectedMessageCount(1); 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 9bc464ccabebb..c0987d91feb3e 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 @@ -1102,7 +1102,11 @@ public static JsonObject dumpAsJSonObject( } String data = extractBodyForLogging(message, null, allowCachedStreams, allowStreams, allowFiles, maxChars); if (data != null) { - jb.put("value", Jsoner.escape(data)); + if ("[Body is null]".equals(data)) { + jb.put("value", null); + } else { + jb.put("value", Jsoner.escape(data)); + } } } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_8.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_8.adoc index 24fbc24157e73..25c92369df792 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_8.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_8.adoc @@ -25,6 +25,10 @@ performing these copies has increased from 4096 bytes to 16384 bytes (the defaul The tracer (`BacklogTracer`) has changed the `backlogSize` default value from `1000` to `100`, and `maxBodySize` from `128kb` to `32kb`. This reduces the amount of data captured and stored and helps reduce the tracing overhead. +=== camel-jbang + +The `camel trace` command has changed to show tracing status (by default). To dump traced messages use `camel trace --action=dump`. + === Deprecated Components The following components that were marked as deprecated: diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index da83b483961b1..1a201c620f69a 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -1611,20 +1611,51 @@ command but only display message tracing information. This allows you to see eve The `trace` command has many options and can be used to _filter_, _grep_ or output on different detail _levels`. The _exchange id_ is logged (and grouped by colour), so you can use that to correlate the events, when traces are interleaved. -For example if an existing integration is running named chuck, you can trace it as follows: +The trace command will by default list the status of whether tracing is enabled or not in the integrations: [source,bash] ---- -camel trace chuck +camel trace + PID NAME AGE STATUS TOTAL QUEUE FILTER PATTERN + 6911 chuck 5s Standby 0 0 +---- + +Here we can see that the tracer is in standby mode, and you need to start the tracer before Camel will capture messages: + +TIP: Camel 4.8 onwards has tracing in standby mode (when using dev profile). You can enable tracing on startup by setting the configuration `camel.trace.enabled=true` in `application.properties`. + +[source,bash] +---- +camel trace --action=start ---- -You can also trace all running integrations +And if you run `camel trace` again you can see the tracer is started: [source,bash] ---- camel trace +PID NAME AGE STATUS TOTAL QUEUE FILTER PATTERN +6911 chuck 1m5s Started 16 4 ---- +And to show the traces you need to use the `dump` action as follows: + +[source,bash] +---- +camel trace chuck --action=dump +---- + +You can also dump traces from all running integrations: + +[source,bash] +---- +camel trace --action=dump +---- + +To stop tracing use `--action=stop`. + +And you can also clear the already traced messages with `--action=clear`. + ==== Running Camel integrations in background The `run` command allows to run Camel in the background with the `--background` option. 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 1620d294c4425..31b30118cd826 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 @@ -274,6 +274,8 @@ protected void actionTask() { doActionBeanTask(root); } else if ("kafka".equals(action)) { doActionKafkaTask(); + } else if ("trace".equals(action)) { + doActionTraceTask(root); } } catch (Exception e) { // ignore @@ -733,6 +735,24 @@ private void doActionKafkaTask() throws IOException { } } + private void doActionTraceTask(JsonObject root) throws IOException { + DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) + .resolveById("trace"); + if (dc != null) { + String enabled = root.getString("enabled"); + JsonObject json; + if (enabled != null) { + json = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("enabled", enabled)); + } else { + json = (JsonObject) dc.call(DevConsole.MediaType.JSON); + } + LOG.trace("Updating output file: {}", outputFile); + IOHelper.writeText(json.toJson(), outputFile); + } else { + IOHelper.writeText("{}", outputFile); + } + } + private void doActionBeanTask(JsonObject root) throws IOException { String filter = root.getStringOrDefault("filter", ""); String properties = root.getStringOrDefault("properties", "true"); @@ -977,13 +997,20 @@ protected void statusTask() { root.put("fault-tolerance", json); } } - DevConsole dc12a = dcr.resolveById("circuit-breaker"); - if (dc12a != null) { - JsonObject json = (JsonObject) dc12a.call(DevConsole.MediaType.JSON); + DevConsole dc12 = dcr.resolveById("circuit-breaker"); + if (dc12 != null) { + JsonObject json = (JsonObject) dc12.call(DevConsole.MediaType.JSON); if (json != null && !json.isEmpty()) { root.put("circuit-breaker", json); } } + DevConsole dc13 = dcr.resolveById("trace"); + if (dc13 != null) { + JsonObject json = (JsonObject) dc13.call(DevConsole.MediaType.JSON); + if (json != null && !json.isEmpty()) { + root.put("trace", json); + } + } DevConsole dc14 = dcr.resolveById("consumer"); if (dc14 != null) { JsonObject json = (JsonObject) dc14.call(DevConsole.MediaType.JSON); @@ -1069,7 +1096,7 @@ protected void traceTask() { DevConsole dc12 = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) .resolveById("trace"); if (dc12 != null) { - JsonObject json = (JsonObject) dc12.call(DevConsole.MediaType.JSON); + JsonObject json = (JsonObject) dc12.call(DevConsole.MediaType.JSON, Map.of("dump", "true")); JsonArray arr = json.getCollection("traces"); // filter based on last uid if (traceFilePos > 0) { 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 28e5b962b10af..bf569701a5bae 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,6 +23,7 @@ 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; @@ -35,8 +36,13 @@ 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.PidNameAgeCompletionCandidates; import org.apache.camel.dsl.jbang.core.common.ProcessHelper; import org.apache.camel.util.IOHelper; import org.apache.camel.util.StopWatch; @@ -68,9 +74,29 @@ public Iterator iterator() { } } + public static class ActionCompletionCandidates implements Iterable { + + public ActionCompletionCandidates() { + } + + @Override + public Iterator iterator() { + return List.of("dump", "start", "stop", "status", "clear").iterator(); + } + } + @CommandLine.Parameters(description = "Name or pid of running Camel integration. (default selects all)", arity = "0..1") String name = "*"; + @CommandLine.Option(names = { "--action" }, completionCandidates = ActionCompletionCandidates.class, + defaultValue = "status", + description = "Action to start, stop, clear, list status, or dump traces") + String action; + + @CommandLine.Option(names = { "--sort" }, completionCandidates = PidNameAgeCompletionCandidates.class, + description = "Sort by pid, name or age for showing status of tracing", defaultValue = "pid") + String sort; + @CommandLine.Option(names = { "--timestamp" }, defaultValue = "true", description = "Print timestamp.") boolean timestamp = true; @@ -167,6 +193,117 @@ public CamelTraceAction(CamelJBangMain main) { @Override public Integer doCall() throws Exception { + if ("dump".equals(action)) { + return doDumpCall(); + } else if ("status".equals(action)) { + return doStatusCall(); + } + + List pids = findPids(name); + for (long pid : pids) { + if ("clear".equals(action)) { + File f = getTraceFile("" + pid); + if (f.exists()) { + IOHelper.writeText("{}", f); + } + } else { + JsonObject root = new JsonObject(); + root.put("action", "trace"); + if ("start".equals(action)) { + root.put("enabled", "true"); + } else if ("stop".equals(action)) { + root.put("enabled", "false"); + } + File f = getActionFile(Long.toString(pid)); + IOHelper.writeText(root.toJson(), f); + } + } + + return 0; + } + + protected Integer doStatusCall() { + List rows = new ArrayList<>(); + + List pids = findPids(name); + ProcessHandle.allProcesses() + .filter(ph -> pids.contains(ph.pid())) + .forEach(ph -> { + JsonObject root = loadStatus(ph.pid()); + if (root != null) { + StatusRow row = new StatusRow(); + JsonObject context = (JsonObject) root.get("context"); + if (context == null) { + return; + } + row.name = context.getString("name"); + if ("CamelJBang".equals(row.name)) { + row.name = ProcessHelper.extractName(root, ph); + } + row.pid = Long.toString(ph.pid()); + row.uptime = extractSince(ph); + row.age = TimeUtils.printSince(row.uptime); + JsonObject jo = root.getMap("trace"); + if (jo != null) { + row.enabled = jo.getBoolean("enabled"); + row.standby = jo.getBoolean("standby"); + row.counter = jo.getLong("counter"); + row.queueSize = jo.getLong("queueSize"); + row.filter = jo.getString("traceFilter"); + row.pattern = jo.getString("tracePattern"); + } + rows.add(row); + } + }); + + // sort rows + rows.sort(this::sortStatusRow); + + if (!rows.isEmpty()) { + printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( + new Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> r.pid), + new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT) + .with(r -> r.name), + new Column().header("AGE").headerAlign(HorizontalAlign.CENTER).with(r -> r.age), + new Column().header("STATUS").with(this::getTraceStatus), + new Column().header("TOTAL").with(r -> "" + r.counter), + new Column().header("QUEUE").with(r -> "" + r.queueSize), + new Column().header("FILTER").with(r -> r.filter), + new Column().header("PATTERN").with(r -> r.pattern)))); + } + + return 0; + } + + private String getTraceStatus(StatusRow r) { + if (r.enabled) { + return "Started"; + } else if (r.standby) { + return "Standby"; + } + return "Stopped"; + } + + protected int sortStatusRow(StatusRow o1, StatusRow o2) { + String s = sort; + int negate = 1; + if (s.startsWith("-")) { + s = s.substring(1); + negate = -1; + } + switch (s) { + case "pid": + return Long.compare(Long.parseLong(o1.pid), Long.parseLong(o2.pid)) * negate; + case "name": + return o1.name.compareToIgnoreCase(o2.name) * negate; + case "age": + return Long.compare(o1.uptime, o2.uptime) * negate; + default: + return 0; + } + } + + protected Integer doDumpCall() throws Exception { // setup table helper tableHelper = new MessageTableHelper(); tableHelper.setPretty(pretty); @@ -825,4 +962,17 @@ private static class Row { } + private static class StatusRow { + String pid; + String name; + String age; + long uptime; + boolean enabled; + boolean standby; + long counter; + long queueSize; + String filter; + String pattern; + } + }