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