This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-24003-heap-dump in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8939193b1fe40f5660b4a6d06bf147379c7afa5d Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jul 13 16:34:57 2026 +0200 CAMEL-24003: Add heap dump command to CLI, TUI, and MCP Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../impl/console/HeapDumpDevConsoleConfigurer.java | 63 +++++++++++ .../org/apache/camel/dev-console/heap-dump.json | 15 +++ ...rg.apache.camel.impl.console.HeapDumpDevConsole | 2 + .../org/apache/camel/dev-console/heap-dump | 2 + .../org/apache/camel/dev-consoles.properties | 2 +- .../camel/impl/console/HeapDumpDevConsole.java | 88 ++++++++++++++++ .../jbang-commands/camel-jbang-cmd-heap-dump.adoc | 27 +++++ .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 + .../camel/cli/connector/LocalCliConnector.java | 23 ++++ .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../dsl/jbang/core/commands/CamelJBangMain.java | 1 + .../jbang/core/commands/action/CamelHeapDump.java | 116 +++++++++++++++++++++ .../dsl/jbang/core/commands/ai/ToolRegistry.java | 21 ++++ .../dsl/jbang/core/commands/mcp/RuntimeTools.java | 19 ++++ .../dsl/jbang/core/commands/tui/CamelMonitor.java | 1 + .../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 44 ++++++++ .../dsl/jbang/core/commands/tui/MemoryTab.java | 46 ++++++++ .../jbang/core/commands/tui/MonitorContext.java | 2 + 18 files changed, 473 insertions(+), 2 deletions(-) diff --git a/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.java b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.java new file mode 100644 index 000000000000..52e790256a81 --- /dev/null +++ b/core/camel-console/src/generated/java/org/apache/camel/impl/console/HeapDumpDevConsoleConfigurer.java @@ -0,0 +1,63 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.impl.console; + +import javax.annotation.processing.Generated; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.ExtendedPropertyConfigurerGetter; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.spi.ConfigurerStrategy; +import org.apache.camel.spi.GeneratedPropertyConfigurer; +import org.apache.camel.util.CaseInsensitiveMap; +import org.apache.camel.impl.console.HeapDumpDevConsole; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo") +@SuppressWarnings("unchecked") +public class HeapDumpDevConsoleConfigurer extends org.apache.camel.support.component.PropertyConfigurerSupport implements GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter { + + private static final Map<String, Object> ALL_OPTIONS; + static { + Map<String, Object> map = new CaseInsensitiveMap(); + map.put("CamelContext", org.apache.camel.CamelContext.class); + ALL_OPTIONS = map; + } + + @Override + public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { + org.apache.camel.impl.console.HeapDumpDevConsole target = (org.apache.camel.impl.console.HeapDumpDevConsole) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": target.setCamelContext(property(camelContext, org.apache.camel.CamelContext.class, value)); return true; + default: return false; + } + } + + @Override + public Map<String, Object> getAllOptions(Object target) { + return ALL_OPTIONS; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": return org.apache.camel.CamelContext.class; + default: return null; + } + } + + @Override + public Object getOptionValue(Object obj, String name, boolean ignoreCase) { + org.apache.camel.impl.console.HeapDumpDevConsole target = (org.apache.camel.impl.console.HeapDumpDevConsole) obj; + switch (ignoreCase ? name.toLowerCase() : name) { + case "camelcontext": + case "camelContext": return target.getCamelContext(); + default: return null; + } + } +} + diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-dump.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-dump.json new file mode 100644 index 000000000000..0118872af815 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/heap-dump.json @@ -0,0 +1,15 @@ +{ + "console": { + "kind": "console", + "group": "camel", + "name": "heap-dump", + "title": "Heap Dump", + "description": "Write a heap dump (.hprof) file for deep memory analysis", + "deprecated": false, + "javaType": "org.apache.camel.impl.console.HeapDumpDevConsole", + "groupId": "org.apache.camel", + "artifactId": "camel-console", + "version": "4.22.0-SNAPSHOT" + } +} + diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole new file mode 100644 index 000000000000..eb5ec24f36f2 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.impl.console.HeapDumpDevConsole @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.impl.console.HeapDumpDevConsoleConfigurer diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump new file mode 100644 index 000000000000..264d915a0335 --- /dev/null +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/heap-dump @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.impl.console.HeapDumpDevConsole diff --git a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties index 95209ff8cce9..9573a4157934 100644 --- a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties +++ b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties @@ -1,5 +1,5 @@ # Generated by camel build tools - do NOT edit this file! -dev-consoles=activity bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder system-properties thread top trace transform [...] +dev-consoles=activity bean blocked browse circuit-breaker consumer context datasource debug endpoint errors eval-language event gc health heap-dump heap-histogram inflight internal-tasks java-security jfr-memory-leak jvm log memory message-history processor producer properties receive reload rest rest-spec route route-controller route-dump route-group route-structure route-topology send service simple-language source sql-query sql-trace startup-recorder system-properties thread top trace [...] groupId=org.apache.camel artifactId=camel-console version=4.22.0-SNAPSHOT diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java new file mode 100644 index 000000000000..9b307b8ffb42 --- /dev/null +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/HeapDumpDevConsole.java @@ -0,0 +1,88 @@ +/* + * 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.impl.console; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +import com.sun.management.HotSpotDiagnosticMXBean; +import org.apache.camel.spi.Configurer; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.annotations.DevConsole; +import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.json.JsonObject; + +@DevConsole(name = "heap-dump", displayName = "Heap Dump", + description = "Write a heap dump (.hprof) file for deep memory analysis") +@Configurer(extended = true) +public class HeapDumpDevConsole extends AbstractDevConsole { + + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + @Metadata(label = "action", description = "File name for the heap dump (without .hprof extension)", + javaType = "java.lang.String") + public static final String NAME = "name"; + + @Metadata(label = "action", description = "Whether to dump only live objects (default true)", + defaultValue = "true", javaType = "java.lang.Boolean") + public static final String LIVE = "live"; + + public HeapDumpDevConsole() { + super("jvm", "heap-dump", "Heap Dump", "Write a heap dump (.hprof) file for deep memory analysis"); + } + + @Override + protected String doCallText(Map<String, Object> options) { + JsonObject json = doCallJson(options); + String error = json.getString("error"); + if (error != null) { + return "Heap dump failed: " + error; + } + return "Heap dump written to: " + json.getString("file") + " (" + json.getLong("size") + " bytes)"; + } + + @Override + protected JsonObject doCallJson(Map<String, Object> options) { + JsonObject root = new JsonObject(); + + String name = optionString(options, NAME); + if (name == null || name.isBlank()) { + name = "heap-dump-" + TIMESTAMP.format(LocalDateTime.now()); + } + if (!name.endsWith(".hprof")) { + name = name + ".hprof"; + } + + boolean live = optionBoolean(options, LIVE, true); + + try { + HotSpotDiagnosticMXBean bean = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class); + bean.dumpHeap(name, live); + + File file = new File(name); + root.put("file", file.getAbsolutePath()); + root.put("size", file.length()); + } catch (Exception e) { + root.put("error", e.getMessage()); + } + + return root; + } +} diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc new file mode 100644 index 000000000000..eb49ad8ae2b4 --- /dev/null +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-heap-dump.adoc @@ -0,0 +1,27 @@ + +// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE += camel cmd heap-dump + +Write a heap dump (.hprof) file for deep memory analysis + + +== Usage + +[source,bash] +---- +camel cmd heap-dump [options] +---- + + + +== Options + +[cols="2,5,1,2",options="header"] +|=== +| Option | Description | Default | Type +| `--dump-name` | File name for the heap dump (without .hprof extension) | | String +| `--live` | Whether to dump only live objects | true | boolean +| `-h,--help` | Display the help and sub-commands | | boolean +|=== + + diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc index 1ae76dc01fe4..e6edad675139 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc @@ -22,6 +22,7 @@ camel cmd [options] | xref:jbang-commands/camel-jbang-cmd-disable-processor.adoc[disable-processor] | Disable Camel processor | xref:jbang-commands/camel-jbang-cmd-enable-processor.adoc[enable-processor] | Enable Camel processor | xref:jbang-commands/camel-jbang-cmd-gc.adoc[gc] | Trigger Java Memory Garbage Collector +| xref:jbang-commands/camel-jbang-cmd-heap-dump.adoc[heap-dump] | Write a heap dump (.hprof) file for deep memory analysis | xref:jbang-commands/camel-jbang-cmd-heap-histogram.adoc[heap-histogram] | Display class-level heap memory usage in a running Camel integration | xref:jbang-commands/camel-jbang-cmd-load.adoc[load] | Loads new source files into an existing Camel | xref:jbang-commands/camel-jbang-cmd-logger.adoc[logger] | List or change logging levels 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 eee6b7a0c13d..4fe345bf8c0d 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 @@ -380,6 +380,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C doActionSqlQueryTask(root); } else if ("sql-update-row".equals(action)) { doActionSqlUpdateRowTask(root); + } else if ("heap-dump".equals(action)) { + doActionHeapDumpTask(root); } else if ("jfr-memory-leak".equals(action)) { doActionJfrMemoryLeakTask(root); } else if ("cli-debug".equals(action)) { @@ -855,6 +857,27 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C } } + private void doActionHeapDumpTask(JsonObject root) throws IOException { + DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) + .resolveById("heap-dump"); + if (dc != null) { + Map<String, Object> params = new HashMap<>(); + String name = root.getString("name"); + if (name != null) { + params.put("name", name); + } + String live = root.getString("live"); + if (live != null) { + params.put("live", live); + } + JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON, params); + LOG.trace("Updating output file: {}", outputFile); + IOHelper.writeText(json.toJson(), outputFile); + } else { + IOHelper.writeText("{}", outputFile); + } + } + private void doActionJfrMemoryLeakTask(JsonObject root) throws IOException { DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class) .resolveById("jfr-memory-leak"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index 092515439667..aad2e662ac5d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', or 'anthropic'", "javaType": "LlmClient.ApiType", "type" [...] { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...] 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 87014e76d6f5..5bfd9a8042b9 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 @@ -110,6 +110,7 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("disable-processor", new CommandLine(new CamelProcessorDisableAction(this))) .addSubcommand("enable-processor", new CommandLine(new CamelProcessorEnableAction(this))) .addSubcommand("gc", new CommandLine(new CamelGCAction(this))) + .addSubcommand("heap-dump", new CommandLine(new CamelHeapDump(this))) .addSubcommand("heap-histogram", new CommandLine(new CamelHeapHistogram(this))) .addSubcommand("memory-leak", new CommandLine(new CamelMemoryLeak(this))) .addSubcommand("load", new CommandLine(new CamelLoadAction(this))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java new file mode 100644 index 000000000000..b2974a04368e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java @@ -0,0 +1,116 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.util.json.JsonObject; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "heap-dump", description = "Write a heap dump (.hprof) file for deep memory analysis", sortOptions = false, + showDefaultValues = true, + footer = { + "%nExamples:", + " camel cmd heap-dump", + " camel cmd heap-dump --name=mydump", + " camel cmd heap-dump --name=mydump --live=false" }) +public class CamelHeapDump extends ActionBaseCommand { + + @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") + String name = "*"; + + @CommandLine.Option(names = { "--dump-name" }, + description = "File name for the heap dump (without .hprof extension)") + String dumpName; + + @CommandLine.Option(names = { "--live" }, + description = "Whether to dump only live objects", defaultValue = "true") + boolean live = true; + + public CamelHeapDump(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + List<Long> pids = findPids(name); + if (pids.isEmpty()) { + return 1; + } else if (pids.size() > 1) { + printer().println("Name or pid " + name + " matches " + pids.size() + + " running Camel integrations. Specify a name or PID that matches exactly one."); + return 1; + } + + long pid = pids.get(0); + + // ensure output file is deleted before executing action + Path outputFile = getOutputFile(Long.toString(pid)); + PathUtils.deleteFile(outputFile); + + JsonObject root = new JsonObject(); + root.put("action", "heap-dump"); + if (dumpName != null) { + root.put("name", dumpName); + } + root.put("live", Boolean.toString(live)); + + Path f = getActionFile(Long.toString(pid)); + try { + Files.writeString(f, root.toJson()); + } catch (Exception e) { + // ignore + } + + JsonObject jo = getJsonObject(outputFile, 60000); + if (jo != null) { + String error = jo.getString("error"); + if (error != null) { + printer().println("Heap dump failed: " + error); + return 1; + } + String file = jo.getString("file"); + long size = jo.getLongOrDefault("size", 0); + printer().printf("Heap dump written to: %s (%s)%n", file, formatSize(size)); + } else { + printer().println("Response from running Camel with PID " + pid + " not received within 60 seconds"); + return 1; + } + + // delete output file after use + PathUtils.deleteFile(outputFile); + + return 0; + } + + private static String formatSize(long bytes) { + if (bytes >= 1024 * 1024 * 1024) { + return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } else if (bytes >= 1024 * 1024) { + return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); + } else if (bytes >= 1024) { + return String.format("%.1f KB", bytes / 1024.0); + } else { + return bytes + " bytes"; + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java index 53aa43a55795..04825e196bdf 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java @@ -340,6 +340,27 @@ public final class ToolRegistry { + "Useful for diagnosing memory leaks and understanding which classes dominate heap usage.") .executor((ctx, args) -> ctx.executeAction("heap-histogram", null))); + register(tool("write_heap_dump", + "Write a heap dump (.hprof) file for deep memory analysis with tools like Eclipse MAT or VisualVM. " + + "Returns the absolute file path and size of the dump.") + .param("name", "string", + "File name for the heap dump (without .hprof extension). Defaults to heap-dump-<timestamp>", false) + .param("live", "string", + "Whether to dump only live objects (default true). Live dumps trigger a GC first", false) + .readOnly(false) + .executor((ctx, args) -> { + String name = args.get("name"); + String live = args.get("live"); + return ctx.executeAction("heap-dump", root -> { + if (name != null && !name.isBlank()) { + root.put("name", name); + } + if (live != null && !live.isBlank()) { + root.put("live", live); + } + }); + })); + register(tool("execute_sql", "Execute a SQL query against a DataSource in the running Camel application. " + "Returns structured JSON with columns, rows, and metadata for SELECT queries, " diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java index aa17b63a9980..d0affc50fc51 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java @@ -342,6 +342,25 @@ public class RuntimeTools { return delegateToRegistry("get_heap_histogram", nameOrPid, Map.of()); } + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = false, openWorldHint = false), + description = """ + Write a heap dump (.hprof) file from a running Camel integration for deep memory analysis \ + with tools like Eclipse MAT, VisualVM, or jhat. The dump is written to the process working directory.""") + public JsonObject camel_runtime_heap_dump( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "File name for the heap dump (without .hprof extension). Defaults to heap-dump-<timestamp>") String name, + @ToolArg(description = "Whether to dump only live objects (default true). Live dumps trigger a GC first") String live) { + RuntimeService.ProcessInfo p = runtimeService.findSingleProcess(nameOrPid); + return runtimeService.executeAction(p.pid(), "heap-dump", root -> { + if (name != null && !name.isBlank()) { + root.put("name", name); + } + if (live != null && !live.isBlank()) { + root.put("live", live); + } + }); + } + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = false, openWorldHint = false), description = """ Diagnose memory leaks in a running Camel integration using Java Flight Recorder (JFR). \ diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java index 2de53f6ac5cf..95d1e3ce11d6 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java @@ -267,6 +267,7 @@ public class CamelMonitor extends CamelCommand { actionsPopup.setContext(ctx); actionsPopup.setMonitorContext(ctx); actionsPopup.setNotificationCallback((msg, error) -> setNotification(msg, error)); + ctx.notificationCallback = (msg, error) -> setNotification(msg, error); actionsPopup.setResetStatsAction(this::resetStats); shellPanel.setContext(ctx); aiPanel.setContext(ctx); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java index 983fe07f3e10..44cab307a2fe 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java @@ -127,6 +127,10 @@ class MemoryLeakTab extends AbstractTab { @Override public boolean handleKeyEvent(KeyEvent ke) { + if (ke.isChar('h')) { + triggerHeapDump(); + return true; + } if (state == State.IDLE || state == State.HAS_RESULTS) { if (ke.isCharIgnoreCase('r')) { startRecording(); @@ -725,10 +729,12 @@ class MemoryLeakTab extends AbstractTab { hint(spans, "R", "record"); hint(spans, "d", "mode [" + modeLabel + "]"); hint(spans, "+/-", "duration [" + duration + "s]"); + hint(spans, "h", "heap dump"); } case RECORDING -> { hint(spans, "Esc", "back"); hint(spans, "X", "stop"); + hint(spans, "h", "heap dump"); } case HAS_RESULTS -> { hint(spans, "Esc", "back"); @@ -739,6 +745,7 @@ class MemoryLeakTab extends AbstractTab { hint(spans, "R", "new recording"); hint(spans, "d", "mode [" + modeLabel + "]"); hint(spans, "+/-", "duration [" + duration + "s]"); + hint(spans, "h", "heap dump"); hintLast(spans, "PgUp/Dn", "detail"); } default -> hintLast(spans, "Esc", "back"); @@ -1592,6 +1599,43 @@ class MemoryLeakTab extends AbstractTab { || method.startsWith("jdk.") || method.startsWith("sun.") || method.startsWith("com.sun.")); } + private void triggerHeapDump() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null) { + return; + } + notify("Writing heap dump...", false); + String pid = info.pid; + startDaemonThread("heap-dump-" + pid, () -> { + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + JsonObject root = new JsonObject(); + root.put("action", "heap-dump"); + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(root.toJson(), actionFile); + JsonObject jo = TuiHelper.pollJsonResponse(outputFile, 60000); + if (jo != null) { + String error = jo.getString("error"); + if (error != null) { + notify("Heap dump failed: " + error, true); + } else { + String file = jo.getString("file"); + long size = jo.getLongOrDefault("size", 0); + notify("Heap dump: " + file + " (" + TuiHelper.formatBytes(size) + ")", false); + } + } else { + notify("Heap dump: no response within 60s", true); + } + PathUtils.deleteFile(outputFile); + }); + } + + private void notify(String message, boolean error) { + if (ctx.notificationCallback != null) { + ctx.notificationCallback.accept(message, error); + } + } + private static void startDaemonThread(String name, Runnable task) { Thread t = new Thread(task); t.setDaemon(true); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java index 539e4789a2de..88baf2b7c76f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryTab.java @@ -64,6 +64,10 @@ class MemoryTab extends AbstractTab { triggerGC(); return true; } + if (ke.isChar('h')) { + triggerHeapDump(); + return true; + } return false; } @@ -437,6 +441,47 @@ class MemoryTab extends AbstractTab { public void renderFooter(List<Span> spans) { hint(spans, "Esc", "back"); hint(spans, "g", "gc"); + hint(spans, "h", "heap dump"); + } + + private void triggerHeapDump() { + IntegrationInfo info = ctx.findSelectedIntegration(); + if (info == null) { + return; + } + notify("Writing heap dump...", false); + String pid = info.pid; + Thread t = new Thread(() -> { + Path outputFile = ctx.getOutputFile(pid); + PathUtils.deleteFile(outputFile); + JsonObject root = new JsonObject(); + root.put("action", "heap-dump"); + Path actionFile = ctx.getActionFile(pid); + PathUtils.writeTextSafely(root.toJson(), actionFile); + JsonObject jo = TuiHelper.pollJsonResponse(outputFile, 60000); + if (jo != null) { + String error = jo.getString("error"); + if (error != null) { + notify("Heap dump failed: " + error, true); + } else { + String file = jo.getString("file"); + long size = jo.getLongOrDefault("size", 0); + notify("Heap dump: " + file + " (" + formatBytes(size) + ")", false); + } + } else { + notify("Heap dump: no response within 60s", true); + } + PathUtils.deleteFile(outputFile); + }); + t.setDaemon(true); + t.setName("heap-dump-" + pid); + t.start(); + } + + private void notify(String message, boolean error) { + if (ctx.notificationCallback != null) { + ctx.notificationCallback.accept(message, error); + } } private void triggerGC() { @@ -540,6 +585,7 @@ class MemoryTab extends AbstractTab { ## Keys - `g` — trigger garbage collection on the running integration (sends a GC request to the JVM — useful for testing if high usage is just uncollected garbage) + - `h` — write a heap dump (.hprof) file for deep analysis with tools like Eclipse MAT or VisualVM - `Esc` — back """; } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java index 995c9b1f0bcf..e298e7664358 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java @@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.nio.file.Path; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import dev.tamboui.tui.TuiRunner; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; @@ -39,6 +40,7 @@ class MonitorContext { int logPinPercent; boolean logPinVisible; boolean ratePerMinute; + BiConsumer<String, Boolean> notificationCallback; MonitorContext( AtomicReference<List<IntegrationInfo>> data,
