Skip to content

CAMEL-23719: camel-micrometer - when ProducerTemplate sends to a SEDA endpoint, Exchange.getFromRouteId() returns null#23864

Merged
davsclaus merged 3 commits into
apache:mainfrom
fernandobalieiro:bugfix/event-notifier-prometheus-registration-failure
Jun 10, 2026
Merged

CAMEL-23719: camel-micrometer - when ProducerTemplate sends to a SEDA endpoint, Exchange.getFromRouteId() returns null#23864
davsclaus merged 3 commits into
apache:mainfrom
fernandobalieiro:bugfix/event-notifier-prometheus-registration-failure

Conversation

@fernandobalieiro

Copy link
Copy Markdown
Contributor

Description

The goal of this PR is to ensure that the Prometheus Event Notification happens when a RouteId is configured in the destination Route of a message sent through a ProducerTemplate.

Current Scenario

When a project is configured to use quarkus-micrometer-registry-prometheus and a message is sent to a SEDA endpoint through ProducerTemplate, the following warning appears in the logs:

2026-06-08 14:32:45,577 WARN  [org.apache.camel.support.EventHelper] (Camel (camel-6) thread #21 - seda://producer) Error notifying event C99DFEF70A95789-0000000000000001 exchange completed took: 3ms. This exception will be ignored.: java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'camel_exchange_event_notifier_seconds' containing tag keys [camelContext, endpointName, eventType, failed, kind]. The meter you are attempting to register has keys [camelContext, endpointName, eventType, failed, kind, routeId].
        at io.micrometer.prometheus.PrometheusMeterRegistry.lambda$throwExceptionOnRegistrationFailure$19(PrometheusMeterRegistry.java:619)
        at io.micrometer.core.instrument.MeterRegistry.meterRegistrationFailed(MeterRegistry.java:1291)
        at io.micrometer.prometheus.PrometheusMeterRegistry.lambda$applyToCollector$18(PrometheusMeterRegistry.java:593)
        at java.base/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1956)
        at io.micrometer.prometheus.PrometheusMeterRegistry.applyToCollector(PrometheusMeterRegistry.java:579)
        at io.micrometer.prometheus.PrometheusMeterRegistry.newTimer(PrometheusMeterRegistry.java:328)
        at io.micrometer.core.instrument.MeterRegistry.lambda$timer$6(MeterRegistry.java:379)
        at io.micrometer.core.instrument.MeterRegistry.getOrCreateMeter(MeterRegistry.java:725)
        at io.micrometer.core.instrument.MeterRegistry.registerMeterIfNecessary(MeterRegistry.java:652)
        at io.micrometer.core.instrument.MeterRegistry.timer(MeterRegistry.java:377)
        at io.micrometer.core.instrument.Timer$Builder.register(Timer.java:471)
        at io.micrometer.core.instrument.Timer$Builder.register(Timer.java:465)
        at io.micrometer.core.instrument.composite.CompositeTimer.registerNewMeter(CompositeTimer.java:205)
        at io.micrometer.core.instrument.composite.CompositeTimer.registerNewMeter(CompositeTimer.java:35)
        at io.micrometer.core.instrument.composite.AbstractCompositeMeter.add(AbstractCompositeMeter.java:67)
        at java.base/java.lang.Iterable.forEach(Iterable.java:75)
        at java.base/java.util.Collections$SetFromMap.forEach(Collections.java:6060)
        at io.micrometer.core.instrument.composite.CompositeMeterRegistry.lambda$new$0(CompositeMeterRegistry.java:67)
        at io.micrometer.core.instrument.composite.CompositeMeterRegistry.lock(CompositeMeterRegistry.java:189)
        at io.micrometer.core.instrument.composite.CompositeMeterRegistry.lambda$new$1(CompositeMeterRegistry.java:67)
        at io.micrometer.core.instrument.MeterRegistry.getOrCreateMeter(MeterRegistry.java:735)
        at io.micrometer.core.instrument.MeterRegistry.registerMeterIfNecessary(MeterRegistry.java:652)
        at io.micrometer.core.instrument.MeterRegistry.timer(MeterRegistry.java:377)
        at io.micrometer.core.instrument.MeterRegistry.timer(MeterRegistry.java:534)
        at org.apache.camel.component.micrometer.eventnotifier.MicrometerExchangeEventNotifier.handleDoneEvent(MicrometerExchangeEventNotifier.java:204)
        at org.apache.camel.component.micrometer.eventnotifier.MicrometerExchangeEventNotifier.notify(MicrometerExchangeEventNotifier.java:179)
        at org.apache.camel.support.EventHelper.doNotifyEvent(EventHelper.java:1575)
        at org.apache.camel.support.EventHelper.notifyExchangeDone(EventHelper.java:783)
        at org.apache.camel.impl.engine.DefaultUnitOfWork.done(DefaultUnitOfWork.java:280)
        at org.apache.camel.support.UnitOfWorkHelper.doneUow(UnitOfWorkHelper.java:53)
        at org.apache.camel.impl.engine.CamelInternalProcessor$UnitOfWorkProcessorAdvice.after(CamelInternalProcessor.java:1178)
        at org.apache.camel.impl.engine.CamelInternalProcessor$UnitOfWorkProcessorAdvice.after(CamelInternalProcessor.java:1115)
        at org.apache.camel.impl.engine.AdviceIterator.runAfterTask(AdviceIterator.java:45)
        at org.apache.camel.impl.engine.AdviceIterator.runAfterTasks(AdviceIterator.java:39)
        at org.apache.camel.impl.engine.CamelInternalProcessor$AsyncAfterTask.done(CamelInternalProcessor.java:263)
        at org.apache.camel.AsyncCallback.run(AsyncCallback.java:44)
        at org.apache.camel.impl.engine.DefaultReactiveExecutor$Worker.doRun(DefaultReactiveExecutor.java:202)
        at org.apache.camel.impl.engine.DefaultReactiveExecutor$Worker.executeReactiveWork(DefaultReactiveExecutor.java:192)
        at org.apache.camel.impl.engine.DefaultReactiveExecutor$Worker.tryExecuteReactiveWork(DefaultReactiveExecutor.java:169)
        at org.apache.camel.impl.engine.DefaultReactiveExecutor$Worker.schedule(DefaultReactiveExecutor.java:143)
        at org.apache.camel.impl.engine.DefaultReactiveExecutor.scheduleMain(DefaultReactiveExecutor.java:59)
        at org.apache.camel.processor.Pipeline.process(Pipeline.java:162)
        at org.apache.camel.impl.engine.CamelInternalProcessor.processNonTransacted(CamelInternalProcessor.java:385)
        at org.apache.camel.impl.engine.CamelInternalProcessor.process(CamelInternalProcessor.java:361)
        at org.apache.camel.component.seda.SedaConsumer.sendToConsumers(SedaConsumer.java:351)
        at org.apache.camel.component.seda.SedaConsumer.processPolledExchange(SedaConsumer.java:267)
        at org.apache.camel.component.seda.SedaConsumer.doRun(SedaConsumer.java:210)
        at org.apache.camel.component.seda.SedaConsumer.run(SedaConsumer.java:146)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
        at java.base/java.lang.Thread.run(Thread.java:1474)

