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 650372c7b1de CAMEL-24003: Add activity queue to BacklogTracer with 
EventNotifier
650372c7b1de is described below

commit 650372c7b1de64868e1076575116ba8a1c4f2c3e
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 11 23:54:47 2026 +0200

    CAMEL-24003: Add activity queue to BacklogTracer with EventNotifier
    
    Adds a rolling window of recent exchange lifecycle activity to BacklogTracer
    for live monitoring dashboards. Uses EventNotifier to capture exchange 
created,
    sent, completed, and failed events. Introduces BacklogTracerActivityMessage 
as
    a lightweight API (no body/headers) with EndpointSend entries tracking which
    remote endpoints each exchange called. Bounded queue (default 100) with
    ConcurrentHashMap-based in-flight tracking and 5-minute stale eviction.
    
    Closes #24598
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles.properties   |   1 +
 .../camel/catalog/dev-consoles/activity.json       |  15 ++
 .../main/camel-main-configuration-metadata.json    |   1 +
 .../java/org/apache/camel/spi/BacklogTracer.java   |  43 ++++
 .../camel/spi/BacklogTracerActivityMessage.java    | 103 ++++++++++
 .../apache/camel/impl/debugger/BacklogTracer.java  | 218 +++++++++++++++++++++
 .../DefaultBacklogTracerActivityMessage.java       | 139 +++++++++++++
 .../apache/camel/impl/engine/DefaultChannel.java   |   5 +
 .../org/apache/camel/dev-console/activity.json     |  15 ++
 .../services/org/apache/camel/dev-console/activity |   2 +
 .../org/apache/camel/dev-consoles.properties       |   2 +-
 .../camel/impl/console/ActivityDevConsole.java     | 108 ++++++++++
 .../TracerConfigurationPropertiesConfigurer.java   |   7 +
 .../camel-main-configuration-metadata.json         |   1 +
 core/camel-main/src/main/docs/main.adoc            |   3 +-
 .../org/apache/camel/main/BaseMainSupport.java     |   1 +
 .../camel/main/TracerConfigurationProperties.java  |  23 +++
 .../mbean/ManagedBacklogTracerMBean.java           |   9 +
 .../management/mbean/ManagedBacklogTracer.java     |  15 ++
 .../management/BacklogTracerActivityTest.java      | 173 ++++++++++++++++
 20 files changed, 882 insertions(+), 2 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index 92fda01cffe3..ade3587e222b 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -1,3 +1,4 @@
+activity
 aws-secrets
 aws2-s3
 azure-secrets
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/activity.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/activity.json
new file mode 100644
index 000000000000..1c8f94954a0e
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/activity.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "activity",
+    "title": "Camel Activity",
+    "description": "Recent completed exchange activity",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.ActivityDevConsole",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-console",
+    "version": "4.22.0-SNAPSHOT"
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 034183e3dda7..9c5e7a7718e5 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -483,6 +483,7 @@
     { "name": "camel.threadpool.poolSize", "required": false, "description": 
"Sets the default core pool size (threads to keep minimum in pool)", 
"sourceType": "org.apache.camel.main.ThreadPoolConfigurationProperties", 
"type": "integer", "javaType": "java.lang.Integer", "secret": false },
     { "name": "camel.threadpool.rejectedPolicy", "required": false, 
"description": "Sets the default handler for tasks which cannot be executed by 
the thread pool.", "sourceType": 
"org.apache.camel.main.ThreadPoolConfigurationProperties", "type": "enum", 
"javaType": "org.apache.camel.util.concurrent.ThreadPoolRejectedPolicy", 
"secret": false, "enum": [ "Abort", "CallerRuns", "DiscardOldest", "Discard" ] 
},
     { "name": "camel.threadpool.timeUnit", "required": false, "description": 
"Sets the default time unit used for keep alive time", "sourceType": 
"org.apache.camel.main.ThreadPoolConfigurationProperties", "type": "object", 
"javaType": "java.util.concurrent.TimeUnit", "secret": false },
+    { "name": "camel.trace.activitySize", "required": false, "description": 
"Defines how many completed exchange summaries to keep in the activity queue 
(should be between 1 - 1000). The activity queue captures lightweight metadata 
(no body or headers) each time an exchange completes a route, providing a 
rolling window of recent exchange activity.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "integer", 
"javaType": "int", "defaultValue": 100, "secret": false },
     { "name": "camel.trace.backlogSize", "required": false, "description": 
"Defines how many of the last messages to keep in the tracer (should be between 
1 - 1000).", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "integer", 
"javaType": "int", "defaultValue": 100, "secret": false },
     { "name": "camel.trace.bodyIncludeFiles", "required": false, 
"description": "Whether to include the message body of file based messages. The 
overhead is that the file content has to be read from the file.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": true, "secret": false },
     { "name": "camel.trace.bodyIncludeStreams", "required": false, 
"description": "Whether to include the message body of stream based messages. 
If enabled then beware the stream may not be re-readable later. See more about 
Stream Caching.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracer.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracer.java
index d56acdc2dde5..5b7563ae8247 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracer.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracer.java
@@ -79,6 +79,24 @@ public interface BacklogTracer {
      */
     void setBacklogSize(int backlogSize);
 
+    /**
+     * Number of completed exchange summaries to keep in the activity queue. 
Default is 100.
+     * <p>
+     * The activity queue captures a lightweight summary (metadata only, no 
body or headers) each time an exchange
+     * completes a route. This provides a rolling window of recent exchange 
activity suitable for live monitoring.
+     *
+     * @since 4.22
+     */
+    int getActivitySize();
+
+    /**
+     * Number of completed exchange summaries to keep in the activity queue 
(should be between 1 - 1000). Default is
+     * 100.
+     *
+     * @since 4.22
+     */
+    void setActivitySize(int activitySize);
+
     /**
      * Remove the currently traced messages when dump methods are invoked
      */
@@ -237,6 +255,31 @@ public interface BacklogTracer {
      */
     Collection<BacklogTracerEventMessage> getLatestMessageHistory();
 
+    /**
+     * Get all activity summaries (without removing).
+     * <p>
+     * Activity summaries are lightweight snapshots captured when an exchange 
completes. They contain exchange-level
+     * metadata (exchange ID, route, elapsed time, status) and a list of 
remote endpoints that were called — no message
+     * body or headers.
+     *
+     * @since 4.22
+     */
+    Collection<BacklogTracerActivityMessage> getActivity();
+
+    /**
+     * Dumps all activity summaries.
+     *
+     * @since 4.22
+     */
+    List<BacklogTracerActivityMessage> dumpActivity();
+
+    /**
+     * Dumps all activity summaries as JSon.
+     *
+     * @since 4.22
+     */
+    String dumpActivityAsJSon();
+
     /**
      * Dumps all tracing data
      */
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerActivityMessage.java
 
b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerActivityMessage.java
new file mode 100644
index 000000000000..94f6a7a72453
--- /dev/null
+++ 
b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogTracerActivityMessage.java
@@ -0,0 +1,103 @@
+/*
+ * 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.spi;
+
+import java.util.List;
+
+/**
+ * Lightweight summary of a completed exchange captured by the {@link 
BacklogTracer} activity queue.
+ * <p>
+ * Unlike {@link BacklogTracerEventMessage} which captures full message 
snapshots (body, headers, properties) at each
+ * route node, activity messages contain only exchange-level metadata and a 
list of remote endpoints that were called
+ * during the exchange lifecycle. This makes them suitable for live monitoring 
dashboards without the overhead of full
+ * tracing.
+ *
+ * @since 4.22
+ */
+public interface BacklogTracerActivityMessage {
+
+    /**
+     * Unique trace counter for this activity entry.
+     */
+    long getUid();
+
+    /**
+     * Timestamp when the exchange was created.
+     */
+    long getTimestamp();
+
+    /**
+     * The exchange id.
+     */
+    String getExchangeId();
+
+    /**
+     * The route id where the exchange originated.
+     */
+    String getRouteId();
+
+    /**
+     * The endpoint URI of the route consumer (the "from" endpoint).
+     */
+    String getFromEndpointUri();
+
+    /**
+     * Total elapsed time in milliseconds for the exchange.
+     */
+    long getElapsed();
+
+    /**
+     * Whether the exchange failed.
+     */
+    boolean isFailed();
+
+    /**
+     * The exception message if the exchange failed, or null if successful.
+     */
+    String getExceptionMessage();
+
+    /**
+     * List of remote endpoints that were called during the exchange lifecycle.
+     * <p>
+     * Only remote endpoints are captured (endpoints where {@link 
org.apache.camel.Endpoint#isRemote()} returns true).
+     * Internal endpoints like direct, seda, timer, and log are excluded.
+     */
+    List<EndpointSend> getEndpointSends();
+
+    /**
+     * A single remote endpoint call captured during the exchange lifecycle.
+     *
+     * @since 4.22
+     */
+    interface EndpointSend {
+
+        /**
+         * The endpoint URI that was called.
+         */
+        String getEndpointUri();
+
+        /**
+         * Whether this is a remote endpoint.
+         */
+        boolean isRemoteEndpoint();
+
+        /**
+         * Time taken in milliseconds for this endpoint call.
+         */
+        long getElapsed();
+    }
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
index 17ac2eb04972..29e77353857a 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/BacklogTracer.java
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -30,14 +31,19 @@ import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.NamedNode;
+import org.apache.camel.NonManagedService;
 import org.apache.camel.Predicate;
 import org.apache.camel.Route;
+import org.apache.camel.spi.BacklogTracerActivityMessage;
 import org.apache.camel.spi.BacklogTracerEventMessage;
+import org.apache.camel.spi.CamelEvent;
 import org.apache.camel.spi.Language;
 import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.LoggerHelper;
 import org.apache.camel.support.MessageHelper;
 import org.apache.camel.support.PatternHelper;
+import org.apache.camel.support.SimpleEventNotifierSupport;
+import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -66,6 +72,12 @@ public class BacklogTracer extends ServiceSupport implements 
org.apache.camel.sp
     // use tracer to capture additional information for capturing latest 
completed exchange message-history
     private final Queue<BacklogTracerEventMessage> provisionalHistoryQueue = 
new LinkedBlockingQueue<>(MAX_BACKLOG_SIZE);
     private final Queue<BacklogTracerEventMessage> completeHistoryQueue = new 
LinkedBlockingQueue<>(MAX_BACKLOG_SIZE + 1);
+    // rolling window of completed exchange activity for live monitoring
+    private final Queue<BacklogTracerActivityMessage> activityQueue = new 
LinkedBlockingQueue<>(MAX_BACKLOG_SIZE);
+    private final ConcurrentHashMap<String, 
DefaultBacklogTracerActivityMessage> inflightActivity = new 
ConcurrentHashMap<>();
+    private final ActivityEventNotifier activityEventNotifier = new 
ActivityEventNotifier();
+    private int activitySize = 100;
+    private static final long INFLIGHT_EVICTION_MILLIS = 5 * 60 * 1000;
     private volatile String lastCompletedBreadcrumbId;
     private boolean removeOnDump = true;
     private int bodyMaxChars = 32 * 1024;
@@ -260,6 +272,7 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
                 completeHistoryQueue.add(event);
             }
         }
+
         if (!enabled) {
             return;
         }
@@ -290,6 +303,15 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
         }
     }
 
+    private void drainActivity() {
+        int drain = activityQueue.size() - activitySize + 1;
+        if (drain > 0) {
+            for (int i = 0; i < drain; i++) {
+                activityQueue.poll();
+            }
+        }
+    }
+
     private boolean shouldTraceFilter(Exchange exchange) {
         return predicate.matches(exchange);
     }
@@ -331,6 +353,23 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
         this.backlogSize = backlogSize;
     }
 
