You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2022/10/15 13:32:05 UTC

[camel] branch main updated: camel-jbang: Route controller CLI command

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 1276085c9d9 camel-jbang: Route controller CLI command
1276085c9d9 is described below

commit 1276085c9d9216a745792e4b91336575fdd58c2d
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sat Oct 15 15:31:38 2022 +0200

    camel-jbang: Route controller CLI command
---
 .../camel/impl/console/RouteControllerConsole.java |  15 +-
 .../java/org/apache/camel/util/StringHelper.java   |  17 ++
 .../org/apache/camel/util/StringHelperTest.java    |   8 +
 .../camel/cli/connector/LocalCliConnector.java     |   8 +
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../core/commands/action/CamelThreadDump.java      |   2 +-
 .../commands/action/RouteControllerAction.java     | 285 +++++++++++++++++++++
 7 files changed, 331 insertions(+), 6 deletions(-)

diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteControllerConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteControllerConsole.java
index ce35bb6a5cf..0a5fc83aad4 100644
--- a/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteControllerConsole.java
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/RouteControllerConsole.java
@@ -19,6 +19,7 @@ package org.apache.camel.impl.console;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
@@ -93,7 +94,7 @@ public class RouteControllerConsole extends AbstractDevConsole {
                 long time = state != null && BackOffTimer.Task.Status.Active == state.getStatus()
                         ? state.getFirstAttemptTime() : 0;
                 if (time > 0) {
-                    elapsed = TimeUtils.printSince(time);
+                    elapsed = TimeUtils.printDuration(time);
                 }
                 time = state != null && BackOffTimer.Task.Status.Active == state.getStatus() ? state.getLastAttemptTime() : 0;
                 if (time > 0) {
@@ -169,6 +170,7 @@ public class RouteControllerConsole extends AbstractDevConsole {
             long started = routes.stream().filter(r -> src.getRouteStatus(r.getRouteId()).isStarted())
                     .count();
 
+            root.put("controller", "SupervisingRouteController");
             root.put("totalRoutes", routes.size());
             root.put("startedRoutes", started);
             root.put("restartingRoutes", src.getRestartingRoutes().size());
@@ -198,7 +200,7 @@ public class RouteControllerConsole extends AbstractDevConsole {
                 long time = state != null && BackOffTimer.Task.Status.Active == state.getStatus()
                         ? state.getCurrentElapsedTime() : 0;
                 if (time > 0) {
-                    elapsed = TimeUtils.printSince(time);
+                    elapsed = TimeUtils.printDuration(time);
                 }
                 time = state != null && BackOffTimer.Task.Status.Active == state.getStatus() ? state.getLastAttemptTime() : 0;
                 if (time > 0) {
@@ -225,10 +227,12 @@ public class RouteControllerConsole extends AbstractDevConsole {
                         jo.put("error", Jsoner.escape(error));
                         if (includeStacktrace) {
                             JsonArray arr2 = new JsonArray();
+                            StringWriter writer = new StringWriter();
+                            cause.printStackTrace(new PrintWriter(writer));
+                            writer.flush();
+                            String trace = writer.toString();
                             jo.put("stackTrace", arr2);
-                            for (StackTraceElement e : cause.getStackTrace()) {
-                                arr2.add(e.toString());
-                            }
+                            Collections.addAll(arr2, trace.split("\n"));
                         }
                     }
                 }
@@ -237,6 +241,7 @@ public class RouteControllerConsole extends AbstractDevConsole {
             Set<Route> routes = new TreeSet<>(Comparator.comparing(Route::getId));
             routes.addAll(rc.getControlledRoutes());
 
+            root.put("controller", "DefaultRouteController");
             root.put("totalRoutes", routes.size());
             root.put("routes", list);
             for (Route route : routes) {
diff --git a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
index a8b7a221fa7..603edd3d434 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
@@ -1139,4 +1139,21 @@ public final class StringHelper {
         }
     }
 
+    /**
+     * Fills the string with repeating chars
+     *
+     * @param ch  the char
+     * @param count number of chars
+     */
+    public static String fillChars(char ch, int count) {
+        if (count <= 0) {
+            return "";
+        } else {
+            byte[] arr = new byte[count];
+            byte b = (byte) ch;
+            Arrays.fill(arr, b);
+            return new String(arr);
+        }
+    }
+
 }
diff --git a/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java b/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
index e9c0ad5628a..8f90d4688a9 100644
--- a/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
+++ b/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
@@ -225,4 +225,12 @@ public class StringHelperTest {
         assertEquals("", StringHelper.padString(0));
         assertEquals("", StringHelper.padString(0, 2));
     }
+
+    @Test
+    public void testFillChars() {
+        assertEquals("", StringHelper.fillChars('-', 0));
+        assertEquals("==", StringHelper.fillChars('=', 2));
+        assertEquals("----", StringHelper.fillChars('-', 4));
+        assertEquals("..........", StringHelper.fillChars('.', 10));
+    }
 }
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 15c476dccb0..a611e9bac59 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
@@ -278,6 +278,14 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
                     LOG.trace("Updating output file: {}", outputFile);
                     IOHelper.writeText(json.toJson(), outputFile);
                 }
+            } else if ("route-controller".equals(action)) {
+                DevConsole dc = camelContext.getExtension(DevConsoleRegistry.class).resolveById("route-controller");
+                if (dc != null) {
+                    String stacktrace = root.getString("stacktrace");
+                    JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("stacktrace", stacktrace));
+                    LOG.trace("Updating output file: {}", outputFile);
+                    IOHelper.writeText(json.toJson(), outputFile);
+                }
             }
 
             // action done so delete file
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 e5b823631ea..65999c69f5a 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
@@ -30,6 +30,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceTop;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump;
 import org.apache.camel.dsl.jbang.core.commands.action.LoggerAction;