The issue seems to be caused by recent changes in the Micrometer framework, as reported in the Quarkus 3.35 Release Notes:
https://github.com/quarkusio/quarkus/wiki/Migration-Guide-3.35#micrometer-prometheus-registry

A reproducer can be found in this repository. The message will appear by either running the project in Quarkus dev mode and invoking http://localhost:8080/hello endpoint or by executing the ProducerRouteIT test.

  1. Clone the repository:
git clone https://github.com/fernandobalieiro/camel-event-notifier-reproducer
  1. Start the server:
./mvnw quarkus:dev
  1. Invoke the endpoint:
curl http://localhost:8080/hello

Or execute the integration tests directly:

 ./mvnw verify -DskipITs=false

The warning message containing the stack trace should appear in the logs.

Expected Scenario

The event should be properly registered and no warning message should be visible in the logs.

Consideration

I'm aware that this is maybe not the best approach, as the solution requires iterating over all routes to retrieve the target routeId, so I'm open to suggestions.

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it).

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.

  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR and the detailed reproducer — the bug report is valid. When ProducerTemplate sends to a SEDA endpoint, getFromRouteId() returns null, causing getTags() to produce a tag set without routeId. Other exchanges produce tags with routeId. Since Micrometer Prometheus now requires consistent tag keys across meters with the same name, this causes an IllegalArgumentException.