+    @Override
+    public int getActivitySize() {
+        return activitySize;
+    }
+
+    @Override
+    public void setActivitySize(int activitySize) {
+        if (activitySize <= 0) {
+            throw new IllegalArgumentException("The activity size must be a 
positive number, was: " + activitySize);
+        }
+        if (activitySize > MAX_BACKLOG_SIZE) {
+            throw new IllegalArgumentException(
+                    "The activity size cannot be greater than the max size of 
" + MAX_BACKLOG_SIZE + ", was: " + activitySize);
+        }
+        this.activitySize = activitySize;
+    }
+
     @Override
     public boolean isRemoveOnDump() {
         return removeOnDump;
@@ -478,6 +517,62 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
         return Collections.unmodifiableCollection(completeHistoryQueue);
     }
 
+    @Override
+    public Collection<BacklogTracerActivityMessage> getActivity() {
+        return Collections.unmodifiableCollection(activityQueue);
+    }
+
+    @Override
+    public List<BacklogTracerActivityMessage> dumpActivity() {
+        List<BacklogTracerActivityMessage> answer = new 
ArrayList<>(activityQueue);
+        if (isRemoveOnDump()) {
+            activityQueue.clear();
+        }
+        return answer;
+    }
+
+    @Override
+    public String dumpActivityAsJSon() {
+        List<BacklogTracerActivityMessage> events = dumpActivity();
+
+        JsonObject root = new JsonObject();
+        JsonArray arr = new JsonArray();
+        root.put("activity", arr);
+        for (BacklogTracerActivityMessage event : events) {
+            JsonObject jo = new JsonObject();
+            jo.put("uid", event.getUid());
+            jo.put("exchangeId", event.getExchangeId());
+            if (event.getRouteId() != null) {
+                jo.put("routeId", event.getRouteId());
+            }
+            if (event.getFromEndpointUri() != null) {
+                jo.put("fromEndpointUri", event.getFromEndpointUri());
+            }
+            if (event.getTimestamp() > 0) {
+                jo.put("timestamp", event.getTimestamp());
+            }
+            jo.put("elapsed", event.getElapsed());
+            jo.put("failed", event.isFailed());
+            if (event.getExceptionMessage() != null) {
+                jo.put("exception", event.getExceptionMessage());
+            }
+            List<BacklogTracerActivityMessage.EndpointSend> sends = 
event.getEndpointSends();
+            if (sends != null && !sends.isEmpty()) {
+                JsonArray sa = new JsonArray();
+                for (BacklogTracerActivityMessage.EndpointSend send : sends) {
+                    JsonObject so = new JsonObject();
+                    so.put("endpointUri", send.getEndpointUri());
+                    so.put("remoteEndpoint", send.isRemoteEndpoint());
+                    so.put("elapsed", send.getElapsed());
+                    sa.add(so);
+                }
+                jo.put("endpointSends", sa);
+            }
+            arr.add(jo);
+        }
+        return root.toJson();
+    }
+
     public List<BacklogTracerEventMessage> dumpTracedMessages(String nodeId) {
         List<BacklogTracerEventMessage> answer = new ArrayList<>();
         if (nodeId != null) {
@@ -585,6 +680,8 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
         queue.clear();
         completeHistoryQueue.clear();
         provisionalHistoryQueue.clear();
+        activityQueue.clear();
+        inflightActivity.clear();
     }
 
     @Override
@@ -592,9 +689,130 @@ public class BacklogTracer extends ServiceSupport 
implements org.apache.camel.sp
         return traceCounter.incrementAndGet();
     }
 
+    @Override
+    protected void doStart() throws Exception {
+        
camelContext.getManagementStrategy().addEventNotifier(activityEventNotifier);
+        ServiceHelper.startService(activityEventNotifier);
+    }
+
     @Override
     protected void doStop() throws Exception {
+        ServiceHelper.stopService(activityEventNotifier);
+        
camelContext.getManagementStrategy().removeEventNotifier(activityEventNotifier);
+        inflightActivity.clear();
         clear();
     }
 
+    private void onExchangeCreated(CamelEvent.ExchangeCreatedEvent event) {
+        evictStaleInflight();
+
+        Exchange exchange = event.getExchange();
+        String exchangeId = exchange.getExchangeId();
+        long timestamp = event.getTimestamp() > 0 ? event.getTimestamp() : 
System.currentTimeMillis();
+
+        DefaultBacklogTracerActivityMessage activity = new 
DefaultBacklogTracerActivityMessage(
+                incrementTraceCounter(), timestamp, exchangeId);
+        inflightActivity.put(exchangeId, activity);
+    }
+
+    private void onExchangeSent(CamelEvent.ExchangeSentEvent event) {
+        Endpoint endpoint = event.getEndpoint();
+        if (!endpoint.isRemote()) {
+            return;
+        }
+
+        String exchangeId = event.getExchange().getExchangeId();
+        DefaultBacklogTracerActivityMessage activity = 
inflightActivity.get(exchangeId);
+        if (activity != null) {
+            activity.addEndpointSend(endpoint.getEndpointUri(), 
endpoint.isRemote(), event.getTimeTaken());
+        }
+    }
+
+    private void onExchangeCompleted(CamelEvent.ExchangeCompletedEvent event) {
+        Exchange exchange = event.getExchange();
+        String exchangeId = exchange.getExchangeId();
+        DefaultBacklogTracerActivityMessage activity = 
inflightActivity.remove(exchangeId);
+        if (activity != null) {
+            String routeId = exchange.getFromRouteId();
+            String fromEndpointUri = resolveFromEndpointUri(routeId);
+            activity.complete(routeId, fromEndpointUri, 
exchange.getClock().elapsed(), false, null);
+            drainActivity();
+            activityQueue.offer(activity);
+        }
+    }
+
+    private void onExchangeFailed(CamelEvent.ExchangeFailedEvent event) {
+        Exchange exchange = event.getExchange();
+        String exchangeId = exchange.getExchangeId();
+        DefaultBacklogTracerActivityMessage activity = 
inflightActivity.remove(exchangeId);
+        if (activity != null) {
+            String exMessage = null;
+            Exception cause = exchange.getException();
+            if (cause != null) {
+                exMessage = cause.getMessage();
+            }
+            String routeId = exchange.getFromRouteId();
+            String fromEndpointUri = resolveFromEndpointUri(routeId);
+            activity.complete(routeId, fromEndpointUri, 
exchange.getClock().elapsed(), true, exMessage);
+            drainActivity();
+            activityQueue.offer(activity);
+        }
+    }
+
+    private String resolveFromEndpointUri(String routeId) {
+        if (routeId != null) {
+            Route route = camelContext.getRoute(routeId);
+            if (route != null && route.getConsumer() != null) {
+                return route.getConsumer().getEndpoint().getEndpointUri();
+            }
+        }
+        return null;
+    }
+
+    private void evictStaleInflight() {
+        if (inflightActivity.isEmpty()) {
+            return;
+        }
+        long cutoff = System.currentTimeMillis() - INFLIGHT_EVICTION_MILLIS;
+        inflightActivity.entrySet().removeIf(e -> e.getValue().getTimestamp() 
< cutoff);
+    }
+
+    /**
+     * Inner EventNotifier that captures exchange lifecycle events for 
activity monitoring.
+     */
+    private class ActivityEventNotifier extends SimpleEventNotifierSupport 
implements NonManagedService {
+
+        ActivityEventNotifier() {
+            // enable the exchange events we need for activity tracking
+            setIgnoreExchangeEvents(false);
+            setIgnoreExchangeCreatedEvent(false);
+            setIgnoreExchangeCompletedEvent(false);
+            setIgnoreExchangeFailedEvents(false);
+            setIgnoreExchangeSentEvents(false);
+            // ignore all non-exchange events
+            setIgnoreCamelContextInitEvents(true);
+            setIgnoreCamelContextEvents(true);
+            setIgnoreRouteEvents(true);
+            setIgnoreServiceEvents(true);
+            setIgnoreStepEvents(true);
+        }
+
+        @Override
+        public boolean isDisabled() {
+            return !enabled && !standby;
+        }
+
+        @Override
+        public void notify(CamelEvent event) throws Exception {
+            if (event instanceof CamelEvent.ExchangeCreatedEvent ece) {
+                onExchangeCreated(ece);
+            } else if (event instanceof CamelEvent.ExchangeSentEvent ese) {
+                onExchangeSent(ese);
+            } else if (event instanceof CamelEvent.ExchangeCompletedEvent ece) 
{
+                onExchangeCompleted(ece);
+            } else if (event instanceof CamelEvent.ExchangeFailedEvent efe) {
+                onExchangeFailed(efe);
+            }
+        }
+    }
 }
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerActivityMessage.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerActivityMessage.java
new file mode 100644
index 000000000000..d50feac72b78
--- /dev/null
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultBacklogTracerActivityMessage.java
@@ -0,0 +1,139 @@
+/*
+ * 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.debugger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.camel.spi.BacklogTracerActivityMessage;
+
+/**
+ * Default implementation of {@link BacklogTracerActivityMessage}.
+ */
+public final class DefaultBacklogTracerActivityMessage implements 
BacklogTracerActivityMessage {
+
+    private final long uid;
+    private final long timestamp;
+    private final String exchangeId;
+    private String routeId;
+    private String fromEndpointUri;
+    private final List<DefaultEndpointSend> endpointSends = new ArrayList<>();
+    private long elapsed;
+    private boolean failed;
+    private String exceptionMessage;
+
+    public DefaultBacklogTracerActivityMessage(long uid, long timestamp, 
String exchangeId) {
+        this.uid = uid;
+        this.timestamp = timestamp;
+        this.exchangeId = exchangeId;
+    }
+
+    @Override
+    public long getUid() {
+        return uid;
+    }
+
+    @Override
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public String getExchangeId() {
+        return exchangeId;
+    }
+
+    @Override
+    public String getRouteId() {
+        return routeId;
+    }
+
+    @Override
+    public String getFromEndpointUri() {
+        return fromEndpointUri;
+    }
+
+    @Override
+    public long getElapsed() {
+        return elapsed;
+    }
+
+    @Override
+    public boolean isFailed() {
+        return failed;
+    }
+
+    @Override
+    public String getExceptionMessage() {
+        return exceptionMessage;
+    }
+
+    @Override
+    public List<EndpointSend> getEndpointSends() {
+        synchronized (endpointSends) {
+            return Collections.unmodifiableList(new 
ArrayList<>(endpointSends));
+        }
+    }
+
+    public void addEndpointSend(String endpointUri, boolean remoteEndpoint, 
long elapsed) {
+        synchronized (endpointSends) {
+            endpointSends.add(new DefaultEndpointSend(endpointUri, 
remoteEndpoint, elapsed));
+        }
+    }
+
+    public void complete(
+            String routeId, String fromEndpointUri,
+            long elapsed, boolean failed, String exceptionMessage) {
+        this.routeId = routeId;
+        this.fromEndpointUri = fromEndpointUri;
+        this.elapsed = elapsed;
+        this.failed = failed;
+        this.exceptionMessage = exceptionMessage;
+    }
+
+    /**
+     * Default implementation of {@link EndpointSend}.
+     */
+    public static final class DefaultEndpointSend implements EndpointSend {
+
+        private final String endpointUri;
+        private final boolean remoteEndpoint;
+        private final long elapsed;
+
+        public DefaultEndpointSend(String endpointUri, boolean remoteEndpoint, 
long elapsed) {
+            this.endpointUri = endpointUri;
+            this.remoteEndpoint = remoteEndpoint;
+            this.elapsed = elapsed;
+        }
+
+        @Override
+        public String getEndpointUri() {
+            return endpointUri;
+        }
+
+        @Override
+        public boolean isRemoteEndpoint() {
+            return remoteEndpoint;
+        }
+
+        @Override
+        public long getElapsed() {
+            return elapsed;
+        }
+    }
+}
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultChannel.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultChannel.java
index 432612f68c93..2efa27c39a68 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultChannel.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultChannel.java
@@ -312,6 +312,11 @@ public class DefaultChannel extends CamelInternalProcessor 
implements Channel {
             tracer.setTraceTemplates(camelContext.isBacklogTracingTemplates());
             tracer.setTraceRests(camelContext.isBacklogTracingRests());
             
camelContext.getCamelContextExtension().addContextPlugin(BacklogTracer.class, 
tracer);
+            try {
+                camelContext.addService(tracer);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
         }
         return tracer;
     }
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/activity.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/activity.json
new file mode 100644
index 000000000000..1c8f94954a0e
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/activity.json
@@ -0,0 +1,15 @@
+{
+  "console": {
+    "kind": "console",
+    "group": "camel",
+    "name": "activity",
+    "title": "Camel Activity",
+    "description": "Recent completed exchange activity",
+    "deprecated": false,
+    "javaType": "org.apache.camel.impl.console.ActivityDevConsole",
+    "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/dev-console/activity
 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/activity
new file mode 100644
index 000000000000..fe3ec9d28204
--- /dev/null
+++ 
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/activity
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.ActivityDevConsole
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 4f7ab2e10103..95209ff8cce9 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=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 transformers type- [...]
+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 [...]
 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/ActivityDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ActivityDevConsole.java
new file mode 100644
index 000000000000..4082259daafa
--- /dev/null
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/ActivityDevConsole.java
@@ -0,0 +1,108 @@
+/*
+ * 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.util.List;
+import java.util.Map;
+
+import org.apache.camel.spi.BacklogTracer;
+import org.apache.camel.spi.BacklogTracerActivityMessage;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "activity", displayName = "Camel Activity", description = 
"Recent completed exchange activity")
+public class ActivityDevConsole extends AbstractDevConsole {
+
+    public ActivityDevConsole() {
+        super("camel", "activity", "Camel Activity", "Recent completed 
exchange activity");
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        StringBuilder sb = new StringBuilder();
+
+        BacklogTracer tracer = 
getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class);
+        if (tracer != null) {
+            sb.append("Activity Size: 
").append(tracer.getActivitySize()).append("\n");
+            for (BacklogTracerActivityMessage event : tracer.getActivity()) {
+                sb.append(String.format("  %s | %s | %s | %dms | %s",
+                        event.getExchangeId(),
+                        event.getRouteId(),
+                        event.getFromEndpointUri() != null ? 
event.getFromEndpointUri() : "",
+                        event.getElapsed(),
+                        event.isFailed() ? "FAILED" : "OK"));
+                List<BacklogTracerActivityMessage.EndpointSend> sends = 
event.getEndpointSends();
+                if (sends != null && !sends.isEmpty()) {
+                    sb.append(" | sent: ").append(sends.size());
+                }
+                sb.append("\n");
+            }
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        JsonObject root = new JsonObject();
+
+        BacklogTracer tracer = 
getCamelContext().getCamelContextExtension().getContextPlugin(BacklogTracer.class);
+        if (tracer != null) {
+            root.put("activitySize", tracer.getActivitySize());
+
+            JsonArray arr = new JsonArray();
+            root.put("activity", arr);
+            for (BacklogTracerActivityMessage event : tracer.getActivity()) {
+                JsonObject jo = new JsonObject();
+                jo.put("uid", event.getUid());
+                jo.put("exchangeId", event.getExchangeId());
+                if (event.getRouteId() != null) {
+                    jo.put("routeId", event.getRouteId());
+                }
+                if (event.getFromEndpointUri() != null) {
+                    jo.put("fromEndpointUri", event.getFromEndpointUri());
+                }
+                if (event.getTimestamp() > 0) {
+                    jo.put("timestamp", event.getTimestamp());
+                }
+                jo.put("elapsed", event.getElapsed());
+                jo.put("failed", event.isFailed());
+                if (event.getExceptionMessage() != null) {
+                    jo.put("exception", event.getExceptionMessage());
+                }
+                List<BacklogTracerActivityMessage.EndpointSend> sends = 
event.getEndpointSends();
+                if (sends != null && !sends.isEmpty()) {
+                    JsonArray sa = new JsonArray();
+                    for (BacklogTracerActivityMessage.EndpointSend send : 
sends) {
+                        JsonObject so = new JsonObject();
+                        so.put("endpointUri", send.getEndpointUri());
+                        so.put("remoteEndpoint", send.isRemoteEndpoint());
+                        so.put("elapsed", send.getElapsed());
+                        sa.add(so);
+                    }
+                    jo.put("endpointSends", sa);
+                }
+                arr.add(jo);
+            }
+        }
+
+        return root;
+    }
+
+}
diff --git 
a/core/camel-main/src/generated/java/org/apache/camel/main/TracerConfigurationPropertiesConfigurer.java
 
b/core/camel-main/src/generated/java/org/apache/camel/main/TracerConfigurationPropertiesConfigurer.java
index 2c7cf5cafed8..811931a0aad8 100644
--- 
a/core/camel-main/src/generated/java/org/apache/camel/main/TracerConfigurationPropertiesConfigurer.java
+++ 
b/core/camel-main/src/generated/java/org/apache/camel/main/TracerConfigurationPropertiesConfigurer.java
@@ -22,6 +22,7 @@ public class TracerConfigurationPropertiesConfigurer extends 
org.apache.camel.su
     private static final Map<String, Object> ALL_OPTIONS;
     static {
         Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("ActivitySize", int.class);
         map.put("BacklogSize", int.class);
         map.put("BodyIncludeFiles", boolean.class);
         map.put("BodyIncludeStreams", boolean.class);
@@ -43,6 +44,8 @@ public class TracerConfigurationPropertiesConfigurer extends 
org.apache.camel.su
     public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
         org.apache.camel.main.TracerConfigurationProperties target = 
(org.apache.camel.main.TracerConfigurationProperties) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "activitysize":
+        case "activitySize": target.setActivitySize(property(camelContext, 
int.class, value)); return true;
         case "backlogsize":
         case "backlogSize": target.setBacklogSize(property(camelContext, 
int.class, value)); return true;
         case "bodyincludefiles":
@@ -81,6 +84,8 @@ public class TracerConfigurationPropertiesConfigurer extends 
org.apache.camel.su
     @Override
     public Class<?> getOptionType(String name, boolean ignoreCase) {
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "activitysize":
+        case "activitySize": return int.class;
         case "backlogsize":
         case "backlogSize": return int.class;
         case "bodyincludefiles":
@@ -115,6 +120,8 @@ public class TracerConfigurationPropertiesConfigurer 
extends org.apache.camel.su
     public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
         org.apache.camel.main.TracerConfigurationProperties target = 
(org.apache.camel.main.TracerConfigurationProperties) obj;
         switch (ignoreCase ? name.toLowerCase() : name) {
+        case "activitysize":
+        case "activitySize": return target.getActivitySize();
         case "backlogsize":
         case "backlogSize": return target.getBacklogSize();
         case "bodyincludefiles":
diff --git 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 034183e3dda7..9c5e7a7718e5 100644
--- 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++ 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -483,6 +483,7 @@
     { "name": "camel.threadpool.poolSize", "required": false, "description": 
"Sets the default core pool size (threads to keep minimum in pool)", 
"sourceType": "org.apache.camel.main.ThreadPoolConfigurationProperties", 
"type": "integer", "javaType": "java.lang.Integer", "secret": false },
     { "name": "camel.threadpool.rejectedPolicy", "required": false, 
"description": "Sets the default handler for tasks which cannot be executed by 
the thread pool.", "sourceType": 
"org.apache.camel.main.ThreadPoolConfigurationProperties", "type": "enum", 
"javaType": "org.apache.camel.util.concurrent.ThreadPoolRejectedPolicy", 
"secret": false, "enum": [ "Abort", "CallerRuns", "DiscardOldest", "Discard" ] 
},
     { "name": "camel.threadpool.timeUnit", "required": false, "description": 
"Sets the default time unit used for keep alive time", "sourceType": 
"org.apache.camel.main.ThreadPoolConfigurationProperties", "type": "object", 
"javaType": "java.util.concurrent.TimeUnit", "secret": false },
+    { "name": "camel.trace.activitySize", "required": false, "description": 
"Defines how many completed exchange summaries to keep in the activity queue 
(should be between 1 - 1000). The activity queue captures lightweight metadata 
(no body or headers) each time an exchange completes a route, providing a 
rolling window of recent exchange activity.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "integer", 
"javaType": "int", "defaultValue": 100, "secret": false },
     { "name": "camel.trace.backlogSize", "required": false, "description": 
"Defines how many of the last messages to keep in the tracer (should be between 
1 - 1000).", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "integer", 
"javaType": "int", "defaultValue": 100, "secret": false },
     { "name": "camel.trace.bodyIncludeFiles", "required": false, 
"description": "Whether to include the message body of file based messages. The 
overhead is that the file content has to be read from the file.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": true, "secret": false },
     { "name": "camel.trace.bodyIncludeStreams", "required": false, 
"description": "Whether to include the message body of stream based messages. 
If enabled then beware the stream may not be re-readable later. See more about 
Stream Caching.", "sourceType": 
"org.apache.camel.main.TracerConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc 
b/core/camel-main/src/main/docs/main.adoc
index 20d29d34c351..f19111d9f174 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -282,11 +282,12 @@ The camel.debug supports 15 options, which are listed 
below.
 
 
 === Camel Tracer configurations
-The camel.trace supports 14 options, which are listed below.
+The camel.trace supports 15 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
 | Name | Description | Default | Type
+| *camel.trace.activitySize* | Defines how many completed exchange summaries 
to keep in the activity queue (should be between 1 - 1000). The activity queue 
captures lightweight metadata (no body or headers) each time an exchange 
completes a route, providing a rolling window of recent exchange activity. | 
100 | int
 | *camel.trace.backlogSize* | Defines how many of the last messages to keep in 
the tracer (should be between 1 - 1000). | 100 | int
 | *camel.trace.bodyIncludeFiles* | Whether to include the message body of file 
based messages. The overhead is that the file content has to be read from the 
file. | true | boolean
 | *camel.trace.bodyIncludeStreams* | Whether to include the message body of 
stream based messages. If enabled then beware the stream may not be re-readable 
later. See more about Stream Caching. | false | boolean
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 b933427bf2c7..26ce5b2e0bb7 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
@@ -2503,6 +2503,7 @@ public abstract class BaseMainSupport extends BaseService 
{
         tracer.setEnabled(config.isEnabled());
         tracer.setStandby(config.isStandby());
         tracer.setBacklogSize(config.getBacklogSize());
+        tracer.setActivitySize(config.getActivitySize());
         tracer.setRemoveOnDump(config.isRemoveOnDump());
         tracer.setBodyMaxChars(config.getBodyMaxChars());
         tracer.setBodyIncludeStreams(config.isBodyIncludeStreams());
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/TracerConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/TracerConfigurationProperties.java
index 753eb5f0f2c3..de5e6939c335 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/TracerConfigurationProperties.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/TracerConfigurationProperties.java
@@ -34,6 +34,8 @@ public class TracerConfigurationProperties implements 
BootstrapCloseable {
     private boolean standby;
     @Metadata(label = "advanced", defaultValue = "100")
     private int backlogSize = 100;
+    @Metadata(label = "advanced", defaultValue = "100")
+    private int activitySize = 100;
     @Metadata(label = "advanced", defaultValue = "true")
     private boolean removeOnDump = true;
     @Metadata(label = "advanced", defaultValue = "32768")
@@ -104,6 +106,19 @@ public class TracerConfigurationProperties implements 
BootstrapCloseable {
         this.backlogSize = backlogSize;
     }
 
+    public int getActivitySize() {
+        return activitySize;
+    }
+
+    /**
+     * Defines how many completed exchange summaries to keep in the activity 
queue (should be between 1 - 1000). The
+     * activity queue captures lightweight metadata (no body or headers) each 
time an exchange completes a route,
+     * providing a rolling window of recent exchange activity.
+     */
+    public void setActivitySize(int activitySize) {
+        this.activitySize = activitySize;
+    }
+
     public boolean isRemoveOnDump() {
         return removeOnDump;
     }
@@ -253,6 +268,14 @@ public class TracerConfigurationProperties implements 
BootstrapCloseable {
         return this;
     }
 
+    /**
+     * Defines how many completed exchange summaries to keep in the activity 
queue (should be between 1 - 1000).
+     */
+    public TracerConfigurationProperties withActivitySize(int activitySize) {
+        this.activitySize = activitySize;
+        return this;
+    }
+
     /**
      * Whether all traced messages should be removed when the tracer is 
dumping. By default, the messages are removed,
      * which means that dumping will not contain previous dumped messages.
diff --git 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedBacklogTracerMBean.java
 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedBacklogTracerMBean.java
index d254c9a27c6f..2e63926b8bd7 100644
--- 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedBacklogTracerMBean.java
+++ 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/ManagedBacklogTracerMBean.java
@@ -45,6 +45,12 @@ public interface ManagedBacklogTracerMBean {
     @ManagedAttribute(description = "Number of maximum traced messages in 
total to keep in the backlog (FIFO queue)")
     void setBacklogSize(int backlogSize);
 
+    @ManagedAttribute(description = "Number of completed exchange summaries to 
keep in the activity queue")
+    int getActivitySize();
+
+    @ManagedAttribute(description = "Number of completed exchange summaries to 
keep in the activity queue")
+    void setActivitySize(int activitySize);
+
     @ManagedAttribute(description = "Whether to remove traced message from 
backlog when dumping trace messages")
     boolean isRemoveOnDump();
 
@@ -135,6 +141,9 @@ public interface ManagedBacklogTracerMBean {
     @ManagedOperation(description = "Dumps latest completed exchange message 
history in JSon format")
     String dumpLatestMessageHistoryAsJSon();
 
+    @ManagedOperation(description = "Dumps completed exchange activity 
summaries in JSon format")
+    String dumpActivityAsJSon();
+
     @ManagedOperation(description = "Clears the backlog")
     void clear();
 
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBacklogTracer.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBacklogTracer.java
index 098d954ec52c..bd449b0d4134 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBacklogTracer.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedBacklogTracer.java
@@ -86,6 +86,16 @@ public class ManagedBacklogTracer implements 
ManagedBacklogTracerMBean {
         backlogTracer.setBacklogSize(backlogSize);
     }
 
+    @Override
+    public int getActivitySize() {
+        return backlogTracer.getActivitySize();
+    }
+
+    @Override
+    public void setActivitySize(int activitySize) {
+        backlogTracer.setActivitySize(activitySize);
+    }
+
     @Override
     public boolean isRemoveOnDump() {
         return backlogTracer.isRemoveOnDump();
@@ -236,6 +246,11 @@ public class ManagedBacklogTracer implements 
ManagedBacklogTracerMBean {
         return backlogTracer.dumpLatestMessageHistoryAsJSon();
     }
 
+    @Override
+    public String dumpActivityAsJSon() {
+        return backlogTracer.dumpActivityAsJSon();
+    }
+
     @Override
     public void clear() {
         backlogTracer.clear();
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerActivityTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerActivityTest.java
new file mode 100644
index 000000000000..b245b4470829
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogTracerActivityTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.management;
+
+import javax.management.Attribute;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisabledOnOs(OS.AIX)
+public class BacklogTracerActivityTest extends ManagementTestSupport {
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testActivityCapture() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on
+                = new ObjectName(
+                        "org.apache.camel:context=" + 
context.getManagementName() + ",type=tracer,name=BacklogTracer");
+        assertNotNull(on);
+        assertTrue(mbeanServer.isRegistered(on));
+
+        Integer activitySize = (Integer) mbeanServer.getAttribute(on, 
"ActivitySize");
+        assertEquals(100, activitySize.intValue());
+
+        getMockEndpoint("mock:foo").expectedMessageCount(2);
+        getMockEndpoint("mock:bar").expectedMessageCount(2);
+
+        template.sendBody("direct:start", "Hello World");
+        template.sendBody("direct:start", "Bye World");
+
+        assertMockEndpointsSatisfied();
+
+        String json = (String) mbeanServer.invoke(on, "dumpActivityAsJSon", 
null, null);
+        assertNotNull(json);
+        assertTrue(json.contains("activity"));
+        assertTrue(json.contains("exchangeId"));
+        assertTrue(json.contains("routeId"));
+        assertTrue(json.contains("elapsed"));
+        assertTrue(json.contains("\"failed\":false"));
+        assertTrue(json.contains("fromEndpointUri"));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testActivityOverflow() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on
+                = new ObjectName(
+                        "org.apache.camel:context=" + 
context.getManagementName() + ",type=tracer,name=BacklogTracer");
+        assertNotNull(on);
+        assertTrue(mbeanServer.isRegistered(on));
+
+        // set activity size to 5
+        mbeanServer.setAttribute(on, new Attribute("ActivitySize", 5));
+        Integer activitySize = (Integer) mbeanServer.getAttribute(on, 
"ActivitySize");
+        assertEquals(5, activitySize.intValue());
+
+        getMockEndpoint("mock:foo").expectedMessageCount(10);
+        getMockEndpoint("mock:bar").expectedMessageCount(10);
+
+        for (int i = 0; i < 10; i++) {
+            template.sendBody("direct:start", "Message " + i);
+        }
+
+        assertMockEndpointsSatisfied();
+
+        // with removeOnDump=true (default), dumpActivity returns and clears
+        // the activity queue should have been drained to at most 5 entries
+        String json = (String) mbeanServer.invoke(on, "dumpActivityAsJSon", 
null, null);
+        assertNotNull(json);
+
+        // count exchangeId occurrences to verify bounded size
+        int count = 0;
+        int idx = 0;
+        while ((idx = json.indexOf("\"exchangeId\"", idx)) != -1) {
+            count++;
+            idx++;
+        }
+        assertEquals(5, count, "Activity queue should be bounded to 
activitySize=5");
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testActivityClearOnDump() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on
+                = new ObjectName(
+                        "org.apache.camel:context=" + 
context.getManagementName() + ",type=tracer,name=BacklogTracer");
+        assertNotNull(on);
+        assertTrue(mbeanServer.isRegistered(on));
+
+        getMockEndpoint("mock:foo").expectedMessageCount(2);
+        getMockEndpoint("mock:bar").expectedMessageCount(2);
+
+        template.sendBody("direct:start", "Hello World");
+        template.sendBody("direct:start", "Bye World");
+
+        assertMockEndpointsSatisfied();
+
+        // first dump should have entries
+        String json1 = (String) mbeanServer.invoke(on, "dumpActivityAsJSon", 
null, null);
+        assertNotNull(json1);
+        assertTrue(json1.contains("exchangeId"));
+
+        // second dump should be empty (removeOnDump=true by default)
+        String json2 = (String) mbeanServer.invoke(on, "dumpActivityAsJSon", 
null, null);
+        assertNotNull(json2);
+        assertFalse(json2.contains("exchangeId"), "Activity should be cleared 
after dump");
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testActivityCapturesFailed() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on
+                = new ObjectName(
+                        "org.apache.camel:context=" + 
context.getManagementName() + ",type=tracer,name=BacklogTracer");
+        assertNotNull(on);
+        assertTrue(mbeanServer.isRegistered(on));
+
+        getMockEndpoint("mock:foo").expectedMessageCount(1);
+        getMockEndpoint("mock:bar").expectedMessageCount(1);
+
+        template.sendBody("direct:start", "Hello World");
+
+        assertMockEndpointsSatisfied();
+
+        String json = (String) mbeanServer.invoke(on, "dumpActivityAsJSon", 
null, null);
+        assertNotNull(json);
+        assertTrue(json.contains("\"failed\":false"));
+        assertTrue(json.contains("\"routeId\":\"route1\""));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                context.setUseBreadcrumb(false);
+                context.setBacklogTracing(true);
+
+                from("direct:start")
+                        .to("mock:foo").id("foo")
+                        .to("mock:bar").id("bar");
+            }
+        };
+    }
+
+}

Reply via email to