+import org.apache.camel.dsl.jbang.core.commands.action.RouteControllerAction;
 import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand;
 import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogComponent;
 import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDataFormat;
@@ -77,6 +78,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("event", new CommandLine(new ListEvent(main)))
                         .addSubcommand("inflight", new CommandLine(new ListInflight(main)))
                         .addSubcommand("blocked", new CommandLine(new ListBlocked(main)))
+                        .addSubcommand("route-controller", new CommandLine(new RouteControllerAction(main)))
                         .addSubcommand("service", new CommandLine(new ListService(main)))
                         .addSubcommand("source", new CommandLine(new CamelSourceAction(main)))
                         .addSubcommand("vault", new CommandLine(new ListVault(main))))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
index bbfa0534539..e12dbd2dda0 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
@@ -52,7 +52,7 @@ public class CamelThreadDump extends ActionBaseCommand {
     String filter;
 
     @CommandLine.Option(names = { "--trace" },
-                        description = "Include stack traces", defaultValue = "false")
+                        description = "Include stack-traces", defaultValue = "false")
     boolean trace;
 
     @CommandLine.Option(names = { "--depth" },
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
new file mode 100644
index 00000000000..5e521cdcd33
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+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.FileUtil;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "route-controller", description = "List status of route controller in a running Camel integration")
+public class RouteControllerAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "1")
+    String name;
+
+    @CommandLine.Option(names = { "--sort" },
+                        description = "Sort by id, or state", defaultValue = "id")
+    String sort;
+
+    @CommandLine.Option(names = { "--header" },
+                        description = "Include controller configuration details", defaultValue = "true")
+    boolean header;
+
+    @CommandLine.Option(names = { "--trace" },
+                        description = "Include stack-traces in error messages", defaultValue = "true")
+    boolean trace;
+
+    @CommandLine.Option(names = { "--depth" },
+                        description = "Max depth of stack-trace", defaultValue = "1")
+    int depth;
+
+    private volatile long pid;
+
+    public RouteControllerAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer call() throws Exception {
+        List<Row> rows = new ArrayList<>();
+
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            return 0;
+        } else if (pids.size() > 1) {
+            System.out.println("Name or pid " + name + " matches " + pids.size()
+                               + " running Camel integrations. Specify a name or PID that matches exactly one.");
+            return 0;
+        }
+
+        // include stack-traces
+        if (trace && depth <= 1) {
+            depth = Integer.MAX_VALUE;
+        }
+
+        this.pid = pids.get(0);
+
+        // ensure output file is deleted before executing action
+        File outputFile = getOutputFile("" + pid);
+        FileUtil.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "route-controller");
+        root.put("stacktrace", trace ? "true" : "false");
+        File f = getActionFile("" + pid);
+        try {
+            IOHelper.writeText(root.toJson(), f);
+        } catch (Exception e) {
+            // ignore
+        }
+
+        boolean supervising;
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            supervising = "SupervisingRouteController".equals(jo.getString("controller"));
+
+            JsonArray arr = (JsonArray) jo.get("routes");
+            for (int i = 0; i < arr.size(); i++) {
+                JsonObject jt = (JsonObject) arr.get(i);
+
+                Row row = new Row();
+                row.routeId = jt.getString("routeId");
+                row.uri = jt.getString("uri");
+                row.status = jt.getString("status");
+
+                if (supervising) {
+                    row.attempts = jt.getLong("attempts");
+                    row.lastAttemptAgo = jt.getString("lastAttemptAgo");
+                    row.nextAttempt = jt.getString("nextAttempt");
+                    row.elapsed = jt.getString("elapsed");
+                    row.supervising = jt.getString("supervising");
+                    row.error = jt.getString("error");
+                    row.stackTrace = jt.getCollection("stackTrace");
+                }
+
+                rows.add(row);
+            }
+        } else {
+            System.out.println("Response from running Camel with PID " + pid + " not received within 5 seconds");
+            return 1;
+        }
+
+        // sort rows
+        rows.sort(this::sortRow);
+
+        if (!rows.isEmpty()) {
+            if (supervising) {
+                if (header) {
+                    System.out.println("Supervising Route Controller");
+                    System.out.printf("\tRoutes Total: %s%n", jo.getInteger("totalRoutes"));
+                    System.out.printf("\tRoutes Started: %d%n", jo.getInteger("startedRoutes"));
+                    System.out.printf("\tRoutes Restarting: %d%n", jo.getInteger("restartingRoutes"));
+                    System.out.printf("\tRoutes Exhausted: %d%n", jo.getInteger("exhaustedRoutes"));
+                    System.out.printf("\tInitial Delay: %d%n", jo.getInteger("initialDelay"));
+                    System.out.printf("\tBackoff Delay: %d%n", jo.getInteger("backoffDelay"));
+                    System.out.printf("\tBackoff Max Delay: %d%n", jo.getInteger("backoffMaxDelay"));
+                    System.out.printf("\tBackoff Max Elapsed Time: %d%n", jo.getInteger("backoffMaxElapsedTime"));
+                    System.out.printf("\tBackoff Max Attempts: %d%n", jo.getInteger("backoffMaxAttempts"));
+                    System.out.printf("\tThread Pool Size: %d%n", jo.getInteger("threadPoolSize"));
+                    System.out.printf("\tUnhealthy on Exhaust: %b%n", jo.getBoolean("unhealthyOnExhausted"));
+                    System.out.println("\n");
+                }
+                dumpTable(rows, true);
+            } else {
+                if (header) {
+                    System.out.println("Default Route Controller");
+                    System.out.printf("\tRoutes Total: %s%n", jo.getInteger("totalRoutes"));
+                    System.out.println("\n");
+                }
+                dumpTable(rows, false);
+            }
+        }
+
+        // delete output file after use
+        FileUtil.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    protected void dumpTable(List<Row> rows, boolean supervised) {
+        System.out.println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                new Column().header("ID").dataAlign(HorizontalAlign.LEFT).maxWidth(25, OverflowBehaviour.ELLIPSIS_RIGHT)
+                        .with(r -> r.routeId),
+                new Column().header("URI").dataAlign(HorizontalAlign.LEFT).maxWidth(60, OverflowBehaviour.ELLIPSIS_RIGHT)
+                        .with(r -> r.uri),
+                new Column().header("STATE").headerAlign(HorizontalAlign.RIGHT).with(this::getSupervising),
+                new Column().visible(supervised).header("ATTEMPT").headerAlign(HorizontalAlign.CENTER)
+                        .dataAlign(HorizontalAlign.CENTER).with(this::getAttempts),
+                new Column().visible(supervised).header("LAST-AGO").headerAlign(HorizontalAlign.CENTER).with(this::getLast),
+                new Column().visible(supervised).header("ERROR-MESSAGE").headerAlign(HorizontalAlign.LEFT)
+                        .dataAlign(HorizontalAlign.LEFT)
+                        .maxWidth(80, OverflowBehaviour.ELLIPSIS_RIGHT).with(r -> r.error))));
+
+        if (supervised && trace) {
+            rows = rows.stream().filter(r -> r.error != null && !r.error.isEmpty()).collect(Collectors.toList());
+            if (!rows.isEmpty()) {
+                for (Row row : rows) {
+                    System.out.println("\n");
+                    System.out.println(StringHelper.fillChars('-', 120));
+                    System.out.println(StringHelper.padString(1, 55) + "STACK-TRACE");
+                    System.out.println(StringHelper.fillChars('-', 120));
+                    StringBuilder sb = new StringBuilder();
+                    sb.append(String.format("\tID: %s%n", row.routeId));
+                    sb.append(String.format("\tURI: %s%n", row.uri));
+                    sb.append(String.format("\tSTATE: %s%n", getSupervising(row)));
+                    for (int i = 0; i < depth && i < row.stackTrace.size(); i++) {
+                        sb.append(String.format("\t%s%n", row.stackTrace.get(i)));
+                    }
+                    System.out.println(sb);
+                }
+            }
+        }
+    }
+
+    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 "id":
+                return o1.routeId.compareToIgnoreCase(o2.routeId) * negate;
+            case "state":
+                return o1.status.compareToIgnoreCase(o2.status) * negate;
+            default:
+                return 0;
+        }
+    }
+
+    protected JsonObject waitForOutputFile(File outputFile) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < 5000) {
+            try {
+                // give time for response to be ready
+                Thread.sleep(100);
+
+                if (outputFile.exists()) {
+                    FileInputStream fis = new FileInputStream(outputFile);
+                    String text = IOHelper.loadText(fis);
+                    IOHelper.close(fis);
+                    return (JsonObject) Jsoner.deserialize(text);
+                }
+
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return null;
+    }
+
+    protected String getSupervising(Row r) {
+        if (r.supervising != null) {
+            if ("Active".equals(r.supervising)) {
+                if (r.attempts <= 1) {
+                    return "Starting";
+                } else {
+                    return "Restarting";
+                }
+            }
+            return r.supervising;
+        }
+        return r.status;
+    }
+
+    protected String getAttempts(Row r) {
+        if (r.supervising != null) {
+            return "" + r.attempts;
+        }
+        return "";
+    }
+
+    protected String getLast(Row r) {
+        if (!r.lastAttemptAgo.isEmpty()) {
+            return r.lastAttemptAgo + " (" + r.elapsed + ")";
+        }
+        return "";
+    }
+
+    private static class Row {
+        String routeId;
+        String status;
+        String uri;
+        long attempts;
+        String lastAttemptAgo;
+        String nextAttempt;
+        String elapsed;
+        String supervising;
+        String error;
+        List<String> stackTrace;
+    }
+
+}