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 60b9e750cfd9 CAMEL-24259: camel-tui - Add Internal Tasks tab
60b9e750cfd9 is described below
commit 60b9e750cfd914fe8695c2dbf69a9ea6e441ee7a
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 27 14:35:48 2026 +0200
CAMEL-24259: camel-tui - Add Internal Tasks tab
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../core/commands/process/ListInternalTask.java | 6 +-
.../jbang/core/commands/tui/IntegrationInfo.java | 1 +
.../jbang/core/commands/tui/InternalTaskInfo.java | 29 +++
.../jbang/core/commands/tui/InternalTasksTab.java | 241 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/StatusParser.java | 22 ++
.../dsl/jbang/core/commands/tui/TabRegistry.java | 6 +
.../dsl/jbang/core/commands/tui/TuiIcons.java | 1 +
.../jbang/core/commands/tui/TabRegistryTest.java | 6 +-
8 files changed, 306 insertions(+), 6 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
index 17c89c00c6cc..85eb4444b568 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTask.java
@@ -36,12 +36,12 @@ import org.apache.camel.util.json.Jsoner;
import picocli.CommandLine;
import picocli.CommandLine.Command;
-@Command(name = "internal-tasks",
+@Command(name = "internal-task",
description = "List internal tasks of Camel integrations",
sortOptions = false, showDefaultValues = true,
footer = {
"%nExamples:",
- " camel get internal-tasks",
- " camel get internal-tasks --watch" })
+ " camel get internal-task",
+ " camel get internal-task --watch" })
public class ListInternalTask extends ProcessWatchCommand {
@CommandLine.Parameters(description = "Name or pid of running Camel
integration", arity = "0..1")
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 2cb345b043d7..622952024ac7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -101,6 +101,7 @@ class IntegrationInfo {
final List<MicrometerMeterInfo> meters = new ArrayList<>();
final List<HttpEndpointInfo> httpEndpoints = new ArrayList<>();
final List<ConfigurationTab.ConfigProperty> configProperties = new
ArrayList<>();
+ final List<InternalTaskInfo> internalTasks = new ArrayList<>();
final List<DataSourceInfo> dataSources = new ArrayList<>();
final List<SqlTraceInfo> sqlTraceStatements = new ArrayList<>();
long sqlTraceTotal;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
new file mode 100644
index 000000000000..23ee67a0615d
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTaskInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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.tui;
+
+class InternalTaskInfo {
+ String name;
+ String status;
+ long attempts;
+ long delay;
+ long elapsed;
+ long firstTime;
+ long lastTime;
+ long nextTime;
+ String error;
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
new file mode 100644
index 000000000000..61c7b476c8f8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InternalTasksTab.java
@@ -0,0 +1,241 @@
+/*
+ * 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.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Span;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.table.Cell;
+import dev.tamboui.widgets.table.Row;
+import dev.tamboui.widgets.table.Table;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.*;
+
+class InternalTasksTab extends AbstractTableTab {
+
+ InternalTasksTab(MonitorContext ctx) {
+ super(ctx, "name", "status", "attempts", "elapsed");
+ }
+
+ @Override
+ protected int getRowCount() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ return info != null ? info.internalTasks.size() : 0;
+ }
+
+ @Override
+ protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
+ List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+ sorted.sort(this::sortTask);
+
+ List<Row> rows = new ArrayList<>();
+ for (InternalTaskInfo ti : sorted) {
+ Style statusStyle = statusStyle(ti.status);
+ String elapsed = TimeUtils.printAge(ti.elapsed);
+ String first = ti.firstTime > 0 ?
TimeUtils.printSince(ti.firstTime) : "";
+ String last = ti.lastTime > 0 ? TimeUtils.printSince(ti.lastTime,
true) : "";
+ String next = formatNext(ti.nextTime);
+ String error = ti.error != null ? ti.error : "";
+
+ rows.add(Row.from(
+ Cell.from(Span.styled(" " + (ti.name != null ? ti.name :
""), Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(Span.styled(ti.status != null ? ti.status : "",
statusStyle)),
+ rightCell(String.valueOf(ti.attempts), 8),
+ rightCell(String.valueOf(ti.delay), 8),
+ Cell.from(elapsed),
+ Cell.from(first),
+ Cell.from(last),
+ Cell.from(next),
+ Cell.from(Span.styled(error, error.isEmpty() ? Style.EMPTY
: Theme.error()))));
+ }
+
+ if (rows.isEmpty()) {
+ rows.add(emptyRow("No internal tasks", 9));
+ }
+
+ Table table = Table.builder()
+ .rows(rows)
+ .header(Row.from(
+ Cell.from(Span.styled(" " + sortLabel("NAME", "name"),
sortStyle("name"))),
+ Cell.from(Span.styled(sortLabel("STATUS", "status"),
sortStyle("status"))),
+ rightCell(sortLabel("ATTEMPTS", "attempts"), 8,
sortStyle("attempts")),
+ rightCell("DELAY", 8, Style.EMPTY.bold()),
+ Cell.from(Span.styled(sortLabel("ELAPSED", "elapsed"),
sortStyle("elapsed"))),
+ Cell.from(Span.styled("FIRST", Style.EMPTY.bold())),
+ Cell.from(Span.styled("LAST", Style.EMPTY.bold())),
+ Cell.from(Span.styled("NEXT", Style.EMPTY.bold())),
+ Cell.from(Span.styled("ERROR", Style.EMPTY.bold()))))
+ .widths(
+ Constraint.length(30),
+ Constraint.length(12),
+ Constraint.length(8),
+ Constraint.length(8),
+ Constraint.length(10),
+ Constraint.length(10),
+ Constraint.length(10),
+ Constraint.length(10),
+ Constraint.fill())
+
.block(Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
+ .title(" Internal Tasks ").build())
+ .build();
+
+ lastTableArea = area;
+ frame.renderStatefulWidget(table, area, tableState);
+ renderScrollbar(frame, sorted.size());
+ }
+
+ private int sortTask(InternalTaskInfo a, InternalTaskInfo b) {
+ int result = switch (sort) {
+ case "status" -> {
+ String sa = a.status != null ? a.status : "";
+ String sb = b.status != null ? b.status : "";
+ yield sa.compareToIgnoreCase(sb);
+ }
+ case "attempts" -> Long.compare(b.attempts, a.attempts);
+ case "elapsed" -> Long.compare(b.elapsed, a.elapsed);
+ default -> { // "name"
+ String na = a.name != null ? a.name : "";
+ String nb = b.name != null ? b.name : "";
+ yield na.compareToIgnoreCase(nb);
+ }
+ };
+ return sortReversed ? -result : result;
+ }
+
+ private static Style statusStyle(String status) {
+ if (status == null) {
+ return Style.EMPTY;
+ }
+ return switch (status) {
+ case "Active" -> Theme.success();
+ case "Completed" -> Theme.info();
+ case "Failed", "Exhausted" -> Theme.error();
+ default -> Style.EMPTY;
+ };
+ }
+
+ private static String formatNext(long nextTime) {
+ if (nextTime <= 0) {
+ return "";
+ }
+ long age = nextTime - System.currentTimeMillis();
+ return TimeUtils.printDuration(age, true);
+ }
+
+ @Override
+ public SelectionContext getSelectionContext() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null || info.internalTasks.isEmpty()) {
+ return null;
+ }
+ List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+ sorted.sort(this::sortTask);
+ List<String> items = sorted.stream().map(t -> t.name != null ? t.name
: "").toList();
+ Integer sel = tableState.selected();
+ return new SelectionContext("table", items, sel != null ? sel : -1,
items.size(), "Internal Tasks");
+ }
+
+ @Override
+ public String description() {
+ return "Background task registry (reconnections, retries)";
+ }
+
+ @Override
+ public String getHelpText() {
+ return """
+ # Internal Tasks
+
+ Internal tasks are background operations that Camel components
schedule
+ for retry and reconnection purposes. For example, when a JMS
or SMPP
+ connection drops, the component registers an internal task
that periodically
+ attempts to reconnect.
+
+ Tasks self-register when they start running and are removed
when they
+ complete, fail permanently, or exhaust their retry budget. The
table
+ shows only **currently active** tasks.
+
+ ## Table Columns
+
+ - **NAME** — Descriptive name of the task (e.g., connection
target or component)
+ - **STATUS** — Current state: `Active` (running), `Completed`
(finished successfully), `Failed` (gave up), `Exhausted` (retry budget spent)
+ - **ATTEMPTS** — Number of retry attempts made so far
+ - **DELAY** — Current delay between attempts in milliseconds
+ - **ELAPSED** — Total time since the task started
+ - **FIRST** — When the first attempt was made (ago)
+ - **LAST** — When the last attempt was made (ago)
+ - **NEXT** — Time until the next attempt
+ - **ERROR** — Last error message, if any
+
+ ## When Tasks Appear
+
+ You will typically see internal tasks when:
+ - A messaging broker connection is lost and the consumer is
reconnecting
+ - A leader election is in progress (e.g., camel-master)
+ - A component is retrying a failed initialization
+
+ An empty table means all connections and background operations
are healthy.
+
+ ## Keys
+
+ - `Up/Down` — select task
+ - `s` — cycle sort column
+ - `S` — reverse sort order
+ """;
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Internal Tasks");
+ JsonArray rows = new JsonArray();
+ List<InternalTaskInfo> sorted = new ArrayList<>(info.internalTasks);
+ sorted.sort(this::sortTask);
+ for (InternalTaskInfo ti : sorted) {
+ JsonObject row = new JsonObject();
+ row.put("name", ti.name);
+ row.put("status", ti.status);
+ row.put("attempts", ti.attempts);
+ row.put("delay", ti.delay);
+ row.put("elapsed", ti.elapsed);
+ row.put("firstTime", ti.firstTime);
+ row.put("lastTime", ti.lastTime);
+ row.put("nextTime", ti.nextTime);
+ row.put("error", ti.error);
+ rows.add(row);
+ }
+ result.put("rows", rows);
+ result.put("totalRows", info.internalTasks.size());
+ Integer sel = tableState.selected();
+ result.put("selectedIndex", sel != null ? sel : -1);
+ return result;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
index e7a725f2049a..37ce90e54c47 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusParser.java
@@ -460,6 +460,28 @@ final class StatusParser {
}
}
+ // Parse internal tasks
+ JsonObject internalTasksObj = (JsonObject) root.get("internal-tasks");
+ if (internalTasksObj != null) {
+ JsonArray taskArr = (JsonArray) internalTasksObj.get("tasks");
+ if (taskArr != null) {
+ for (Object t : taskArr) {
+ JsonObject tj = (JsonObject) t;
+ InternalTaskInfo ti = new InternalTaskInfo();
+ ti.name = tj.getString("name");
+ ti.status = tj.getString("status");
+ ti.attempts = tj.getLongOrDefault("attempts", 0);
+ ti.delay = tj.getLongOrDefault("delay", 0);
+ ti.elapsed = tj.getLongOrDefault("elapsed", 0);
+ ti.firstTime = tj.getLongOrDefault("firstTime", 0);
+ ti.lastTime = tj.getLongOrDefault("lastTime", 0);
+ ti.nextTime = tj.getLongOrDefault("nextTime", 0);
+ ti.error = tj.getString("error");
+ info.internalTasks.add(ti);
+ }
+ }
+ }
+
// Parse circuit breakers: resilience4j, fault-tolerance, core
parseCbSection(root, "resilience4j", info);
parseCbSection(root, "fault-tolerance", info);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 3d667e06667a..803f174b7f97 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -103,6 +103,7 @@ class TabRegistry {
private KafkaTab kafkaTab;
private DataSourceTab dataSourceTab;
private SqlQueryTab sqlQueryTab;
+ private InternalTasksTab internalTasksTab;
private SqlTraceTab sqlTraceTab;
private MonitorTab activeMoreTab;
@@ -129,6 +130,7 @@ class TabRegistry {
heapHistogramTab = new HeapHistogramTab(ctx);
memoryLeakTab = new MemoryLeakTab(ctx);
sqlQueryTab = new SqlQueryTab(ctx);
+ internalTasksTab = new InternalTasksTab(ctx);
sqlTraceTab = new SqlTraceTab(ctx);
endpointsTab = new EndpointsTab(ctx, dataService.metrics());
networkTab = new NetworkTab(ctx, dataService.metrics());
@@ -195,6 +197,10 @@ class TabRegistry {
TuiIcons.TAB_NETWORK, "Network Services", "&Network
Services", networkTab, "Observability",
List.of(), info -> !info.services.isEmpty()),
new MoreTab(TuiIcons.TAB_EVENTS, "Events", "&Exchange Events",
eventTab, "Observability"),
+ new MoreTab(
+ TuiIcons.TAB_INTERNAL_TASKS, "Internal Tasks",
"In&ternal Tasks", internalTasksTab,
+ "Observability",
+ List.of(), info -> !info.internalTasks.isEmpty()),
new MoreTab(
TuiIcons.TAB_SPANS, "Spans", "&OTel Spans", spansTab,
"Observability",
List.of("opentelemetry")),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
index 32d95332f8e4..6c9c186ce06f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiIcons.java
@@ -150,6 +150,7 @@ final class TuiIcons {
static final String TAB_THREADS = "🧵";
static final String TAB_PRODUCERS = "📤";
static final String TAB_EVENTS = "📣";
+ static final String TAB_INTERNAL_TASKS = "🔩";
static final String TAB_ROUTE_CONTROLLER = "🚦";
/** Icons for {@link TabRegistry#TAB_OVERVIEW}..{@link
TabRegistry#TAB_MORE} (in order). */
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
index 6122563554f2..91ce742833c6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistryTest.java
@@ -82,7 +82,7 @@ class TabRegistryTest {
@Test
void moreTabsHasTwentySevenEntries() {
- assertEquals(28, registry.moreTabs().size());
+ assertEquals(29, registry.moreTabs().size());
}
@Test
@@ -108,8 +108,8 @@ class TabRegistryTest {
// MORE_SHORTCUTS array carried before the MoreTab refactor. A label
edit that repoints a key must fail here.
List<Character> shortcuts =
registry.moreTabs().stream().map(TabRegistry.MoreTab::shortcut).toList();
assertEquals(
- List.of('B', 'C', 'H', 'I', 'P', 'R', 'C', 'H', 'M', 'N', 'E',
'O', 'J', 'K', 'Q', 'Q', 'C', 'M', 'M', 'M',
- 'P', 'S', 'T', 'B', 'C', 'C', 'V', 'M'),
+ List.of('B', 'C', 'H', 'I', 'P', 'R', 'C', 'H', 'M', 'N', 'E',
'T', 'O', 'J', 'K', 'Q', 'Q', 'C', 'M', 'M',
+ 'M', 'P', 'S', 'T', 'B', 'C', 'C', 'V', 'M'),
shortcuts, "More tab shortcut letters must match the
historical sequence");
}