Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9b0c048
CAMEL-24003: Wire activity data into CLI connector
davsclaus Jul 11, 2026
0182328
CAMEL-24003: Add camel get activity CLI command
davsclaus Jul 11, 2026
a9f605b
CAMEL-24003: Add unit test for ListActivity CLI command
davsclaus Jul 11, 2026
3145a0f
CAMEL-24003: Add TUI Activity tab for live exchange monitoring
davsclaus Jul 11, 2026
41a5443
CAMEL-24003: Use HistoryTab-style show BHPV footer hints in ActivityTab
davsclaus Jul 11, 2026
1621290
chore: fix missing trailing space in show hint key for HistoryTab and…
davsclaus Jul 11, 2026
7a171c9
chore: improve TUI footer hints for Activity tab and detail scroll la…
davsclaus Jul 11, 2026
2d5186f
CAMEL-24003: Add F1 help text for Activity tab
davsclaus Jul 11, 2026
ac0f85e
CAMEL-24003: Enhance TUI Activity tab with stats panel, endpoint send…
davsclaus Jul 12, 2026
c00d38a
CAMEL-24003: Remove body/headers/properties/variables from Activity tab
davsclaus Jul 12, 2026
e2c58ea
CAMEL-24003: Add Detail title to Activity tab panel and shrink stats …
davsclaus Jul 12, 2026
e23aa7d
CAMEL-24003: Address review feedback - fix FQCNs and remove dead filt…
davsclaus Jul 12, 2026
cc805d9
CAMEL-24003: Widen Overview FAIL column to avoid truncating age
davsclaus Jul 12, 2026
e97544c
CAMEL-24003: Fix infra popup port dialog navigation and align cycling…
davsclaus Jul 12, 2026
92438c6
CAMEL-24003: Fix Errors tab detail panel layout, title, exception dup…
davsclaus Jul 12, 2026
aa5e6d5
CAMEL-24003: Add Detail title to History tab detail panels
davsclaus Jul 12, 2026
4774747
CAMEL-24003: Improve Errors tab message history with colored route ID…
davsclaus Jul 12, 2026
8286356
CAMEL-24003: Improve Activity summary with percentiles, error rate, t…
davsclaus Jul 13, 2026
fa39aa4
CAMEL-24003: Fix thread-safety in BacklogTracer message history queue
davsclaus Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public class BacklogTracer extends ServiceSupport implements org.apache.camel.sp
private final ActivityEventNotifier activityEventNotifier = new ActivityEventNotifier();
private int activitySize = 100;
private static final long INFLIGHT_EVICTION_MILLIS = 5 * 60 * 1000;
private final Object historyLock = new Object();
private volatile String lastCompletedBreadcrumbId;
private boolean removeOnDump = true;
private int bodyMaxChars = 32 * 1024;
Expand Down Expand Up @@ -218,59 +219,66 @@ public void traceEvent(BacklogTracerEventMessage event) {

// handle capturing events for last full completed exchange (aka replay)
if (camelContext.isMessageHistory()) {
var head = provisionalHistoryQueue.peek();
String bid = null;
String tid = null;
if (head != null) {
bid = head.getBreadcrumbId();
tid = head.getExchangeId();
}
// correlate by breadcrumb ID when available (links exchanges across broker boundaries)
// fallback to exchange ID / correlation ID matching when breadcrumb is not set
boolean match;
if (bid != null && event.getBreadcrumbId() != null) {
match = bid.equals(event.getBreadcrumbId());
} else {
match = tid == null || tid.equals(event.getExchangeId()) || tid.equals(event.getCorrelationExchangeId());
}
// check if this event continues a previously completed breadcrumb flow
// (e.g. a downstream route connected via Kafka/SEDA that starts after the originating route finished)
boolean appendMode = false;
if (head == null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
appendMode = true;
} else if (head != null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)
&& head.getBreadcrumbId() != null
&& head.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
appendMode = true;
}
if (match || appendMode) {
boolean added = provisionalHistoryQueue.offer(event);
boolean original = head != null && event.getRouteId() != null && event.getRouteId().equals(head.getRouteId());
if (event.isLast() && original) {
if (appendMode) {
// downstream route finished: merge into existing complete history
completeHistoryQueue.addAll(provisionalHistoryQueue);
if (!added) {
completeHistoryQueue.add(event);
}
} else {
// originating route finished: replace complete history
completeHistoryQueue.clear();
completeHistoryQueue.addAll(provisionalHistoryQueue);
if (!added) {
completeHistoryQueue.add(event);
// synchronized to prevent race conditions when multiple threads (e.g. timer thread
// and Kafka consumer threads) call traceEvent concurrently — the peek/offer/addAll/clear
// sequence must be atomic to avoid dropping events or corrupting the queue state
synchronized (historyLock) {
var head = provisionalHistoryQueue.peek();
String bid = null;
String tid = null;
if (head != null) {
bid = head.getBreadcrumbId();
tid = head.getExchangeId();
}
// correlate by breadcrumb ID when available (links exchanges across broker boundaries)
// fallback to exchange ID / correlation ID matching when breadcrumb is not set
boolean match;
if (bid != null && event.getBreadcrumbId() != null) {
match = bid.equals(event.getBreadcrumbId());
} else {
match = tid == null || tid.equals(event.getExchangeId())
|| tid.equals(event.getCorrelationExchangeId());
}
// check if this event continues a previously completed breadcrumb flow
// (e.g. a downstream route connected via Kafka/SEDA that starts after the originating route finished)
boolean appendMode = false;
if (head == null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
appendMode = true;
} else if (head != null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)
&& head.getBreadcrumbId() != null
&& head.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
appendMode = true;
}
if (match || appendMode) {
boolean added = provisionalHistoryQueue.offer(event);
boolean original
= head != null && event.getRouteId() != null && event.getRouteId().equals(head.getRouteId());
if (event.isLast() && original) {
if (appendMode) {
// downstream route finished: merge into existing complete history
completeHistoryQueue.addAll(provisionalHistoryQueue);
if (!added) {
completeHistoryQueue.add(event);
}
} else {
// originating route finished: replace complete history
completeHistoryQueue.clear();
completeHistoryQueue.addAll(provisionalHistoryQueue);
if (!added) {
completeHistoryQueue.add(event);
}
lastCompletedBreadcrumbId = event.getBreadcrumbId();
}
lastCompletedBreadcrumbId = event.getBreadcrumbId();
provisionalHistoryQueue.clear();
}
provisionalHistoryQueue.clear();
} else if (lastCompletedBreadcrumbId != null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
// late-arriving event from a downstream route (e.g. second branch of a multicast via Kafka/SEDA)
// that arrived after the provisional queue was claimed by a new exchange
completeHistoryQueue.add(event);
}
} else if (lastCompletedBreadcrumbId != null && event.getBreadcrumbId() != null
&& event.getBreadcrumbId().equals(lastCompletedBreadcrumbId)) {
// late-arriving event from a downstream route (e.g. second branch of a multicast via Kafka/SEDA)
// that arrived after the provisional queue was claimed by a new exchange
completeHistoryQueue.add(event);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
= camel get activity

Get recent completed exchange activity


== Usage

[source,bash]
----
camel get activity [options]
----



== Options

[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
| `--filter` | Filter activity by route ID | | String
| `--json` | Output in JSON Format | | boolean
| `--limit` | Filter activity by limiting to the given number of rows | | int
| `--short-uri` | List endpoint URI without query parameters (short) | | boolean
| `--sort` | Sort by pid, name, age, elapsed, or since | since | String
| `--watch` | Execute periodically and showing output fullscreen | | boolean
| `--wide-uri` | List endpoint URI in full details | | boolean
| `-h,--help` | Display the help and sub-commands | | boolean
|===


Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ camel get [options]
[cols="2,5",options="header"]
|===
| Subcommand | Description
| xref:jbang-commands/camel-jbang-get-activity.adoc[activity] | Get recent completed exchange activity
| xref:jbang-commands/camel-jbang-get-bean.adoc[bean] | List beans in a running Camel integration
| xref:jbang-commands/camel-jbang-get-blocked.adoc[blocked] | Get blocked messages of Camel integrations
| xref:jbang-commands/camel-jbang-get-circuit-breaker.adoc[circuit-breaker] | Get status of Circuit Breaker EIPs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
private File errorFile;
private File receiveFile;
private long receiveFilePos; // keep track of receive offset
private File activityFile;
private byte[] lastSource;
private ExpressionDefinition lastSourceExpression;

Expand Down Expand Up @@ -202,6 +203,7 @@ protected void doStart() throws Exception {
errorFile = createLockFile(lockFile.getName() + "-error.json");
debugFile = createLockFile(lockFile.getName() + "-debug.json");
receiveFile = createLockFile(lockFile.getName() + "-receive.json");
activityFile = createLockFile(lockFile.getName() + "-activity.json");
scheduledFuture = executor.scheduleWithFixedDelay(this::task, 0, delay, TimeUnit.MILLISECONDS);
LOG.info("Camel CLI connector enabled");
} else {
Expand Down Expand Up @@ -1693,6 +1695,20 @@ protected void traceTask() {
LOG.trace("Error updating receive file: {} due to: {}. This exception is ignored.",
receiveFile, e.getMessage(), e);
}
try {
DevConsole dc15 = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
.resolveById("activity");
if (dc15 != null) {
JsonObject json = (JsonObject) dc15.call(DevConsole.MediaType.JSON);
LOG.trace("Updating activity file: {}", activityFile);
String data = json.toJson() + System.lineSeparator();
IOHelper.writeText(data, activityFile);
}
} catch (Exception e) {
// ignore
LOG.trace("Error updating activity file: {} due to: {}. This exception is ignored.",
activityFile, e.getMessage(), e);
}
}

private JsonObject collectMemory() {
Expand Down Expand Up @@ -1853,6 +1869,9 @@ protected void doStop() throws Exception {
if (receiveFile != null) {
FileUtil.deleteFile(receiveFile);
}
if (activityFile != null) {
FileUtil.deleteFile(activityFile);
}
if (executor != null) {
camelContext.getExecutorServiceManager().shutdown(executor);
executor = null;
Expand Down
Loading