diff --git a/components/camel-micrometer/pom.xml b/components/camel-micrometer/pom.xml index 53b8e3780ea30..acd3673e8c2c2 100644 --- a/components/camel-micrometer/pom.xml +++ b/components/camel-micrometer/pom.xml @@ -78,6 +78,11 @@ camel-test-spring-junit6 test + + io.micrometer + micrometer-registry-prometheus + test + org.mockito 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 e5806e015cc76..01939f9524770 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 void notify(CamelEvent eventObject) { 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 @@ protected void handleDoneEvent(ExchangeEvent doneEvent) { // 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 10d8ba415d78e..5e32f4829d25a 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 @@ default Tags getTags(ExchangeEvent event, Endpoint endpoint) { 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 c1b1451c675ef..532b4b4721d06 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 @@ void getLegacyInflightExchangesName() { 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 0000000000000..fd0bd3073101f --- /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 fd03a8e24d59b..ec098f6310046 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