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

davsclaus pushed a commit to branch fix/CAMEL-23688-get-listings
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 63ea7323cfe6ae3eff35f9934a750e26ae730aef
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 22:51:48 2026 +0200

    CAMEL-23688: camel-jbang - Tests for get route, processor, metric and health
    
    Tests for camel get route (65% covered), get processor (59%), get metric
    (58%) and get health (60%), with status files as the route, micrometer
    and health dev consoles write them. They found these bugs, fixed here:
    
    * camel get metric skipped an integration that has a distribution
      summary, with a warning: it read the count of the summary from a field
      the dev console does not have.
    * camel get metric --filter=timer*, the example of its help, showed
      nothing; the filter takes a pattern now. --custom left out the metrics
      of Camel in the classic naming (CamelExchangesTotal) only, not in the
      default naming of Micrometer (camel.exchanges.total).
    * camel get route --error --limit limited the routes before it picked
      the ones with an error, so it could show none, and --note did not show
      the notes of the routes with an error.
    * camel get processor --filter showed the matching processors without
      their route, and so without the name and pid of their integration;
      --filter-mean left out the faster routes but not the faster processors.
    * camel get health --level with a value other than full or default was
      taken as full; now a usage error.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 +
 .../modules/ROOT/pages/camel-jbang-managing.adoc   |   3 +
 .../commands/process/CamelProcessorStatus.java     |  58 +--
 .../core/commands/process/CamelRouteStatus.java    |  13 +-
 .../jbang/core/commands/process/ListHealth.java    |  10 +-
 .../jbang/core/commands/process/ListMetric.java    |  56 ++-
 .../commands/process/CamelProcessorStatusTest.java | 278 +++++++++-----
 .../commands/process/CamelRouteStatusTest.java     | 410 +++++++++++++--------
 .../core/commands/process/ListHealthTest.java      | 272 +++++++++-----
 .../core/commands/process/ListMetricTest.java      | 250 +++++++++----
 10 files changed, 907 insertions(+), 456 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index b332441dfd88..a64f86317060 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -409,6 +409,19 @@ The JSON of the listings (`--json`) changes, so that a 
script can use the values
   the state is `status` (it was `state` in `camel get circuit-breaker`). 
`camel get consumer` has `total` instead of
   `poll`. Every row of `camel get processor` has the `name` of its integration.
 
+=== camel-jbang - camel get route, processor, metric and health
+
+* `camel get route --error --limit` shows that number of routes with an error 
(it limited the routes first, and then
+  showed the ones with an error among them), and `--note` shows the notes of 
the routes with an error too.
+* `camel get processor --filter` shows the matching processors with their 
route, and so with the name and pid of their
+  integration (it showed the processors alone). `--filter-mean` leaves out the 
faster processors too, as its
+  description says (it left out the faster routes only).
+* `camel get metric` shows the distribution summaries: an integration that has 
one was skipped with a warning.
+  `--filter` takes a pattern such as `timer*`, as in its help (it showed 
nothing), and `--custom` leaves out the metrics
+  of Camel in the default naming of Micrometer, such as 
`camel.exchanges.total`, not only in the classic naming
+  (`CamelExchangesTotal`).
+* `camel get health --level` other than `full` or `default` exits with `2` (it 
showed every check, as `full` does).
+
 === camel-jbang - camel stop and camel restart wait for the integration
 
 * `camel stop` waits until the integration has stopped, up to `--timeout` (60 
seconds by default), and exits with `124`
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
index 3088a9644d4c..d46af59be16d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc
@@ -824,6 +824,9 @@ $ camel get metric
  11562  MyCoolCamel  gauge  system.load.average.1m                        
3.58935546875
 ----
 
