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 c0540a460cbd CAMEL-23719: camel-micrometer - when ProducerTemplate 
sends to a SEDA endpoint, Exchange.getFromRouteId() returns null (#23864)
c0540a460cbd is described below

commit c0540a460cbd9e7dc114437724dc59a750b78a68
Author: Fernando Matias Balieiro <[email protected]>
AuthorDate: Wed Jun 10 11:01:19 2026 +0100

    CAMEL-23719: camel-micrometer - when ProducerTemplate sends to a SEDA 
endpoint, Exchange.getFromRouteId() returns null (#23864)
    
    * CAMEL-23719: camel-micrometer - ensure Prometheus Event Notification when 
RouteId is set
    
    Ensure Prometheus Event Notification happens when a RouteId is configured 
in the destination Route of a message sent by a ProducerTemplate.
    
    * CAMEL-23719: camel-micrometer - changes after code review
    
    * CAMEL-23719: camel-micrometer - reuse exchange event timers and add 
Prometheus focused test
    
    Ensure MicrometerExchangeEventNotifier registers Exchange Event Timers with 
Tag Keys so Prometheus export works when Route triggered and ProducerTemplate 
Exchanges are mixed.
    
    - Reuse existing timers via getOrCreateTimer() for sent and completed 
events;
    - Add ProducerTemplate test with Prometheus scrape validation;
    - Add micrometer-registry-prometheus test dependency;
    - Document routeId tag behavior change in the 4.21 upgrade guide.
    
    Generated-by: Cursor Agent on behalf of @fernandobalieiro.
---
 components/camel-micrometer/pom.xml                |   5 +
 .../MicrometerExchangeEventNotifier.java           |  14 ++-
 ...rometerExchangeEventNotifierNamingStrategy.java |  24 ++---
 ...terExchangeEventNotifierNamingStrategyTest.java |  72 ++++++++++++++-
 ...rExchangeEventNotifierProducerTemplateTest.java | 101 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc    |   7 ++
 6 files changed, 203 insertions(+), 20 deletions(-)

diff --git a/components/camel-micrometer/pom.xml 
b/components/camel-micrometer/pom.xml
index 53b8e3780ea3..acd3673e8c2c 100644
--- a/components/camel-micrometer/pom.xml
+++ b/components/camel-micrometer/pom.xml
@@ -78,6 +78,11 @@
             <artifactId>camel-test-spring-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-registry-prometheus</artifactId>
+            <scope>test</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.mockito</groupId>
diff --git 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifier.java
 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifier.java
index e5806e015cc7..01939f952477 100644
--- 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifier.java
+++ 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifier.java
@@ -185,8 +185,7 @@ public class MicrometerExchangeEventNotifier extends 
AbstractMicrometerEventNoti
     protected void handleSentEvent(ExchangeSentEvent sentEvent) {
         String name = getNamingStrategy().getName(sentEvent.getExchange(), 
sentEvent.getEndpoint());
         Tags tags = getNamingStrategy().getTags(sentEvent, 
sentEvent.getEndpoint());
-        Timer timer = Timer.builder(name).tags(tags).description("Time taken 
to send message to the endpoint")
-                .register(getMeterRegistry());
+        Timer timer = getOrCreateTimer(name, tags, "Time taken to send message 
to the endpoint");
         timer.record(sentEvent.getTimeTaken(), TimeUnit.MILLISECONDS);
     }
 
@@ -201,11 +200,20 @@ public class MicrometerExchangeEventNotifier extends 
AbstractMicrometerEventNoti
         // Would have preferred LongTaskTimer, but you cannot set the 
FAILED_TAG once it is registered
         Timer.Sample sample = (Timer.Sample) 
doneEvent.getExchange().removeProperty("eventTimer:" + name);
         if (sample != null) {
-            sample.stop(getMeterRegistry().timer(name, tags));
+            Timer timer = getOrCreateTimer(name, tags, "Time taken for 
exchange processing");
+            sample.stop(timer);
         }
         setLastTimeExchange();
     }
 
+    private Timer getOrCreateTimer(final String name, final Tags tags, final 
String description) {
+        Timer timer = getMeterRegistry().find(name).tags(tags).timer();
+        if (timer == null) {
+            timer = 
Timer.builder(name).tags(tags).description(description).register(getMeterRegistry());
+        }
+        return timer;
+    }
+
     private void setLastTimeExchange() {
         Gauge meter = 
getMeterRegistry().find(MicrometerConstants.CAMEL_EXCHANGE_LAST_TIME_METER_NAME).gauge();
         if (meter == null) {
diff --git 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategy.java
 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategy.java
index 10d8ba415d78..5e32f4829d25 100644
--- 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategy.java
+++ 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategy.java
@@ -72,23 +72,15 @@ public interface 
MicrometerExchangeEventNotifierNamingStrategy {
                 uri = StringHelper.before(uri, "?", uri);
             }
         }
+
         String routeId = event.getExchange().getFromRouteId();
-        if (routeId != null) {
-            return Tags.of(
-                    CAMEL_CONTEXT_TAG, 
event.getExchange().getContext().getName(),
-                    KIND, KIND_EXCHANGE,
-                    EVENT_TYPE_TAG, event.getClass().getSimpleName(),
-                    ROUTE_ID_TAG, routeId,
-                    ENDPOINT_NAME, uri,
-                    FAILED_TAG, 
Boolean.toString(event.getExchange().isFailed()));
-        } else {
-            return Tags.of(
-                    CAMEL_CONTEXT_TAG, 
event.getExchange().getContext().getName(),
-                    KIND, KIND_EXCHANGE,
-                    EVENT_TYPE_TAG, event.getClass().getSimpleName(),
-                    ENDPOINT_NAME, uri,
-                    FAILED_TAG, 
Boolean.toString(event.getExchange().isFailed()));
-        }
+        return Tags.of(
+                CAMEL_CONTEXT_TAG, event.getExchange().getContext().getName(),
+                KIND, KIND_EXCHANGE,
+                EVENT_TYPE_TAG, event.getClass().getSimpleName(),
+                ROUTE_ID_TAG, routeId != null ? routeId : "",
+                ENDPOINT_NAME, uri,
+                FAILED_TAG, Boolean.toString(event.getExchange().isFailed()));
     }
 
     default Tags getInflightExchangesTags(CamelContext camelContext, String 
routeId) {
diff --git 
a/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategyTest.java
 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategyTest.java
index c1b1451c675e..532b4b4721d0 100644
--- 
a/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategyTest.java
+++ 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierNamingStrategyTest.java
@@ -16,13 +16,31 @@
  */
 package org.apache.camel.component.micrometer.eventnotifier;
 
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.event.ExchangeSentEvent;
 import org.junit.jupiter.api.Test;
 
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.CAMEL_CONTEXT_TAG;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.ENDPOINT_NAME;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.EVENT_TYPE_TAG;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.FAILED_TAG;
+import static org.apache.camel.component.micrometer.MicrometerConstants.KIND;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.KIND_EXCHANGE;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.ROUTE_ID_TAG;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 class MicrometerExchangeEventNotifierNamingStrategyTest {
 
+    private static final String CONTEXT_NAME = "testContext";
+    private static final String ENDPOINT_URI = "seda://in";
+
     @Test
     void testDefaultFormatName() {
         MicrometerExchangeEventNotifierNamingStrategy strategy = 
MicrometerExchangeEventNotifierNamingStrategy.DEFAULT;
@@ -57,4 +75,56 @@ class MicrometerExchangeEventNotifierNamingStrategyTest {
         assertEquals("CamelExchangesInflight", result);
     }
 
+    @Test
+    void getTagsWhenFromRouteIdIsNotNullShouldIncludeRouteIdTag() {
+        final var strategy = 
MicrometerExchangeEventNotifierNamingStrategy.DEFAULT;
+        final var exchange = mock(Exchange.class);
+        final var context = mock(CamelContext.class);
+        final var endpoint = mock(Endpoint.class);
+
+        when(exchange.getFromRouteId()).thenReturn("existingRoute");
+        when(exchange.getContext()).thenReturn(context);
+        when(exchange.isFailed()).thenReturn(false);
+        when(context.getName()).thenReturn(CONTEXT_NAME);
+        when(endpoint.toString()).thenReturn(ENDPOINT_URI);
+
+        final var event = new ExchangeSentEvent(exchange, endpoint, 10L);
+        final var tags = strategy.getTags(event, endpoint);
+
+        assertThat(tagValue(tags, ROUTE_ID_TAG)).isEqualTo("existingRoute");
+        assertThat(tagValue(tags, 
EVENT_TYPE_TAG)).isEqualTo("ExchangeSentEvent");
+    }
+
+    @Test
+    void getTagsWhenFromRouteIdIsNullShouldUseEmptyRouteIdTag() {
+        final var strategy = 
MicrometerExchangeEventNotifierNamingStrategy.DEFAULT;
+        final var exchange = mock(Exchange.class);
+        final var context = mock(CamelContext.class);
+        final var endpoint = mock(Endpoint.class);
+
+        when(exchange.getFromRouteId()).thenReturn(null);
+        when(exchange.getContext()).thenReturn(context);
+        when(exchange.isFailed()).thenReturn(true);
+        when(context.getName()).thenReturn(CONTEXT_NAME);
+        when(endpoint.toString()).thenReturn("mock://other");
+
+        final var event = new ExchangeSentEvent(exchange, endpoint, 10L);
+        final var tags = strategy.getTags(event, endpoint);
+
+        assertThat(tagValue(tags, ROUTE_ID_TAG)).isEmpty();
+        assertThat(tagValue(tags, CAMEL_CONTEXT_TAG)).isEqualTo(CONTEXT_NAME);
+        assertThat(tagValue(tags, KIND)).isEqualTo(KIND_EXCHANGE);
+        assertThat(tagValue(tags, 
EVENT_TYPE_TAG)).isEqualTo("ExchangeSentEvent");
+        assertThat(tagValue(tags, ENDPOINT_NAME)).isEqualTo("mock://other");
+        assertThat(tagValue(tags, FAILED_TAG)).isEqualTo("true");
+    }
+
+    private static String tagValue(final Tags tags, final String key) {
+        return tags.stream()
+                .filter(tag -> key.equals(tag.getKey()))
+                .map(Tag::getValue)
+                .findFirst()
+                .orElse(null);
+    }
+
 }
diff --git 
a/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierProducerTemplateTest.java
 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierProducerTemplateTest.java
new file mode 100644
index 000000000000..fd0bd3073101
--- /dev/null
+++ 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/eventnotifier/MicrometerExchangeEventNotifierProducerTemplateTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.component.micrometer.eventnotifier;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.core.instrument.util.HierarchicalNameMapper;
+import io.micrometer.jmx.JmxMeterRegistry;
+import io.micrometer.prometheusmetrics.PrometheusConfig;
+import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.micrometer.CamelJmxConfig;
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.DEFAULT_CAMEL_EXCHANGE_EVENT_METER_NAME;
+import static 
org.apache.camel.component.micrometer.MicrometerConstants.ROUTE_ID_TAG;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MicrometerExchangeEventNotifierProducerTemplateTest extends 
AbstractMicrometerEventNotifierTest {
+
+    private PrometheusMeterRegistry prometheusRegistry;
+
+    private static final String DIRECT_TRIGGER = "direct:trigger";
+    private static final String SEDA_WORKER = "seda:worker";
+    private static final String MOCK_RESULT = "mock:result";
+    private static final String PRODUCER_ROUTE_ID = "producer-route";
+    private static final String WORKER_ROUTE_ID = "worker-route";
+
+    @Override
+    protected AbstractMicrometerEventNotifier<?> getEventNotifier() {
+        return new MicrometerExchangeEventNotifier();
+    }
+
+    @Override
+    public void addRegistry() {
+        meterRegistry = new CompositeMeterRegistry();
+        meterRegistry.add(new SimpleMeterRegistry());
+        prometheusRegistry = new 
PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+        meterRegistry.add(prometheusRegistry);
+        meterRegistry.add(new JmxMeterRegistry(CamelJmxConfig.DEFAULT, 
Clock.SYSTEM, HierarchicalNameMapper.DEFAULT));
+    }
+
+    @Test
+    void 
notify_whenProducerTemplateAndRouteExchangesAreMixed_shouldRegisterTimersWithConsistentTagKeys()
 throws Exception {
+        final var mock = getMockEndpoint(MOCK_RESULT);
+        mock.expectedMessageCount(2);
+
+        template.sendBody(DIRECT_TRIGGER, "from-route");
+        template.sendBody(SEDA_WORKER, "from-producer-template");
+
+        mock.assertIsSatisfied();
+
+        final var exchangeEventTimers = meterRegistry.getMeters().stream()
+                .map(Meter::getId)
+                .filter(id -> 
DEFAULT_CAMEL_EXCHANGE_EVENT_METER_NAME.equals(id.getName()))
+                .toList();
+
+        final var tagKeySets = exchangeEventTimers.stream()
+                .map(id -> 
id.getTags().stream().map(Tag::getKey).sorted().toList())
+                .distinct()
+                .toList();
+
+        assertThat(exchangeEventTimers).isNotEmpty();
+        assertThat(tagKeySets).hasSize(1);
+        assertThat(exchangeEventTimers)
+                .allSatisfy(id -> 
assertThat(id.getTags().stream().map(Tag::getKey))
+                        .contains(ROUTE_ID_TAG));
+        assertThat(prometheusRegistry.scrape()).isNotBlank();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(DIRECT_TRIGGER).routeId(PRODUCER_ROUTE_ID)
+                        .to(SEDA_WORKER);
+                from(SEDA_WORKER).routeId(WORKER_ROUTE_ID)
+                        .to(MOCK_RESULT);
+            }
+        };
+    }
+
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index fd03a8e24d59..ec098f631004 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -18,6 +18,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 The library used for Camel Grok component has been migrated from no more 
maintained `io.krakens:java-grok` to its fork `io.github.whatap:java-grok`.
 It implies small differences listed 
https://github.com/whatap/java-grok#what-is-different-from-iokrakensjava-grok[here].
 
+=== camel-micrometer
+
+The `MicrometerExchangeEventNotifier` now always includes the `routeId` tag on 
exchange event metrics.
+When `fromRouteId` is not available (for example, on Exchanges created by 
`ProducerTemplate`), the tag value is an empty string instead of omitting the 
tag.
+This ensures Prometheus-compatible metric export when Route triggered and 
ProducerTemplate Exchanges are mixed.
+Dashboards or alerts that assumed the `routeId` label was absent must be 
updated to treat `routeId=""` accordingly.
+
 === camel-core
 
 ==== Simple language: internal builder classes reorganized

Reply via email to