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 2f0e8903e90 CAMEL-19960: camel-jbang - Add camel get startup-recorder 2f0e8903e90 is described below commit 2f0e8903e904542bb2d13aa20e0f3ff2c37cf428 Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Thu Oct 5 20:17:11 2023 +0200 CAMEL-19960: camel-jbang - Add camel get startup-recorder --- .../org/apache/camel/main/BaseMainSupport.java | 15 +- .../camel/cli/connector/LocalCliConnector.java | 8 + .../dsl/jbang/core/commands/CamelJBangMain.java | 2 + .../action/CamelStartupRecorderAction.java | 196 +++++++++++++++++++++ .../java/org/apache/camel/main/KameletMain.java | 3 + 5 files changed, 220 insertions(+), 4 deletions(-) diff --git a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java index b529a16f91f..bc9760e909e 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java @@ -39,6 +39,7 @@ import org.apache.camel.CamelConfiguration; import org.apache.camel.CamelContext; import org.apache.camel.Component; import org.apache.camel.Configuration; +import org.apache.camel.ExtendedCamelContext; import org.apache.camel.NoSuchLanguageException; import org.apache.camel.PropertiesLookupListener; import org.apache.camel.RuntimeCamelException; @@ -484,6 +485,8 @@ public abstract class BaseMainSupport extends BaseService { } protected void configureStartupRecorder(CamelContext camelContext) { + ExtendedCamelContext ecc = camelContext.getCamelContextExtension(); + // we need to load these configurations early as they control the startup recorder when using camel-jfr // and we want to start jfr recording as early as possible to also capture details during bootstrapping Camel @@ -516,16 +519,20 @@ public abstract class BaseMainSupport extends BaseService { if ("off".equals(mainConfigurationProperties.getStartupRecorder()) || "false".equals(mainConfigurationProperties.getStartupRecorder())) { - camelContext.getCamelContextExtension().getStartupStepRecorder().setEnabled(false); + ecc.getStartupStepRecorder().setEnabled(false); } else if ("logging".equals(mainConfigurationProperties.getStartupRecorder())) { - camelContext.getCamelContextExtension().setStartupStepRecorder(new LoggingStartupStepRecorder()); + if (!(ecc.getStartupStepRecorder() instanceof LoggingStartupStepRecorder)) { + ecc.setStartupStepRecorder(new LoggingStartupStepRecorder()); + } } else if ("backlog".equals(mainConfigurationProperties.getStartupRecorder())) { - camelContext.getCamelContextExtension().setStartupStepRecorder(new BacklogStartupStepRecorder()); + if (!(ecc.getStartupStepRecorder() instanceof BacklogStartupStepRecorder)) { + ecc.setStartupStepRecorder(new BacklogStartupStepRecorder()); + } } else if ("jfr".equals(mainConfigurationProperties.getStartupRecorder()) || "java-flight-recorder".equals(mainConfigurationProperties.getStartupRecorder()) || mainConfigurationProperties.getStartupRecorder() == null) { // try to auto discover camel-jfr to use - StartupStepRecorder fr = camelContext.getCamelContextExtension().getBootstrapFactoryFinder() + StartupStepRecorder fr = ecc.getBootstrapFactoryFinder() .newInstance(StartupStepRecorder.FACTORY, StartupStepRecorder.class).orElse(null); if (fr != null) { LOG.debug("Discovered startup recorder: {} from classpath", fr); 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 365f13b6908..9ff207ec741 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 @@ -324,6 +324,14 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C LOG.trace("Updating output file: {}", outputFile); IOHelper.writeText(json.toJson(), outputFile); } + } else if ("startup-recorder".equals(action)) { + DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) + .resolveById("startup-recorder"); + if (dc != null) { + JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON); + LOG.trace("Updating output file: {}", outputFile); + IOHelper.writeText(json.toJson(), outputFile); + } } else if ("stub".equals(action)) { String filter = root.getString("filter"); String limit = root.getString("limit"); 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 4f052d10534..27235d7e27c 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 @@ -31,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStopAction; import org.apache.camel.dsl.jbang.core.commands.action.CamelSendAction; 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.CamelStartupRecorderAction; import org.apache.camel.dsl.jbang.core.commands.action.CamelStubAction; import org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump; import org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction; @@ -108,6 +109,7 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("service", new CommandLine(new ListService(main))) .addSubcommand("source", new CommandLine(new CamelSourceAction(main))) .addSubcommand("route-dump", new CommandLine(new CamelRouteDumpAction(main))) + .addSubcommand("startup-recorder", new CommandLine(new CamelStartupRecorderAction(main))) .addSubcommand("vault", new CommandLine(new ListVault(main)))) .addSubcommand("top", new CommandLine(new CamelTop(main)) .addSubcommand("context", new CommandLine(new CamelContextTop(main))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderAction.java new file mode 100644 index 00000000000..33d51103982 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderAction.java @@ -0,0 +1,196 @@ +/* + * 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.Iterator; +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.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; + +@CommandLine.Command(name = "startup-recorder", + description = "Display startup recording", sortOptions = false) +public class CamelStartupRecorderAction extends ActionWatchCommand { + + @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") + String name = "*"; + + @CommandLine.Option(names = { "--sort" }, completionCandidates = DurationTypeCompletionCandidates.class, + description = "Sort by duration, or type") + String sort; + + private volatile long pid; + + public CamelStartupRecorderAction(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doWatchCall() 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; + } + + this.pid = pids.get(0); + + // ensure output file is deleted before executing action + File outputFile = getOutputFile(Long.toString(pid)); + FileUtil.deleteFile(outputFile); + + JsonObject root = new JsonObject(); + root.put("action", "startup-recorder"); + File f = getActionFile(Long.toString(pid)); + try { + IOHelper.writeText(root.toJson(), f); + } catch (Exception e) { + // ignore + } + + JsonObject jo = waitForOutputFile(outputFile); + if (jo != null) { + JsonArray arr = (JsonArray) jo.get("steps"); + for (int i = 0; arr != null && i < arr.size(); i++) { + JsonObject o = (JsonObject) arr.get(i); + Row row = new Row(); + row.id = o.getInteger("id"); + row.parentId = o.getInteger("parentId"); + row.level = o.getInteger("level"); + row.name = o.getString("name"); + row.type = o.getString("type"); + row.description = o.getString("description"); + row.duration = o.getLong("duration"); + 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("DURATION").dataAlign(HorizontalAlign.RIGHT).with(this::getDuration), + new Column().header("TYPE").dataAlign(HorizontalAlign.LEFT).with(r -> r.type), + new Column().header("STEP (END)").dataAlign(HorizontalAlign.LEFT) + .maxWidth(80, OverflowBehaviour.ELLIPSIS_RIGHT) + .with(this::getStep)))); + } + + // delete output file after use + FileUtil.deleteFile(outputFile); + + return 0; + } + + private String getStep(Row r) { + String pad = StringHelper.padString(r.level); + String out = r.description; + if (r.name != null && !r.name.equals("null")) { + out = String.format("%s(%s)", r.description, r.name); + } + return pad + out; + } + + private String getDuration(Row r) { + if (r.duration > 0) { + return "" + r.duration; + } + return ""; + } + + protected int sortRow(Row o1, Row o2) { + String s = sort != null ? sort : ""; + int negate = 1; + if (s.startsWith("-")) { + s = s.substring(1); + negate = -1; + } + switch (s) { + case "duration": + return Long.compare(o1.duration, o2.duration) * negate; + case "type": + return o1.type.compareToIgnoreCase(o2.type) * 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; + } + + private static class Row { + int id; + int parentId; + int level; + String name; + String type; + String description; + long duration; + } + + public static class DurationTypeCompletionCandidates implements Iterable<String> { + + public DurationTypeCompletionCandidates() { + } + + @Override + public Iterator<String> iterator() { + return List.of("duration", "type").iterator(); + } + + } + +} diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java index 6bdbf41ceab..a7a4388faf8 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java @@ -82,6 +82,7 @@ import org.apache.camel.support.DefaultContextReloadStrategy; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.RouteOnDemandReloadStrategy; import org.apache.camel.support.service.ServiceHelper; +import org.apache.camel.support.startup.BacklogStartupStepRecorder; import org.apache.camel.tooling.maven.MavenGav; /** @@ -347,6 +348,8 @@ public class KameletMain extends MainCommandLineSupport { // do not build/init camel context yet DefaultCamelContext answer = new DefaultCamelContext(false); + // setup backlog recorder from very start + answer.getCamelContextExtension().setStartupStepRecorder(new BacklogStartupStepRecorder()); if (download) { ClassLoader dynamicCL = createApplicationContextClassLoader(answer); answer.setApplicationContextClassLoader(dynamicCL);