+Use `--filter` for the metrics of a type, or with a name or tags that contain 
the text or match a pattern such as
+`timer*`, `--custom` to leave out the metrics of Camel itself, and `--all` to 
include the metrics that are still `0`.
+
 == Circuit breaker status
 
 View the state of xref:components:eips:circuitBreaker-eip.adoc[Circuit 
Breakers]:
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
index b85032610f45..1b8a23a573f6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatus.java
@@ -22,6 +22,7 @@ import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import com.github.freva.asciitable.AsciiTable;
@@ -211,28 +212,13 @@ public class CamelProcessorStatus extends 
ProcessWatchCommand {
 
         // filter rows
         if (running || filter != null || group != null) {
-            rows.removeIf(r -> {
-                boolean keep = true;
-                if (filter != null) {
-                    keep = PatternHelper.matchPatterns(r.processorId, filter);
-                }
-                if (!keep && filter != null) {
-                    for (String f : filter) {
-                        if (!keep) {
-                            String w = f.endsWith("*") ? f : f + "*"; // use 
wildcard in matching processor
-                            keep = PatternHelper.matchPattern(r.processor, w);
-                        }
-                    }
-                }
-                // group take precedence
-                if (keep && group != null) {
-                    keep = PatternHelper.matchPatterns(r.group, group);
-                }
-                if (keep && running) {
-                    keep = "Started".equals(r.state);
-                }
-                return !keep;
-            });
+            // a route stays with its processors that are kept, so they are 
shown with their integration and route
+            Set<String> routesOfProcessors = rows.stream()
+                    .filter(r -> r.processorId != null && accept(r))
+                    .map(r -> r.pid + "/" + r.routeId)
+                    .collect(Collectors.toSet());
+            rows.removeIf(r -> !accept(r)
+                    && (r.processorId != null || 
!routesOfProcessors.contains(r.pid + "/" + r.routeId)));
         }
 
         // sort rows
@@ -245,6 +231,29 @@ public class CamelProcessorStatus extends 
ProcessWatchCommand {
         return 0;
     }
 
+    private boolean accept(Row r) {
+        boolean keep = true;
+        if (filter != null) {
+            keep = PatternHelper.matchPatterns(r.processorId, filter);
+        }
+        if (!keep && filter != null) {
+            for (String f : filter) {
+                if (!keep) {
+                    String w = f.endsWith("*") ? f : f + "*"; // use wildcard 
in matching processor
+                    keep = PatternHelper.matchPattern(r.processor, w);
+                }
+            }
+        }
+        // group take precedence
+        if (keep && group != null) {
+            keep = PatternHelper.matchPatterns(r.group, group);
+        }
+        if (keep && running) {
+            keep = "Started".equals(r.state);
+        }
+        return keep;
+    }
+
     private void addProcessors(Row route, List<Row> rows, List<JsonObject> 
processors) {
         for (JsonObject o : processors) {
             Row row = new Row();
@@ -252,7 +261,6 @@ public class CamelProcessorStatus extends 
ProcessWatchCommand {
             row.name = route.name;
             row.routeId = route.routeId;
             row.group = route.group;
-            rows.add(row);
             row.processorId = o.getString("id");
             row.nodePrefixId = o.getString("nodePrefixId");
             row.processor = o.getString("processor");
@@ -305,6 +313,10 @@ public class CamelProcessorStatus extends 
ProcessWatchCommand {
                     }
                 }
             }
+            // with --filter-mean only the slower processors, as for the routes
+            if (mean <= 0 || (row.mean != null && Long.parseLong(row.mean) >= 
mean)) {
+                rows.add(row);
+            }
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
index d7477ccc66bc..d8c7f3ce2027 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatus.java
@@ -244,17 +244,20 @@ public class CamelRouteStatus extends ProcessWatchCommand 
{
         // sort rows
         rows.sort(order());
 
+        if (error) {
+            // the limit is a number of routes with an error
+            rows.removeIf(r -> r.lastErrorPhase == null);
+        }
         if (limit > 0 && rows.size() > limit) {
             rows.subList(limit, rows.size()).clear();
         }
 
         if (!rows.isEmpty() || jsonOutput) {
             if (error) {
-                List<Row> failed = rows.stream().filter(r -> r.lastErrorPhase 
!= null).toList();
                 if (jsonOutput) {
-                    
printer().println(Jsoner.serialize(failed.stream().map(this::toErrorJson).collect(Collectors.toList())));
+                    
printer().println(Jsoner.serialize(rows.stream().map(this::toErrorJson).collect(Collectors.toList())));
                 } else {
-                    for (Row r : failed) {
+                    for (Row r : rows) {
                         printErrorTable(r, remoteVisible.get());
                     }
                 }
@@ -382,10 +385,10 @@ public class CamelRouteStatus extends ProcessWatchCommand 
{
                 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("ID").visible(!description).dataAlign(HorizontalAlign.LEFT)
+                new Column().header("ID").visible(!description && 
!note).dataAlign(HorizontalAlign.LEFT)
                         .maxWidth(20, OverflowBehaviour.ELLIPSIS_RIGHT)
                         .with(this::getId),
-                new 
Column().header("ID").visible(description).dataAlign(HorizontalAlign.LEFT)
+                new Column().header("ID").visible(description || 
note).dataAlign(HorizontalAlign.LEFT)
                         .maxWidth(errIdDescW, OverflowBehaviour.NEWLINE)
                         .with(this::getIdAndNoteDescription),
                 new 
Column().header("FROM").visible(!wideUri).dataAlign(HorizontalAlign.LEFT)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealth.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealth.java
index 0ff5cd2938f4..fb88d8614e47 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealth.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealth.java
@@ -20,6 +20,7 @@ import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -30,6 +31,7 @@ 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.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.RowSort;
@@ -93,6 +95,12 @@ public class ListHealth extends ProcessWatchCommand {
     public Integer doProcessWatchCall() throws Exception {
         final List<Row> rows = new ArrayList<>();
 
+        // the options are wrong for every integration, so before reading them
+        Comparator<Row> order = ORDER.comparator(sort);
+        if (level != null && !"default".equals(level) && 
!"full".equals(level)) {
+            throw CliException.usage("--level must be full or default, was: " 
+ level);
+        }
+
         // include stack-traces
         if (trace && depth <= 1) {
             depth = Integer.MAX_VALUE;
@@ -193,7 +201,7 @@ public class ListHealth extends ProcessWatchCommand {
         });
 
         // sort rows
-        rows.sort(ORDER.comparator(sort));
+        rows.sort(order);
 
         if (!rows.isEmpty() || jsonOutput) {
             if (jsonOutput) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetric.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetric.java
index 91deab646e3b..0aa05a1dedbf 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetric.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetric.java
@@ -33,6 +33,7 @@ import 
org.apache.camel.dsl.jbang.core.common.PidNameAgeCompletionCandidates;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.RowSort;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
+import org.apache.camel.support.PatternHelper;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -117,14 +118,7 @@ public class ListMetric extends ProcessWatchCommand {
                         row.tags = extractTags(jo);
                         row.count = jo.getDouble("count");
 
-                        if (custom && row.metricName.startsWith("Camel")) {
-                            continue; // skip internal camel metrics
-                        }
-                        if (!all && getNumber(row.count).isEmpty()) {
-                            continue;
-                        }
-                        if (filter == null || row.type.equals(filter) || 
row.metricName.contains(filter)
-                                || row.tags.contains(filter)) {
+                        if (accept(row)) {
                             rows.add(row);
                         }
                     }
@@ -144,14 +138,7 @@ public class ListMetric extends ProcessWatchCommand {
                         row.max = jo.getDouble("max");
                         row.total = jo.getDouble("total");
 
-                        if (custom && row.metricName.startsWith("Camel")) {
-                            continue; // skip internal camel metrics
-                        }
-                        if (!all && getNumber(row.count).isEmpty()) {
-                            continue;
-                        }
-                        if (filter == null || row.type.equals(filter) || 
row.metricName.contains(filter)
-                                || row.tags.contains(filter)) {
+                        if (accept(row)) {
                             rows.add(row);
                         }
                     }
@@ -171,14 +158,7 @@ public class ListMetric extends ProcessWatchCommand {
                         row.tags = extractTags(jo);
                         row.count = jo.getDouble("value");
 
-                        if (custom && row.metricName.startsWith("Camel")) {
-                            continue; // skip internal camel metrics
-                        }
-                        if (!all && getNumber(row.count).isEmpty()) {
-                            continue;
-                        }
-                        if (filter == null || row.type.equals(filter) || 
row.metricName.contains(filter)
-                                || row.tags.contains(filter)) {
+                        if (accept(row)) {
                             rows.add(row);
                         }
                     }
@@ -193,19 +173,12 @@ public class ListMetric extends ProcessWatchCommand {
                         row.metricDescription = jo.getString("description");
                         row.metricId = extractId(jo);
                         row.tags = extractTags(jo);
-                        row.count = jo.getDouble("value");
+                        row.count = jo.getDouble("count");
                         row.mean = jo.getDouble("mean");
                         row.max = jo.getDouble("max");
                         row.total = jo.getDouble("totalAmount");
 
-                        if (custom && row.metricName.startsWith("Camel")) {
-                            continue; // skip internal camel metrics
-                        }
-                        if (!all && getNumber(row.count).isEmpty()) {
-                            continue;
-                        }
-                        if (filter == null || row.type.equals(filter) || 
row.metricName.contains(filter)
-                                || row.tags.contains(filter)) {
+                        if (accept(row)) {
                             rows.add(row);
                         }
                     }
@@ -271,6 +244,23 @@ public class ListMetric extends ProcessWatchCommand {
         return 0;
     }
 
+    // whether the metric is listed
+    private boolean accept(Row row) {
+        if (custom && (row.metricName.startsWith("Camel") || 
row.metricName.startsWith("camel."))) {
+            return false; // skip internal camel metrics (in the classic and 
the micrometer naming)
+        }
+        if (!all && getNumber(row.count).isEmpty()) {
+            return false;
+        }
+        return filter == null || PatternHelper.matchPattern(row.type, filter) 
|| matches(row.metricName)
+                || matches(row.tags);
+    }
+
+    // the filter is a part of the text, or a pattern such as timer*
+    private boolean matches(String text) {
+        return text.contains(filter) || PatternHelper.matchPattern(text, 
filter);
+    }
+
     private String getNumber(double d) {
         String s = Double.toString(d);
         if (s.equals("0.0") || s.equals("0,0")) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
index 92a0a56acaf3..fd869702d6b0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
@@ -16,153 +16,259 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.process;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get processor: the routes of the integrations with their processors 
as a tree, their statistics, and filtered.
+ */
 @ExtendWith(MockitoExtension.class)
 class CamelProcessorStatusTest extends ProcessCommandTestSupport {
 
     @Test
-    void testDisplaysProcessorRow() throws Exception {
-        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "25", "0", "0"));
-
-        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
-
+    void testEmptyOutputWhenNoProcesses() throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
             ProcessHandle current = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(current);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
-
-            int exit = command.doCall();
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("myApp"), "Should show integration 
name");
-            assertTrue(output.contains("myRoute"), "Should show route ID");
-            assertTrue(output.contains("25"), "Should show mean processing 
time");
+            assertThat(command().doCall()).isZero();
         }
+
+        assertThat(printer.getStdout()).isEmpty();
+        assertThat(printer.getStderr()).isEqualTo("No running Camel 
integrations");
     }
 
     @Test
-    void testFilterMeanExcludesSlowProcessors() throws Exception {
-        // processor with mean=5 should be excluded when --filter-mean=10
-        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "5", "0", "0"));
+    void processorsOfTheRoutesAsATree() throws Exception {
+        writeStatusFile(TEST_PID, integration(route("orders", "timer:tick", 
"Started", 25,
+                processor("orders-log1", "log", 1, 5),
+                processor("orders-to1", "to[mock:out]", 2, 20))));
+
+        assertThat(call(command())).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("PID", "NAME", "ID", "PROCESSOR", 
"STATUS", "TOTAL", "MEAN");
+        assertThat(lines[1]).contains("12345", "myApp", "orders", 
"timer:tick", "Started", "25");
+        // the processors under their route, the id without the node prefix 
and indented by level
+        assertThat(lines[2]).doesNotContain("12345", "myApp").contains(" log1 
", "   log ", "5");
+        assertThat(lines[3]).contains(" to1 ", "     to[mock:out]", "20");
+    }
 
-        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.mean = 10;
+    @Test
+    void jsonHasTheRoutesAndTheirProcessors() throws Exception {
+        writeStatusFile(TEST_PID, integration(route("orders", "timer:tick", 
"Started", 25,
+                processor("orders-log1", "log", 1, 5))));
+
+        assertThat(call(command("--json"))).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        assertThat(rows).hasSize(2);
+        JsonObject route = rows.getMap(0);
+        assertThat(route.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(route.getString("id")).isEqualTo("orders");
+        assertThat(route.getString("processor")).isEqualTo("timer:tick");
+        assertThat(route.getLong("mean")).isEqualTo(25);
+        JsonObject log = rows.getMap(1);
+        assertThat(log.getString("routeId")).isEqualTo("orders");
+        assertThat(log.getString("id")).isEqualTo("log1");
+        assertThat(log.getString("processor")).isEqualTo("log");
+        assertThat(log.getString("status")).isEqualTo("Started");
+        assertThat(log.getLong("total")).isEqualTo(10);
+        assertThat(log.getLong("mean")).isEqualTo(5);
+    }
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle current = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(current);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+    @Test
+    void disabledProcessor() throws Exception {
+        JsonObject log = processor("orders-log1", "log", 1, 5);
+        log.put("disabled", true);
+        writeStatusFile(TEST_PID, integration(route("orders", "timer:tick", 
"Started", 25, log)));
 
-            int exit = command.doCall();
+        assertThat(call(command())).isZero();
 
-            assertEquals(0, exit);
-            assertEquals("", printer.getOutput().trim(), "Processor below mean 
threshold should be filtered out");
-        }
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[2]).contains("log1", "Disabled");
     }
 
     @Test
-    void testFilterMeanIncludesSlowEnoughProcessors() throws Exception {
-        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "50", "0", "0"));
+    void limitIsANumberOfRoutes() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                route("orders", "timer:a", "Started", 25, 
processor("orders-log1", "log", 1, 5)),
+                route("payments", "timer:b", "Started", 25, 
processor("payments-log2", "log", 1, 5))));
 
-        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.mean = 10;
+        assertThat(call(command("--limit=1"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle current = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(current);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        // with the processors of the route
+        assertThat(printer.getOutput()).contains("orders", 
"log1").doesNotContain("payments", "log2");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void filterShowsTheMatchingProcessorsWithTheirRoute() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                route("orders", "timer:a", "Started", 25,
+                        processor("orders-log1", "log", 1, 5),
+                        processor("orders-to1", "to[mock:out]", 1, 20)),
+                route("payments", "timer:b", "Started", 25, 
processor("payments-to2", "to[mock:pay]", 1, 5))));
+
+        assertThat(call(command("--filter=log*"))).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines).hasSize(3);
+        assertThat(lines[1]).contains("12345", "myApp", "orders");
+        assertThat(lines[2]).contains("log1");
+    }
 
-            assertEquals(0, exit);
-            assertTrue(printer.getOutput().contains("50"), "Processor above 
mean threshold should be visible");
-        }
+    @Test
+    void filterMeanShowsTheSlowerProcessors() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                route("orders", "timer:a", "Started", 25,
+                        processor("orders-log1", "log", 1, 2),
+                        processor("orders-to1", "to[mock:out]", 1, 20)),
+                route("payments", "timer:b", "Started", 5, 
processor("payments-log2", "log", 1, 5))));
+
+        assertThat(call(command("--filter-mean=10"))).isZero();
+
+        assertThat(printer.getOutput()).contains("orders", 
"to1").doesNotContain("log1", "payments", "log2");
     }
 
     @Test
-    void testEmptyOutputWhenNoProcesses() throws Exception {
-        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+    void groupShowsTheRoutesOfTheGroupWithTheirProcessors() throws Exception {
+        JsonObject orders = route("orders", "timer:a", "Started", 25, 
processor("orders-log1", "log", 1, 5));
+        orders.put("group", "sales");
+        JsonObject audit = route("audit", "timer:b", "Started", 25, 
processor("audit-log2", "log", 1, 5));
+        audit.put("group", "ops");
+        writeStatusFile(TEST_PID, integration(orders, audit));
+
+        assertThat(call(command("--group=sales", "--show-group"))).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("GROUP");
+        assertThat(printer.getOutput()).contains("sales", "orders", 
"log1").doesNotContain("audit", "log2");
+    }
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle current = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(current);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+    @Test
+    void runningHidesTheRoutesThatAreNotStarted() throws Exception {
+        JsonObject stoppedLog = processor("stopped-log2", "log", 1, 5);
+        stoppedLog.put("state", "Stopped");
+        writeStatusFile(TEST_PID, integration(
+                route("started", "timer:a", "Started", 25, 
processor("started-log1", "log", 1, 5)),
+                route("stopped", "timer:b", "Stopped", 25, stoppedLog)));
 
-            int exit = command.doCall();
+        assertThat(call(command("--running"))).isZero();
 
-            assertEquals(0, exit);
-            assertEquals("", printer.getStdout());
-            assertEquals("No running Camel integrations", printer.getStderr());
-        }
+        assertThat(printer.getOutput()).contains("started", 
"log1").doesNotContain("stopped", "log2");
+    }
+
+    @Test
+    void sourceShowsTheSourceLocationAndTheCodeOfTheProcessors() throws 
Exception {
+        JsonObject log = processor("orders-log1", "log", 1, 5);
+        log.put("source", "file:src/main/resources/camel/app.yaml:14");
+        log.put("code", new JsonArray(
+                List.of(
+                        new JsonObject(Map.of("line", 13, "code", "- from:")),
+                        new JsonObject(Map.of("line", 14, "code", "    - log: 
\\\"hi\\\"", "match", true)))));
+        writeStatusFile(TEST_PID, integration(route("orders", "timer:tick", 
"Started", 25, log)));
+
+        assertThat(call(command("--source"))).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[2]).contains("app.yaml:14", "- log: 
\"hi\"").doesNotContain("log1");
     }
 
     @Test
-    void testJsonOutput() throws Exception {
-        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "30", "2", "0"));
+    void descriptionInTheIdColumn() throws Exception {
+        JsonObject log = processor("orders-log1", "log", 1, 5);
+        log.put("description", "Logs the order");
+        writeStatusFile(TEST_PID, integration(route("orders", "timer:tick", 
"Started", 25, log)));
+
+        assertThat(call(command("--description"))).isZero();
+
+        assertThat(printer.getOutput()).contains("log1", "Logs the order");
+    }
+
+    @Test
+    void testFilterMeanExcludesSlowProcessors() throws Exception {
+        // the route with mean=5 is excluded when --filter-mean=10
+        writeStatusFile(TEST_PID, integration(route("myRoute", "log:output", 
"Started", 5)));
 
+        assertThat(call(command("--filter-mean=10"))).isZero();
+
+        assertThat(printer.getOutput()).isEmpty();
+    }
+
+    private CamelProcessorStatus command(String... args) {
         CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.jsonOutput = true;
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
 
+    // runs the command with the integration of the status file of the test
+    private static int call(CamelProcessorStatus command) throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
             ProcessHandle ph = mockProcessHandle(TEST_PID);
             ProcessHandle current = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(current);
             mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
-
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.startsWith("["), "JSON output should be an 
array");
-            assertTrue(output.contains("myRoute"), "JSON should contain route 
ID");
+            return command.doCall();
         }
     }
 
-    private static JsonObject buildStatusWithProcessor(
-            String routeId, String processorId, String from,
-            String state, String mean, String failed, String inflight) {
-        JsonObject stats = new JsonObject();
-        stats.put("exchangesTotal", "10");
-        stats.put("exchangesFailed", failed);
-        stats.put("exchangesInflight", inflight);
-        stats.put("meanProcessingTime", mean);
-        stats.put("maxProcessingTime", mean);
-        stats.put("minProcessingTime", mean);
+    private static JsonObject integration(JsonObject... routes) {
+        JsonObject context = new JsonObject();
+        context.put("name", "myApp");
+        context.put("phase", 5);
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routes", new JsonArray(Arrays.asList(routes)));
+        return root;
+    }
 
+    // as the route dev console has them, with the processors
+    private static JsonObject route(String routeId, String from, String state, 
long mean, JsonObject... processors) {
         JsonObject route = new JsonObject();
         route.put("routeId", routeId);
-        route.put("processorId", processorId);
+        route.put("nodePrefixId", routeId + "-");
         route.put("from", from);
         route.put("state", state);
-        route.put("statistics", stats);
-
-        JsonArray routes = new JsonArray();
-        routes.add(route);
+        route.put("statistics", stats(mean));
+        route.put("processors", new JsonArray(Arrays.asList(processors)));
+        return route;
+    }
 
-        JsonObject context = new JsonObject();
-        context.put("name", "myApp");
-        context.put("phase", 5);
+    private static JsonObject processor(String id, String name, int level, 
long mean) {
+        JsonObject processor = new JsonObject();
+        processor.put("id", id);
+        processor.put("nodePrefixId", id.substring(0, id.indexOf('-') + 1));
+        processor.put("processor", name);
+        processor.put("level", level);
+        processor.put("state", "Started");
+        processor.put("statistics", stats(mean));
+        return processor;
+    }
 
-        JsonObject root = new JsonObject();
-        root.put("context", context);
-        root.put("routes", routes);
-        return root;
+    private static JsonObject stats(long mean) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", 10);
+        stats.put("exchangesFailed", 0);
+        stats.put("exchangesInflight", 0);
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+        return stats;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
index 8ad3b6dabd4d..109011a05f9a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
@@ -16,241 +16,347 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.process;
 
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get route: the routes of the integrations, their state and 
statistics, filtered, and the last error of the
+ * routes that have one.
+ */
 @ExtendWith(MockitoExtension.class)
 class CamelRouteStatusTest extends ProcessCommandTestSupport {
 
+    private static final long OTHER_PID = 23456L;
+
     @Test
     void testEmptyOutputWhenNoProcesses() throws Exception {
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
+        assertThat(call(command())).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+        assertThat(printer.getStdout()).isEmpty();
+        assertThat(printer.getStderr()).isEqualTo("No running Camel 
integrations");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void routesWithTheirStateAndStatistics() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("route1", "timer:tick", "Started", stats(12, 1, 2, 25)),
+                route("route2", "timer:tock", "Suspended", stats(0, 0, 0, 
-1))));
+
+        assertThat(call(command())).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("PID", "NAME", "ID", "FROM", "STATUS", 
"TOTAL", "FAIL", "INFLIGHT", "MEAN")
+                .doesNotContain("GROUP", "REMOTE");
+        assertThat(lines[1]).contains("12345", "myApp", "route1", 
"timer:tick", "Started", "12", "25");
+        assertThat(lines[2]).contains("route2", "timer:tock", "Suspended");
+    }
 
-            assertEquals(0, exit);
-            assertEquals("", printer.getStdout());
-            assertEquals("No running Camel integrations", printer.getStderr());
-        }
+    @Test
+    void jsonHasTheNumbersOfTheRoutes() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("route1", "timer:tick", "Started", stats(12, 1, 2, 25)),
+                route("route2", "timer:tock", "Started", stats(0, 0, 0, -1))));
+
+        assertThat(call(command("--json"))).isZero();
+
+        JsonArray routes = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        assertThat(routes).hasSize(2);
+        JsonObject route1 = routes.getMap(0);
+        assertThat(route1.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(route1.getString("routeId")).isEqualTo("route1");
+        assertThat(route1.getString("status")).isEqualTo("Started");
+        assertThat(route1.getLong("total")).isEqualTo(12);
+        assertThat(route1.getLong("failed")).isEqualTo(1);
+        assertThat(route1.getLong("inflight")).isEqualTo(2);
+        assertThat(route1.getLong("mean")).isEqualTo(25);
+        // no exchange yet: no mean
+        JsonObject route2 = routes.getMap(1);
+        assertThat(route2.get("mean")).isNull();
     }
 
     @Test
-    void testDisplaysSingleRoute() throws Exception {
-        JsonObject status = buildRouteStatus("route1", "Started");
-        writeStatusFile(TEST_PID, status);
+    void jsonWithoutRoutesIsAnEmptyArray() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp"));
 
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command("--json"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(printer.getOutput()).isEqualTo("[]");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void errorShowsTheRoutesWithAnErrorAndTheirStackTrace() throws Exception {
+        JsonObject failed = route("failed", "kafka:in", "Stopped", stats(0, 0, 
0, -1));
+        failed.put("lastError", lastError("start", "Cannot connect"));
+        writeStatusFile(TEST_PID, integration("myApp", route("ok", 
"timer:tick", "Started", stats(1, 0, 0, 5)), failed));
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("route1"), "Should show route ID");
-            assertTrue(output.contains("timer:tick"), "Should show from URI");
-            assertTrue(output.contains("Started"), "Should show route state");
-        }
+        assertThat(call(command("--error"))).isZero();
+
+        assertThat(printer.getOutput())
+                .contains("PHASE", "MESSAGE", "failed", "kafka:in", "Error", 
"start", "Cannot connect", "STACK-TRACE",
+                        "java.lang.IllegalStateException: Cannot connect", "at 
org.example.Kafka.start(Kafka.java:42)")
+                .doesNotContain("timer:tick");
     }
 
     @Test
-    void testDisplaysMultipleRoutes() throws Exception {
-        JsonObject routeStats = buildRouteStats();
-        JsonObject route1 = buildRoute("route1", "timer:tick1", routeStats);
-        JsonObject route2 = buildRoute("route2", "timer:tick2", routeStats);
+    void errorAsJson() throws Exception {
+        JsonObject failed = route("failed", "kafka:in", "Stopped", stats(0, 0, 
0, -1));
+        failed.put("lastError", lastError("start", "Cannot connect"));
+        writeStatusFile(TEST_PID, integration("myApp", route("ok", 
"timer:tick", "Started", stats(1, 0, 0, 5)), failed));
+
+        assertThat(call(command("--error", "--json"))).isZero();
+
+        JsonArray routes = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        assertThat(routes).hasSize(1);
+        JsonObject route = routes.getMap(0);
+        assertThat(route.getString("routeId")).isEqualTo("failed");
+        assertThat(route.getString("status")).isEqualTo("Error");
+        JsonObject error = route.getMap("lastError");
+        assertThat(error.getString("phase")).isEqualTo("start");
+        assertThat(error.getString("message")).isEqualTo("Cannot connect");
+        assertThat(error.getString("ago")).isNotBlank();
+        List<String> stackTrace = error.getCollection("stackTrace");
+        assertThat(stackTrace).hasSize(2);
+    }
 
-        JsonArray routes = new JsonArray();
-        routes.add(route1);
-        routes.add(route2);
+    @Test
+    void limitCountsTheRoutesWithAnError() throws Exception {
+        JsonObject failed = route("failed", "kafka:in", "Stopped", stats(0, 0, 
0, -1));
+        failed.put("lastError", lastError("start", "Cannot connect"));
+        writeStatusFile(TEST_PID, integration("myApp", route("ok", 
"timer:tick", "Started", stats(1, 0, 0, 5)), failed));
 
-        JsonObject context = new JsonObject();
-        context.put("name", "multiRouteApp");
-        context.put("phase", 5);
+        assertThat(call(command("--error", "--limit=1"))).isZero();
 
-        JsonObject root = new JsonObject();
-        root.put("context", context);
-        root.put("routes", routes);
-        writeStatusFile(TEST_PID, root);
+        assertThat(printer.getOutput()).contains("failed", "Cannot connect");
+    }
 
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+    @Test
+    void noteOfARouteWithAnError() throws Exception {
+        JsonObject failed = route("failed", "kafka:in", "Stopped", stats(0, 0, 
0, -1));
+        failed.put("note", "Needs the broker");
+        failed.put("lastError", lastError("start", "Cannot connect"));
+        writeStatusFile(TEST_PID, integration("myApp", failed));
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(call(command("--error", "--note"))).isZero();
 
-            int exit = command.doCall();
+        assertThat(printer.getOutput()).contains("Needs the broker");
+    }
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("route1"));
-            assertTrue(output.contains("route2"));
-        }
+    @Test
+    void filterByRouteIdOrEndpoint() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("orders", "kafka:orders?groupId=a", "Started", stats(0, 
0, 0, -1)),
+                route("payments", "jms:payments", "Started", stats(0, 0, 0, 
-1)),
+                route("audit", "timer:audit", "Started", stats(0, 0, 0, -1))));
+
+        // the route id, or the start of the endpoint without its query 
parameters
+        assertThat(call(command("--filter=orders", 
"--filter=jms:pay"))).isZero();
+
+        assertThat(printer.getOutput()).contains("orders", 
"payments").doesNotContain("audit");
     }
 
     @Test
-    void testSuspendedRouteShownCorrectly() throws Exception {
-        JsonObject status = buildRouteStatus("suspendedRoute", "Suspended");
-        writeStatusFile(TEST_PID, status);
+    void filterMeanShowsTheSlowerRoutes() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("slow", "timer:slow", "Started", stats(5, 0, 0, 50)),
+                route("fast", "timer:fast", "Started", stats(5, 0, 0, 5)),
+                route("idle", "timer:idle", "Started", stats(0, 0, 0, -1))));
 
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command("--filter-mean=10"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        
assertThat(printer.getOutput()).contains("slow").doesNotContain("fast", "idle");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void groupFilterAndColumn() throws Exception {
+        JsonObject orders = route("orders", "timer:orders", "Started", 
stats(0, 0, 0, -1));
+        orders.put("group", "sales");
+        JsonObject audit = route("audit", "timer:audit", "Started", stats(0, 
0, 0, -1));
+        audit.put("group", "ops");
+        writeStatusFile(TEST_PID, integration("myApp", orders, audit));
+
+        assertThat(call(command("--group=sales", "--show-group"))).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("GROUP");
+        assertThat(lines).hasSize(2);
+        assertThat(lines[1]).contains("sales", "orders");
+    }
 
-            assertEquals(0, exit);
-            assertTrue(printer.getOutput().contains("Suspended"));
-        }
+    @Test
+    void runningHidesTheRoutesThatAreNotStarted() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("started", "timer:a", "Started", stats(0, 0, 0, -1)),
+                route("stopped", "timer:b", "Stopped", stats(0, 0, 0, -1)),
+                route("suspended", "timer:c", "Suspended", stats(0, 0, 0, 
-1))));
+
+        assertThat(call(command("--running"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("started").doesNotContain("stopped", 
"suspended");
     }
 
     @Test
-    void testJsonOutput() throws Exception {
-        JsonObject status = buildRouteStatus("route1", "Started");
-        writeStatusFile(TEST_PID, status);
+    void shortUriLeavesOutTheQueryParameters() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp", route("route1", 
"timer:t?period=5", "Started", stats(0, 0, 0, -1))));
 
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.jsonOutput = true;
+        assertThat(call(command("--short-uri"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        
assertThat(printer.getOutput()).contains("timer:t").doesNotContain("period");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void wideUriShowsTheWholeEndpoint() throws Exception {
+        writeStatusFile(TEST_PID, integration("myApp",
+                route("route1", 
"timer:tick?period=5000&delay=1000&repeatCount=10", "Started", stats(0, 0, 0, 
-1))));
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.startsWith("["), "JSON output should be array");
-            assertTrue(output.contains("route1"));
-        }
+        assertThat(call(command("--wide-uri"))).isZero();
+
+        // across lines when wider than the column
+        assertThat(printer.getOutput().replaceAll("\\s", 
"")).contains("repeatCount=10");
     }
 
     @Test
-    void testFilterByRouteId() throws Exception {
-        JsonObject routeStats = buildRouteStats();
-        JsonObject route1 = buildRoute("matchingRoute", "timer:tick1", 
routeStats);
-        JsonObject route2 = buildRoute("otherRoute", "timer:tick2", 
routeStats);
+    void sourceShowsTheSourceLocationInsteadOfTheId() throws Exception {
+        JsonObject route = route("route1", "timer:tick", "Started", stats(0, 
0, 0, -1));
+        route.put("source", "file:src/main/resources/camel/orders.yaml:12");
+        writeStatusFile(TEST_PID, integration("myApp", route));
 
-        JsonArray routes = new JsonArray();
-        routes.add(route1);
-        routes.add(route2);
+        assertThat(call(command("--source"))).isZero();
 
-        JsonObject context = new JsonObject();
-        context.put("name", "myApp");
-        context.put("phase", 5);
+        
assertThat(printer.getOutput()).contains("orders.yaml:12").doesNotContain("route1");
+    }
 
-        JsonObject root = new JsonObject();
-        root.put("context", context);
-        root.put("routes", routes);
-        writeStatusFile(TEST_PID, root);
+    @Test
+    void descriptionAndNoteInTheIdColumn() throws Exception {
+        JsonObject route = route("route1", "timer:tick", "Started", stats(0, 
0, 0, -1));
+        route.put("description", "Polls the orders");
+        route.put("note", "Owned by sales");
+        writeStatusFile(TEST_PID, integration("myApp", route));
 
-        CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.filter = new String[] { "matchingRoute" };
+        assertThat(call(command("--description", "--note"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(printer.getOutput()).contains("route1", "Polls the orders", 
"Owned by sales");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void remoteColumnWhenTheIntegrationTellsWhichEndpointsAreRemote() throws 
Exception {
+        JsonObject kafka = route("kafka", "kafka:in", "Started", stats(0, 0, 
0, -1));
+        kafka.put("remote", true);
+        JsonObject timer = route("timer", "timer:tick", "Started", stats(0, 0, 
0, -1));
+        timer.put("remote", false);
+        writeStatusFile(TEST_PID, integration("myApp", kafka, timer));
+
+        assertThat(call(command())).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("REMOTE");
+        assertThat(lines[1]).contains("kafka", " x ");
+        assertThat(lines[2]).contains("timer").doesNotContain(" x ");
+    }
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("matchingRoute"), "Filtered route 
should appear");
-            assertFalse(output.contains("otherRoute"), "Non-matching route 
should be hidden by filter");
-        }
+    @Test
+    void sortByNameOfTheIntegrations() throws Exception {
+        writeStatusFile(TEST_PID, integration("zeta", route("z1", "timer:z", 
"Started", stats(0, 0, 0, -1))));
+        writeStatusFile(OTHER_PID, integration("alpha", route("a1", "timer:a", 
"Started", stats(0, 0, 0, -1))));
+
+        assertThat(call(command("--sort=name"))).isZero();
+        String output = printer.getOutput();
+        assertThat(output.indexOf("alpha")).isLessThan(output.indexOf("zeta"));
+
+        printer = new StringPrinter();
+        assertThat(call(command("--sort=-name"))).isZero();
+        output = printer.getOutput();
+        assertThat(output.indexOf("zeta")).isLessThan(output.indexOf("alpha"));
     }
 
     @Test
     void testLimitReducesRows() throws Exception {
-        JsonObject routeStats = buildRouteStats();
-        JsonArray routes = new JsonArray();
-        for (int i = 1; i <= 5; i++) {
-            routes.add(buildRoute("route" + i, "timer:tick" + i, routeStats));
+        JsonObject[] routes = new JsonObject[5];
+        for (int i = 0; i < routes.length; i++) {
+            routes[i] = route("route" + (i + 1), "timer:tick" + (i + 1), 
"Started", stats(0, 0, 0, -1));
         }
+        writeStatusFile(TEST_PID, integration("bigApp", routes));
 
-        JsonObject context = new JsonObject();
-        context.put("name", "bigApp");
-        context.put("phase", 5);
+        assertThat(call(command("--limit=2"))).isZero();
 
-        JsonObject root = new JsonObject();
-        root.put("context", context);
-        root.put("routes", routes);
-        writeStatusFile(TEST_PID, root);
+        assertThat(printer.getOutput()).contains("route1", 
"route2").doesNotContain("route3", "route5");
+    }
 
+    private CamelRouteStatus command(String... args) {
         CamelRouteStatus command = new CamelRouteStatus(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.limit = 2;
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
 
+    // runs the command with the integrations of the status files of the test
+    private static int call(CamelRouteStatus command) throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            List<ProcessHandle> handles = Stream.of(TEST_PID, OTHER_PID)
+                    .filter(pid -> 
Files.exists(CommandLineHelper.getCamelDir().resolve(pid + "-status.json")))
+                    
.map(ProcessCommandTestSupport::mockProcessHandle).toList();
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
-
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            // Only 2 routes should appear — limit is enforced during row 
collection
-            assertTrue(output.contains("route1"));
-            assertTrue(output.contains("route2"));
-            assertFalse(output.contains("route5"), "route5 should be excluded 
by limit=2");
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
handles.stream());
+            return command.doCall();
         }
     }
 
-    private static JsonObject buildRouteStats() {
-        JsonObject stats = new JsonObject();
-        stats.put("exchangesTotal", "0");
-        stats.put("exchangesFailed", "0");
-        stats.put("exchangesInflight", "0");
-        stats.put("meanProcessingTime", "-1");
-        stats.put("maxProcessingTime", "0");
-        stats.put("minProcessingTime", "0");
-        return stats;
+    private static JsonObject integration(String name, JsonObject... routes) {
+        JsonObject context = new JsonObject();
+        context.put("name", name);
+        context.put("phase", 5);
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routes", new JsonArray(Arrays.asList(routes)));
+        return root;
     }
 
-    private static JsonObject buildRoute(String routeId, String fromUri, 
JsonObject stats) {
+    // as the route dev console has them
+    private static JsonObject route(String routeId, String from, String state, 
JsonObject stats) {
         JsonObject route = new JsonObject();
         route.put("routeId", routeId);
-        route.put("from", fromUri);
-        route.put("state", "Started");
+        route.put("from", from);
+        route.put("state", state);
         route.put("uptime", "1m");
         route.put("statistics", stats);
         return route;
     }
+
+    private static JsonObject stats(long total, long failed, long inflight, 
long mean) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", total);
+        stats.put("exchangesFailed", failed);
+        stats.put("exchangesInflight", inflight);
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", Math.max(mean, 0));
+        stats.put("minProcessingTime", Math.max(mean, 0));
+        return stats;
+    }
+
+    private static JsonObject lastError(String phase, String message) {
+        JsonObject error = new JsonObject();
+        error.put("phase", phase);
+        error.put("timestamp", System.currentTimeMillis() - 5000);
+        error.put("message", message);
+        error.put("stackTrace", new JsonArray(
+                List.of("java.lang.IllegalStateException: " + message,
+                        "\tat org.example.Kafka.start(Kafka.java:42)")));
+        return error;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
index d72fe9382e42..aad02cd58be3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
@@ -16,160 +16,246 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.process;
 
-import java.util.Collections;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get health: the health checks of the integrations, as the health dev 
console has them in the status file.
+ */
 @ExtendWith(MockitoExtension.class)
 class ListHealthTest extends ProcessCommandTestSupport {
 
     @Test
     void testEmptyOutputWhenNoProcesses() throws Exception {
-        ListHealth command = new ListHealth(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
             mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
 
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            assertEquals("", printer.getStdout());
-            assertEquals("No running Camel integrations", printer.getStderr());
+            assertThat(command().doCall()).isZero();
         }
+
+        assertThat(printer.getStdout()).isEmpty();
+        assertThat(printer.getStderr()).isEqualTo("No running Camel 
integrations");
     }
 
     @Test
-    void testShowsUpHealthCheck() throws Exception {
-        JsonObject root = buildHealthStatus(check("context", "UP", true));
-        writeStatusFile(TEST_PID, root);
+    void checksWithTheirStateRateAndSince() throws Exception {
+        JsonObject context = check("context", "UP", true, true);
+        context.put("details", details(10, 9, 1, "check.kind", "ALL"));
+        writeStatusFile(TEST_PID, integration(context));
 
-        ListHealth command = new ListHealth(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command())).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("PID", "NAME", "AGE", "ID", "RL", 
"STATUS", "RATE", "SINCE", "MESSAGE");
+        assertThat(lines[1]).contains("12345", "myApp", "camel/context", " RL 
", "UP", "10/9/1", "5s/");
+    }
 
-            int exit = command.doCall();
+    @Test
+    void jsonOfTheChecks() throws Exception {
+        JsonObject context = check("context", "UP", true, true);
+        context.put("details", details(10, 9, 1, "check.kind", "ALL"));
+        writeStatusFile(TEST_PID, integration(context));
+
+        assertThat(call(command("--json"))).isZero();
+
+        JsonArray checks = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        assertThat(checks).hasSize(1);
+        JsonObject check = checks.getMap(0);
+        assertThat(check.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(check.getString("id")).isEqualTo("camel/context");
+        assertThat(check.getString("status")).isEqualTo("UP");
+        assertThat(check.getBoolean("readiness")).isTrue();
+        assertThat(check.getBoolean("liveness")).isTrue();
+        assertThat(check.getLong("total")).isEqualTo(10);
+        assertThat(check.getLong("success")).isEqualTo(9);
+        assertThat(check.getLong("failure")).isEqualTo(1);
+        assertThat(check.getString("sinceLast")).isNotBlank();
+    }
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("UP"), "Should show UP state");
-            assertTrue(output.contains("context"), "Should show check ID");
-        }
+    @Test
+    void theRoutesAndConsumersThatAreUpOnlyAtFullLevel() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                check("context", "UP", true, true),
+                check("route:orders", "UP", true, false),
+                check("consumer:orders", "UP", true, false),
+                check("route:payments", "DOWN", true, false)));
+
+        assertThat(call(command("--json"))).isZero();
+        assertThat(ids()).containsExactly("camel/context", 
"camel/route:payments");
+
+        printer = new StringPrinter();
+        assertThat(call(command("--level=full", "--json"))).isZero();
+        assertThat(ids()).containsExactly("camel/context", 
"camel/route:orders", "camel/consumer:orders",
+                "camel/route:payments");
     }
 
     @Test
-    void testShowsDownHealthCheck() throws Exception {
-        JsonObject root = buildHealthStatus(check("context", "DOWN", true));
-        writeStatusFile(TEST_PID, root);
+    void unknownLevelIsAUsageError() {
+        ListHealth command = command("--level=all");
 
-        ListHealth command = new ListHealth(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThatThrownBy(command::doCall)
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--level must be full or default, was: all");
+    }
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+    @Test
+    void downShowsTheChecksThatAreDown() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                check("context", "UP", true, true),
+                check("route:payments", "DOWN", true, false)));
 
-            int exit = command.doCall();
+        assertThat(call(command("--down", "--json"))).isZero();
 
-            assertEquals(0, exit);
-            assertTrue(printer.getOutput().contains("DOWN"), "Should show DOWN 
state");
-        }
+        assertThat(ids()).containsExactly("camel/route:payments");
+    }
+
+    @Test
+    void liveAndReadyByTheKindOfTheCheck() throws Exception {
+        // readiness and liveness, but the kind of the check says which one it 
is
+        JsonObject ready = check("ready-only", "UP", true, true);
+        ready.put("details", details(1, 1, 0, "check.kind", "READINESS"));
+        JsonObject live = check("live-only", "UP", true, true);
+        live.put("details", details(1, 1, 0, "check.kind", "LIVENESS"));
+        writeStatusFile(TEST_PID, integration(ready, live));
+
+        assertThat(call(command("--live", "--json"))).isZero();
+        assertThat(ids()).containsExactly("camel/live-only");
+
+        printer = new StringPrinter();
+        assertThat(call(command("--ready", "--json"))).isZero();
+        assertThat(ids()).containsExactly("camel/ready-only");
     }
 
     @Test
-    void testJsonOutput() throws Exception {
-        JsonObject root = buildHealthStatus(check("context", "UP", true));
-        writeStatusFile(TEST_PID, root);
+    void traceShowsTheStackTraceAndTheDetailsOfTheCheck() throws Exception {
+        writeStatusFile(TEST_PID, integration(failedKafkaCheck()));
 
-        ListHealth command = new ListHealth(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.jsonOutput = true;
+        assertThat(call(command("--trace"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(printer.getOutput()).contains("STACK-TRACE", "CHECK-ID: 
camel/consumer:kafka", "STATE: DOWN",
+                "METADATA:", "bootstrap.servers = localhost:9092", "MESSAGE: 
Broker not available",
+                "org.apache.kafka.common.errors.TimeoutException: Broker not 
available",
+                "at org.apache.kafka.Client.poll(Client.java:10)", "at 
org.apache.kafka.Client.run(Client.java:20)");
+    }
+
+    @Test
+    void depthLimitsTheStackTrace() throws Exception {
+        writeStatusFile(TEST_PID, integration(failedKafkaCheck()));
 
-            int exit = command.doCall();
+        assertThat(call(command("--trace", "--depth=2"))).isZero();
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.startsWith("["), "JSON output should be an 
array");
-            assertTrue(output.contains("context"));
-        }
+        assertThat(printer.getOutput()).contains("at 
org.apache.kafka.Client.poll(Client.java:10)")
+                .doesNotContain("at 
org.apache.kafka.Client.run(Client.java:20)");
     }
 
     @Test
-    void testFilterReadyOnly() throws Exception {
-        JsonObject root = buildHealthStatus(
-                check("context", "UP", true),
-                check("consumer", "DOWN", false));
-        writeStatusFile(TEST_PID, root);
+    void traceAsJson() throws Exception {
+        writeStatusFile(TEST_PID, integration(failedKafkaCheck()));
+
+        assertThat(call(command("--trace", "--json"))).isZero();
+
+        JsonArray checks = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        JsonObject check = checks.getMap(0);
+        assertThat(check.getString("message")).isEqualTo("Broker not 
available");
+        List<String> stackTrace = check.getCollection("stackTrace");
+        assertThat(stackTrace).hasSize(3);
+        Map<String, Object> metadata = check.getMap("metadata");
+        assertThat(metadata).containsEntry("bootstrap.servers", 
"localhost:9092");
+    }
 
+    private List<String> ids() {
+        JsonArray checks = Jsoner.deserialize(printer.getOutput(), (JsonArray) 
null);
+        return checks.stream().map(c -> ((JsonObject) 
c).getString("id")).toList();
+    }
+
+    private ListHealth command(String... args) {
         ListHealth command = new ListHealth(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.ready = true;
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
 
+    // runs the command with the integration of the status file of the test
+    private static int call(ListHealth command) throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
             ProcessHandle ph = mockProcessHandle(TEST_PID);
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
             mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
-
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("context"), "Readiness check should be 
shown");
-            assertFalse(output.contains("consumer"), "Non-readiness check 
should be hidden with --ready");
+            return command.doCall();
         }
     }
 
-    private static JsonObject buildHealthStatus(JsonObject... checks) {
-        JsonArray checksArr = new JsonArray();
-        Collections.addAll(checksArr, checks);
-
-        JsonObject hc = new JsonObject();
-        hc.put("checks", checksArr);
-
-        JsonObject context = new JsonObject();
-        context.put("name", "myApp");
-
+    private static JsonObject integration(JsonObject... checks) {
+        JsonObject health = new JsonObject();
+        health.put("up", true);
+        health.put("checks", new JsonArray(Arrays.asList(checks)));
         JsonObject root = new JsonObject();
-        root.put("context", context);
-        root.put("healthChecks", hc);
+        root.put("context", new JsonObject(Map.of("name", "myApp")));
+        root.put("healthChecks", health);
         return root;
     }
 
-    private static JsonObject check(String id, String state, boolean 
readiness) {
-        JsonObject c = new JsonObject();
-        c.put("id", id);
-        c.put("group", "camel");
-        c.put("state", state);
-        c.put("readiness", readiness);
-        c.put("liveness", true);
-        return c;
+    // as the health dev console has them
+    private static JsonObject check(String id, String state, boolean 
readiness, boolean liveness) {
+        JsonObject check = new JsonObject();
+        check.put("id", id);
+        check.put("group", "camel");
+        check.put("up", "UP".equals(state));
+        check.put("state", state);
+        check.put("enabled", true);
+        check.put("readiness", readiness);
+        check.put("liveness", liveness);
+        return check;
+    }
+
+    // the details are text, the times as the health checks print them
+    private static JsonObject details(int total, int success, int failure, 
String... more) {
+        String fiveSecondsAgo = 
ZonedDateTime.now().minusSeconds(5).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
+        Map<String, Object> details = new LinkedHashMap<>();
+        details.put("invocation.count", String.valueOf(total));
+        details.put("invocation.time", fiveSecondsAgo);
+        details.put("success.count", String.valueOf(success));
+        details.put("failure.count", String.valueOf(failure));
+        for (int i = 0; i < more.length; i += 2) {
+            details.put(more[i], more[i + 1]);
+        }
+        return new JsonObject(details);
+    }
+
+    private static JsonObject failedKafkaCheck() {
+        JsonObject check = check("consumer:kafka", "DOWN", true, false);
+        check.put("message", "Broker not available");
+        check.put("stackTrace", new JsonArray(
+                List.of(
+                        "org.apache.kafka.common.errors.TimeoutException: 
Broker not available",
+                        "\tat org.apache.kafka.Client.poll(Client.java:10)",
+                        "\tat org.apache.kafka.Client.run(Client.java:20)")));
+        check.put("details", details(3, 0, 3, "bootstrap.servers", 
"localhost:9092"));
+        return check;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
index 40f6909a0a8a..c2d56339cdb8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
@@ -16,117 +16,241 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.process;
 
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.Map;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mockStatic;
 
+/**
+ * camel get metric: the Micrometer metrics of the integrations (counters, 
timers, gauges and distribution summaries),
+ * as the micrometer dev console has them in the status file.
+ */
 @ExtendWith(MockitoExtension.class)
 class ListMetricTest extends ProcessCommandTestSupport {
 
     @Test
     void testEmptyOutputWhenNoMetrics() throws Exception {
         JsonObject root = new JsonObject();
-        root.put("context", contextObj());
+        root.put("context", new JsonObject(Map.of("name", "myApp")));
         writeStatusFile(TEST_PID, root);
 
-        ListMetric command = new ListMetric(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
+        assertThat(call(command())).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        assertThat(printer.getOutput()).isEmpty();
+    }
 
-            int exit = command.doCall();
+    @Test
+    void theMetricsOfEachType() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                counter("orders.placed", 42, tag("routeId", "orders")),
+                timer("orders.time", 10, 25, 80, 250, tag("routeId", 
"orders"), tag("nodeId", "to1")),
+                gauge("orders.queue", 7),
+                distribution("orders.size", 4, 1.5, 3, 6)));
 
-            assertEquals(0, exit);
-            assertEquals("", printer.getOutput().trim());
-        }
+        assertThat(call(command())).isZero();
+
+        String[] lines = printer.getOutput().split(System.lineSeparator());
+        assertThat(lines[0]).contains("PID", "NAME", "TYPE", "METRIC", "ID", 
"VALUE", "MEAN", "MAX", "TOTAL")
+                .doesNotContain("TAGS");
+        assertThat(lines).hasSize(5);
+        // the metrics by name
+        assertThat(lines[1]).contains("12345", "myApp", "counter", 
"orders.placed", "orders", "42");
+        assertThat(lines[2]).contains("gauge", "orders.queue", "7");
+        assertThat(lines[3]).contains("distribution", "orders.size", "4", 
"1.5", "3", "6");
+        assertThat(lines[4]).contains("timer", "orders.time", "orders/to1", 
"10", "25", "80", "250");
     }
 
     @Test
-    void testShowsCounterMetric() throws Exception {
-        JsonObject root = new JsonObject();
-        root.put("context", contextObj());
-        root.put("micrometer", micrometerObj(counterEntry(42.0)));
-        writeStatusFile(TEST_PID, root);
+    void jsonOfTheMetrics() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                timer("orders.time", 10, 25, 80, 250, tag("routeId", 
"orders"), tag("nodeId", "to1")),
+                distribution("orders.size", 4, 1.5, 3, 6)));
 
-        ListMetric command = new ListMetric(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.all = true;
+        assertThat(call(command("--json"))).isZero();
 
-        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph = mockProcessHandle(TEST_PID);
-            ProcessHandle currentHandle = mockCurrentHandle();
-            mocked.when(ProcessHandle::current).thenReturn(currentHandle);
-            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+        JsonArray metrics = Jsoner.deserialize(printer.getOutput(), 
(JsonArray) null);
+        assertThat(metrics).hasSize(2);
+        JsonObject size = metrics.getMap(0);
+        assertThat(size.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(size.getString("type")).isEqualTo("distribution");
+        assertThat(size.getString("metric")).isEqualTo("orders.size");
+        assertThat(size.getDouble("value")).isEqualTo(4);
+        assertThat(size.getDouble("mean")).isEqualTo(1.5);
+        assertThat(size.getDouble("total")).isEqualTo(6);
+        JsonObject time = metrics.getMap(1);
+        assertThat(time.getString("type")).isEqualTo("timer");
+        assertThat(time.getString("id")).isEqualTo("orders/to1");
+        assertThat(time.getDouble("max")).isEqualTo(80);
+        assertThat(time.getString("tags")).isEqualTo("routeId=orders 
nodeId=to1");
+    }
+
+    @Test
+    void unusedMetricsOnlyWithAll() throws Exception {
+        writeStatusFile(TEST_PID, integration(counter("orders.placed", 42), 
counter("orders.lost", 0)));
 
-            int exit = command.doCall();
+        assertThat(call(command())).isZero();
+        
assertThat(printer.getOutput()).contains("orders.placed").doesNotContain("orders.lost");
 
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.contains("myCounter"), "Should show metric 
name");
-        }
+        printer = new StringPrinter();
+        assertThat(call(command("--all"))).isZero();
+        assertThat(printer.getOutput()).contains("orders.placed", 
"orders.lost");
     }
 
     @Test
-    void testJsonOutput() throws Exception {
-        JsonObject root = new JsonObject();
-        root.put("context", contextObj());
-        root.put("micrometer", micrometerObj(counterEntry(10.0)));
-        writeStatusFile(TEST_PID, root);
+    void filterByTypeAsInTheHelp() throws Exception {
+        writeStatusFile(TEST_PID, integration(counter("orders.placed", 42), 
timer("orders.time", 10, 25, 80, 250)));
+
+        assertThat(call(command("--filter=timer*"))).isZero();
+
+        
assertThat(printer.getOutput()).contains("orders.time").doesNotContain("orders.placed");
+    }
+
+    @Test
+    void filterByNameOrTag() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                counter("orders.placed", 42, tag("routeId", "orders")),
+                counter("payments.made", 5, tag("routeId", "payments")),
+                counter("audit.logged", 3, tag("routeId", "audit"))));
 
+        assertThat(call(command("--filter=placed"))).isZero();
+        
assertThat(printer.getOutput()).contains("orders.placed").doesNotContain("payments.made",
 "audit.logged");
+
+        printer = new StringPrinter();
+        assertThat(call(command("--filter=routeId=payments"))).isZero();
+        
assertThat(printer.getOutput()).contains("payments.made").doesNotContain("orders.placed",
 "audit.logged");
+    }
+
+    @Test
+    void customLeavesOutTheMetricsOfCamel() throws Exception {
+        writeStatusFile(TEST_PID, integration(
+                // the micrometer naming (the default) and the classic naming 
of the metrics of camel
+                counter("camel.exchanges.total", 42),
+                counter("CamelExchangesTotal", 42),
+                counter("orders.placed", 5)));
+
+        assertThat(call(command("--custom"))).isZero();
+
+        // not even the start of the names, as the column is narrow
+        
assertThat(printer.getOutput()).contains("orders.placed").doesNotContain("camel.",
 "CamelExch");
+    }
+
+    @Test
+    void tagsColumn() throws Exception {
+        writeStatusFile(TEST_PID, integration(counter("orders.placed", 42, 
tag("routeId", "orders"), tag("nodeId", "to1"))));
+
+        assertThat(call(command("--tags"))).isZero();
+
+        
assertThat(printer.getOutput().split(System.lineSeparator())[0]).contains("TAGS");
+        // across lines when wider than the column
+        assertThat(printer.getOutput().replaceAll("\\s", 
"")).contains("routeId=ordersnodeId=to1");
+    }
+
+    @Test
+    void theInfoGaugeOfTheApplicationIsNotAMetric() throws Exception {
+        writeStatusFile(TEST_PID, integration(gauge("app.info", 1), 
gauge("orders.queue", 7)));
+
+        assertThat(call(command())).isZero();
+
+        
assertThat(printer.getOutput()).contains("orders.queue").doesNotContain("app.info");
+    }
+
+    private ListMetric command(String... args) {
         ListMetric command = new ListMetric(new 
CamelJBangMain().withPrinter(printer));
-        command.sort = "pid";
-        command.all = true;
-        command.jsonOutput = true;
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
 
+    // runs the command with the integration of the status file of the test
+    private static int call(ListMetric command) throws Exception {
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
             ProcessHandle ph = mockProcessHandle(TEST_PID);
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
             mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+            return command.doCall();
+        }
+    }
 
-            int exit = command.doCall();
-
-            assertEquals(0, exit);
-            String output = printer.getOutput();
-            assertTrue(output.startsWith("["), "JSON output should be array");
-            assertTrue(output.contains("myCounter"));
+    private static JsonObject integration(JsonObject... metrics) {
+        JsonArray counters = new JsonArray();
+        JsonArray timers = new JsonArray();
+        JsonArray gauges = new JsonArray();
+        JsonArray distribution = new JsonArray();
+        for (JsonObject metric : metrics) {
+            String type = (String) metric.remove("type");
+            switch (type) {
+                case "counter" -> counters.add(metric);
+                case "timer" -> timers.add(metric);
+                case "gauge" -> gauges.add(metric);
+                default -> distribution.add(metric);
+            }
         }
+        JsonObject micrometer = new JsonObject();
+        micrometer.put("counters", counters);
+        micrometer.put("timers", timers);
+        micrometer.put("gauges", gauges);
+        micrometer.put("distribution", distribution);
+        JsonObject root = new JsonObject();
+        root.put("context", new JsonObject(Map.of("name", "myApp")));
+        root.put("micrometer", micrometer);
+        return root;
     }
 
-    private static JsonObject contextObj() {
-        JsonObject ctx = new JsonObject();
-        ctx.put("name", "myApp");
-        return ctx;
+    // as the micrometer dev console has them
+    private static JsonObject counter(String name, long count, JsonObject... 
tags) {
+        JsonObject metric = metric("counter", name, tags);
+        metric.put("count", count);
+        return metric;
     }
 
-    private static JsonObject micrometerObj(JsonObject... counters) {
-        JsonArray arr = new JsonArray();
-        Collections.addAll(arr, counters);
-        JsonObject mo = new JsonObject();
-        mo.put("counters", arr);
-        return mo;
+    private static JsonObject timer(String name, long count, long mean, long 
max, long total, JsonObject... tags) {
+        JsonObject metric = metric("timer", name, tags);
+        metric.put("count", count);
+        metric.put("mean", mean);
+        metric.put("max", max);
+        metric.put("total", total);
+        return metric;
+    }
+
+    private static JsonObject gauge(String name, double value) {
+        JsonObject metric = metric("gauge", name);
+        metric.put("value", value);
+        return metric;
+    }
+
+    private static JsonObject distribution(String name, long count, double 
mean, double max, double totalAmount) {
+        JsonObject metric = metric("distribution", name);
+        metric.put("count", count);
+        metric.put("mean", mean);
+        metric.put("max", max);
+        metric.put("totalAmount", totalAmount);
+        return metric;
+    }
+
+    private static JsonObject metric(String type, String name, JsonObject... 
tags) {
+        JsonObject metric = new JsonObject();
+        metric.put("type", type);
+        metric.put("name", name);
+        if (tags.length > 0) {
+            metric.put("tags", new JsonArray(Arrays.asList(tags)));
+        }
+        return metric;
     }
 
-    private static JsonObject counterEntry(double count) {
-        JsonObject c = new JsonObject();
-        c.put("name", "myCounter");
-        c.put("description", "");
-        c.put("count", count);
-        return c;
+    private static JsonObject tag(String key, String value) {
+        return new JsonObject(Map.of("key", key, "value", value));
     }
 }

Reply via email to