This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new a6dcfd06bfe camel-resilienc4j - Add dev console and Camel CLI command a6dcfd06bfe is described below commit a6dcfd06bfe14faecf8d2f91927ace44d2147f37 Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Mon Oct 31 12:57:15 2022 +0100 camel-resilienc4j - Add dev console and Camel CLI command --- components/camel-resilience4j/pom.xml | 4 + .../org/apache/camel/dev-console/resilience4j | 2 + .../component/resilience4j/ResilienceConsole.java | 91 +++++++++++ .../camel/cli/connector/LocalCliConnector.java | 7 + .../dsl/jbang/core/commands/CamelJBangMain.java | 2 + .../core/commands/process/ListCircuitBreaker.java | 175 +++++++++++++++++++++ 6 files changed, 281 insertions(+) diff --git a/components/camel-resilience4j/pom.xml b/components/camel-resilience4j/pom.xml index e0a0c7aa09a..b9ad920aa3a 100644 --- a/components/camel-resilience4j/pom.xml +++ b/components/camel-resilience4j/pom.xml @@ -45,6 +45,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-core-reifier</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-console</artifactId> + </dependency> <dependency> <groupId>io.github.resilience4j</groupId> diff --git a/components/camel-resilience4j/src/generated/resources/META-INF/services/org/apache/camel/dev-console/resilience4j b/components/camel-resilience4j/src/generated/resources/META-INF/services/org/apache/camel/dev-console/resilience4j new file mode 100644 index 00000000000..0eab670f554 --- /dev/null +++ b/components/camel-resilience4j/src/generated/resources/META-INF/services/org/apache/camel/dev-console/resilience4j @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.component.resilience4j.ResilienceConsole diff --git a/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceConsole.java b/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceConsole.java new file mode 100644 index 00000000000..1e41d832ee2 --- /dev/null +++ b/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceConsole.java @@ -0,0 +1,91 @@ +package org.apache.camel.component.resilience4j; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.apache.camel.Processor; +import org.apache.camel.Route; +import org.apache.camel.impl.console.AbstractDevConsole; +import org.apache.camel.spi.annotations.DevConsole; +import org.apache.camel.util.json.JsonObject; + +@DevConsole("resilience4j") +public class ResilienceConsole extends AbstractDevConsole { + + public ResilienceConsole() { + super("camel", "resilience4j", "Resilience Circuit Breaker", "Display circuit breaker information"); + } + + @Override + protected String doCallText(Map<String, Object> options) { + StringBuilder sb = new StringBuilder(); + + List<ResilienceProcessor> cbs = new ArrayList<>(); + for (Route route : getCamelContext().getRoutes()) { + List<Processor> list = route.filter("*"); + for (Processor p : list) { + if (p instanceof ResilienceProcessor) { + cbs.add((ResilienceProcessor) p); + } + } + } + // sort by ids + cbs.sort(Comparator.comparing(ResilienceProcessor::getId)); + + for (ResilienceProcessor cb : cbs) { + String id = cb.getId(); + String rid = cb.getRouteId(); + String state = cb.getCircuitBreakerState(); + int sc = cb.getNumberOfSuccessfulCalls(); + int bc = cb.getNumberOfBufferedCalls(); + int fc = cb.getNumberOfFailedCalls(); + long npc = cb.getNumberOfNotPermittedCalls(); + float fr = cb.getFailureRate(); + if (fr > 0) { + sb.append(String.format(" %s/%s: %s (buffered: %d success: %d failure: %d/%.0f%% not-permitted: %d)\n", rid, + id, state, bc, sc, fc, fr, npc)); + } else { + sb.append(String.format(" %s/%s: %s (buffered: %d success: %d failure: 0 not-permitted: %d)\n", rid, id, + state, bc, sc, npc)); + } + } + + return sb.toString(); + } + + @Override + protected JsonObject doCallJson(Map<String, Object> options) { + JsonObject root = new JsonObject(); + + List<ResilienceProcessor> cbs = new ArrayList<>(); + for (Route route : getCamelContext().getRoutes()) { + List<Processor> list = route.filter("*"); + for (Processor p : list) { + if (p instanceof ResilienceProcessor) { + cbs.add((ResilienceProcessor) p); + } + } + } + // sort by ids + cbs.sort(Comparator.comparing(ResilienceProcessor::getId)); + + final List<JsonObject> list = new ArrayList<>(); + for (ResilienceProcessor cb : cbs) { + JsonObject jo = new JsonObject(); + jo.put("id", cb.getId()); + jo.put("routeId", cb.getRouteId()); + jo.put("state", cb.getCircuitBreakerState()); + jo.put("bufferedCalls", cb.getNumberOfBufferedCalls()); + jo.put("successfulCalls", cb.getNumberOfSuccessfulCalls()); + jo.put("failedCalls", cb.getNumberOfFailedCalls()); + jo.put("notPermittedCalls", cb.getNumberOfNotPermittedCalls()); + jo.put("failureRate", cb.getFailureRate()); + list.add(jo); + } + root.put("circuitBreakers", list); + + return root; + } +} diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java index 1d9d82dc330..6e1ba6d7cba 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java @@ -405,6 +405,13 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C root.put("micrometer", json); } } + DevConsole dc10 = dcr.resolveById("resilience4j"); + if (dc10 != null) { + JsonObject json = (JsonObject) dc10.call(DevConsole.MediaType.JSON); + if (json != null && !json.isEmpty()) { + root.put("resilience4j", json); + } + } } // various details JsonObject services = collectServices(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 38645ac1c88..2122d081f12 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -50,6 +50,7 @@ import org.apache.camel.dsl.jbang.core.commands.process.CamelTop; import org.apache.camel.dsl.jbang.core.commands.process.Hawtio; import org.apache.camel.dsl.jbang.core.commands.process.Jolokia; import org.apache.camel.dsl.jbang.core.commands.process.ListBlocked; +import org.apache.camel.dsl.jbang.core.commands.process.ListCircuitBreaker; import org.apache.camel.dsl.jbang.core.commands.process.ListEvent; import org.apache.camel.dsl.jbang.core.commands.process.ListHealth; import org.apache.camel.dsl.jbang.core.commands.process.ListInflight; @@ -82,6 +83,7 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("inflight", new CommandLine(new ListInflight(main))) .addSubcommand("blocked", new CommandLine(new ListBlocked(main))) .addSubcommand("route-controller", new CommandLine(new RouteControllerAction(main))) + .addSubcommand("circuit-breaker", new CommandLine(new ListCircuitBreaker(main))) .addSubcommand("micrometer", new CommandLine(new ListMicrometer(main))) .addSubcommand("service", new CommandLine(new ListService(main))) .addSubcommand("source", new CommandLine(new CamelSourceAction(main))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreaker.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreaker.java new file mode 100644 index 00000000000..6e520c31726 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreaker.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.process; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; +import com.github.freva.asciitable.OverflowBehaviour; +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.util.TimeUtils; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "circuit-breaker", + description = "Get status of Circuit Breaker EIPs") +public class ListCircuitBreaker extends ProcessBaseCommand { + + @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") + String name = "*"; + + @CommandLine.Option(names = { "--sort" }, + description = "Sort by pid, name or age", defaultValue = "pid") + String sort; + + public ListCircuitBreaker(CamelJBangMain main) { + super(main); + } + + @Override + public Integer call() throws Exception { + List<Row> rows = new ArrayList<>(); + + List<Long> pids = findPids(name); + ProcessHandle.allProcesses() + .filter(ph -> pids.contains(ph.pid())) + .forEach(ph -> { + JsonObject root = loadStatus(ph.pid()); + // there must be a status file for the running Camel integration + if (root != null) { + Row row = new Row(); + JsonObject context = (JsonObject) root.get("context"); + if (context == null) { + return; + } + row.name = context.getString("name"); + if ("CamelJBang".equals(row.name)) { + row.name = extractName(root, ph); + } + row.pid = "" + ph.pid(); + row.uptime = extractSince(ph); + row.age = TimeUtils.printSince(row.uptime); + Row baseRow = row.copy(); + + JsonObject mo = (JsonObject) root.get("resilience4j"); + if (mo != null) { + JsonArray arr = (JsonArray) mo.get("circuitBreakers"); + if (arr != null) { + for (int i = 0; i < arr.size(); i++) { + row = baseRow.copy(); + JsonObject jo = (JsonObject) arr.get(i); + row.component = "camel-resilience4j"; + row.id = jo.getString("id"); + row.routeId = jo.getString("routeId"); + row.state = jo.getString("state"); + row.bufferedCalls = jo.getInteger("bufferedCalls"); + row.successfulCalls = jo.getInteger("successfulCalls"); + row.failedCalls = jo.getInteger("failedCalls"); + row.notPermittedCalls = jo.getLong("notPermittedCalls"); + row.failureRate = jo.getDouble("failureRate"); + rows.add(row); + } + } + } + } + }); + + // sort rows + rows.sort(this::sortRow); + + if (!rows.isEmpty()) { + System.out.println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( + new Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> r.pid), + new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT) + .with(r -> r.name), + new Column().header("COMPONENT").dataAlign(HorizontalAlign.LEFT).with(r -> r.component), + new Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT).with(r -> r.routeId), + new Column().header("ID").dataAlign(HorizontalAlign.LEFT).with(r -> r.id), + new Column().header("STATE").dataAlign(HorizontalAlign.LEFT).with(r -> r.state), + new Column().header("BUFFER").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) + .with(r -> "" + r.bufferedCalls), + new Column().header("SUCCESS").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) + .with(r -> "" + r.successfulCalls), + new Column().header("FAIL").headerAlign(HorizontalAlign.CENTER).dataAlign(HorizontalAlign.RIGHT) + .with(this::getFailure), + new Column().header("REJECT").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT) + .with(r -> "" + r.notPermittedCalls)))); + } + + return 0; + } + + protected int sortRow(Row o1, Row o2) { + String s = sort; + int negate = 1; + if (s.startsWith("-")) { + s = s.substring(1); + negate = -1; + } + switch (s) { + case "pid": + return Long.compare(Long.parseLong(o1.pid), Long.parseLong(o2.pid)) * negate; + case "name": + return o1.name.compareToIgnoreCase(o2.name) * negate; + case "age": + return Long.compare(o1.uptime, o2.uptime) * negate; + default: + return 0; + } + } + + private String getFailure(Row r) { + if (r.failedCalls <= 0) { + return ""; + } else if (r.failureRate > 0) { + return +r.failedCalls + " (" + String.format("%.0f", r.failureRate) + "%)"; + } else { + return "" + r.failedCalls; + } + } + + private static class Row implements Cloneable { + String pid; + String name; + String age; + long uptime; + String component; + String id; + String routeId; + String state; + int bufferedCalls; + int successfulCalls; + int failedCalls; + long notPermittedCalls; + double failureRate; + + Row copy() { + try { + return (Row) clone(); + } catch (CloneNotSupportedException e) { + return null; + } + } + } + +}