However, the fix approach has some issues:

1. Semantic mismatchfromRouteId is the route the exchange originated from, not the route it's going to. The lookup finds the destination route by matching endpoint URIs, which is a different concept.

2. Ambiguity — Multiple routes can consume from the same endpoint (e.g., multiple SEDA consumers). findFirst() would pick one arbitrarily.

3. Performance — Iterating all routes on every exchange event is expensive in large contexts with many routes.

4. Incomplete — Many cases won't find a match anyway (dynamic endpoints, timer endpoints, etc.), so the tag inconsistency would still occur for those.

The simpler correct fix is to always include the routeId tag, using an empty string when null. This ensures consistent tag keys across all meters. This is the same approach used in CAMEL-21661 (commit 25b9ce69ae2b) which fixed the same class of problem for getInflightExchangesTags. The entire getTags() method becomes:

String routeId = event.getExchange().getFromRouteId();
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()));

Also, the project requires a JIRA ticket for bug fixes, and commit messages should follow the CAMEL-XXXX: Brief description format.

Please feel free to open a JIRA issue and rework with the simpler approach. Happy to help if you have questions!

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

@fernandobalieiro fernandobalieiro changed the title fix(camel-micrometer): Ensure Prometheus Event Notification when RouteId is set CAMEL-23719: camel-micrometer - when ProducerTemplate sends to a SEDA endpoint, Exchange.getFromRouteId() returns null Jun 9, 2026
@fernandobalieiro fernandobalieiro force-pushed the bugfix/event-notifier-prometheus-registration-failure branch from c4ad39e to 89c99d3 Compare June 9, 2026 09:02

@squakez squakez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the issue is not the tag settings but registering or not the meter. We had a similar issue with app.info - have a look at that to get inspiration for the fix: #17526

@fernandobalieiro fernandobalieiro force-pushed the bugfix/event-notifier-prometheus-registration-failure branch from 89c99d3 to 4b56199 Compare June 9, 2026 10:57
@fernandobalieiro

Copy link
Copy Markdown
Contributor Author

Probably the issue is not the tag settings but registering or not the meter. We had a similar issue with app.info - have a look at that to get inspiration for the fix: #17526

Hello @squakez, makes sense to me, I just pushed the requested changes. Can you review and let me know if something else is needed?

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix for the camel-micrometer exchange event notifier. The changes correctly:

  1. Ensure routeId tag is always present in timer metrics — using an empty string when fromRouteId is null instead of omitting the tag entirely. This prevents Micrometer from creating timers with inconsistent tag key sets (which causes IllegalArgumentException when mixing route-originated and ProducerTemplate-originated exchanges).

  2. Add getOrCreateTimer() helper to consolidate timer lookup/creation logic and avoid re-registering timers.

  3. Include a well-structured test (MicrometerExchangeEventNotifierProducerTemplateTest) that validates consistent tag keys across both route and ProducerTemplate exchanges.

The approach of using an empty string for the missing routeId tag is the right call — it maintains tag cardinality consistency while making it clear the exchange didn't originate from a route.

Fully automatic review from Claude Code

…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.
…rometheus 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.
@davsclaus davsclaus force-pushed the bugfix/event-notifier-prometheus-registration-failure branch from 4b56199 to e592da9 Compare June 10, 2026 06:02
@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-micrometer
  • docs

POM dependency changes: targeted tests included

Modules affected by dependency changes (2)
  • :camel-micrometer
  • :docs
All tested modules (12 modules)
  • Camel :: Docs
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Launcher :: Container
  • Camel :: Micrometer
  • Camel :: Micrometer :: Prometheus
  • Camel :: Observability Services
  • Camel :: Resilience4j :: Micrometer
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin

⚙️ View full build and test results

@davsclaus davsclaus merged commit c0540a4 into apache:main Jun 10, 2026
6 of 8 checks passed
@davsclaus

Copy link
Copy Markdown
Contributor

Thanks for the help

@fernandobalieiro

Copy link
Copy Markdown
Contributor Author

Thanks for the help

No problem at all, thank you for the quick review and merge.
Regards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants