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)); } }
