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 351c58e8c75b CAMEL-16866 add route event-notifier (#19867)
351c58e8c75b is described below

commit 351c58e8c75b948f31165e6a6889e1f0321ec2de
Author: Jono Morris <[email protected]>
AuthorDate: Tue Nov 11 02:14:50 2025 +1300

    CAMEL-16866 add route event-notifier (#19867)
---
 .../main/docs/opentelemetry-metrics-component.adoc |  29 +++++
 .../metrics/OpenTelemetryConstants.java            |  11 ++
 .../OpenTelemetryRouteEventNotifier.java           | 144 +++++++++++++++++++++
 ...nTelemetryRouteEventNotifierNamingStrategy.java |  57 ++++++++
 .../metrics/AbstractOpenTelemetryTest.java         |  82 ++++++++++++
 .../OpenTelemetryRouteEventNotifierTest.java       |  80 ++++++++++++
 6 files changed, 403 insertions(+)

diff --git 
a/components/camel-opentelemetry-metrics/src/main/docs/opentelemetry-metrics-component.adoc
 
b/components/camel-opentelemetry-metrics/src/main/docs/opentelemetry-metrics-component.adoc
index d8638a50cd1e..cf3b38744b92 100644
--- 
a/components/camel-opentelemetry-metrics/src/main/docs/opentelemetry-metrics-component.adoc
+++ 
b/components/camel-opentelemetry-metrics/src/main/docs/opentelemetry-metrics-component.adoc
@@ -292,6 +292,35 @@ from("direct:in")
     .to("direct:out");
 ----
 
+=== OpenTelemetry Event Notification
+
+==== Camel Route Event Notifier
+
+The Route Event Notifier counts the number of added and running routes, and 
can be added to the CamelContext as follows:
+
+[source,java]
+----
+camelContext.getManagementStrategy().addEventNotifier(new 
OpenTelemetryRouteEventNotifier());
+----
+
+Camel specific metrics that are available:
+
+[width="100%",options="header"]
+|=====================================================
+|Default Name |Type |Description
+|camel.routes.added | LongUpDownCounter | Number of routes in total
+|camel.routes.reloaded | LongCounter | Number of routes that have been reloaded
+|camel.routes.running | LongUpDownCounter | Number of routes currently running
+|=====================================================
+
+The following options are supported:
+
+[width="100%",options="header"]
+|=======================================================================
+|Name |Default |Description
+| namingStrategy | OpenTelemetryRouteEventNotifierNamingStrategy.DEFAULT | The 
strategy to use for overriding default metric names.
+|=======================================================================
+
 == OpenTelemetry Configuration
 
 Applications can export collected metrics to various backends using different 
exporters, including the OpenTelemetry Protocol (OTLP) exporter,
diff --git 
a/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/OpenTelemetryConstants.java
 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/OpenTelemetryConstants.java
index 7d3b3ce7251b..5514cc29af99 100644
--- 
a/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/OpenTelemetryConstants.java
+++ 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/OpenTelemetryConstants.java
@@ -39,8 +39,19 @@ public class OpenTelemetryConstants {
 
     public static final String HEADER_METRIC_ATTRIBUTES = HEADER_PREFIX + 
"Attributes";
 
+    // Route-event metrics
+    public static final String DEFAULT_CAMEL_ROUTES_ADDED = 
"camel.routes.added";
+    public static final String DEFAULT_CAMEL_ROUTES_RUNNING = 
"camel.routes.running";
+    public static final String DEFAULT_CAMEL_ROUTES_RELOADED = 
"camel.routes.reloaded";
+
     // OpenTelemetry Attribute keys
     public static final String CAMEL_CONTEXT_ATTRIBUTE = "camelContext";
+    public static final String ROUTE_ID_ATTRIBUTE = "routeId";
+    public static final String EVENT_TYPE_ATTRIBUTE = "eventType";
+    public static final String KIND_ATTRIBUTE = "kind";
+
+    // OpenTelemetry Attribute values
+    public static final String KIND_ROUTE = "CamelRoute";
 
     private OpenTelemetryConstants() {
         // no-op
diff --git 
a/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifier.java
 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifier.java
new file mode 100644
index 000000000000..7a7c92e52cdf
--- /dev/null
+++ 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifier.java
@@ -0,0 +1,144 @@
+/*
+ * 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.opentelemetry.metrics.eventnotifier;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.LongUpDownCounter;
+import io.opentelemetry.api.metrics.Meter;
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.RouteEvent;
+import org.apache.camel.spi.ManagementStrategy;
+import org.apache.camel.support.CamelContextHelper;
+import org.apache.camel.support.EventNotifierSupport;
+
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.CAMEL_CONTEXT_ATTRIBUTE;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.EVENT_TYPE_ATTRIBUTE;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.KIND_ATTRIBUTE;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.KIND_ROUTE;
+
+public class OpenTelemetryRouteEventNotifier extends EventNotifierSupport 
implements CamelContextAware {
+
+    private final Class<RouteEvent> eventType = RouteEvent.class;
+    private Meter meter;
+    boolean registerKamelets;
+    boolean registerTemplates = true;
+    private Attributes attributes = Attributes.of(
+            AttributeKey.stringKey(KIND_ATTRIBUTE), KIND_ROUTE,
+            AttributeKey.stringKey(EVENT_TYPE_ATTRIBUTE), 
RouteEvent.class.getSimpleName());
+
+    // Event Notifier options
+    private OpenTelemetryRouteEventNotifierNamingStrategy namingStrategy
+            = OpenTelemetryRouteEventNotifierNamingStrategy.DEFAULT;
+
+    // OpenTelemetry instruments
+    private LongUpDownCounter addedCounter;
+    private LongUpDownCounter runningCounter;
+    private LongCounter reloadedCounter;
+
+    public OpenTelemetryRouteEventNotifier() {
+        // no-op
+    }
+
+    public void 
setNamingStrategy(OpenTelemetryRouteEventNotifierNamingStrategy namingStrategy) 
{
+        this.namingStrategy = namingStrategy;
+    }
+
+    public OpenTelemetryRouteEventNotifierNamingStrategy getNamingStrategy() {
+        return namingStrategy;
+    }
+
+    public void setMeter(Meter meter) {
+        this.meter = meter;
+    }
+
+    public Meter getMeter() {
+        return meter;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        super.setCamelContext(camelContext);
+        this.attributes = Attributes.of(
+                AttributeKey.stringKey(KIND_ATTRIBUTE), KIND_ROUTE,
+                AttributeKey.stringKey(CAMEL_CONTEXT_ATTRIBUTE), 
camelContext.getName(),
+                AttributeKey.stringKey(EVENT_TYPE_ATTRIBUTE), 
RouteEvent.class.getSimpleName());
+    }
+
+    @Override
+    public boolean isEnabled(CamelEvent eventObject) {
+        return eventType.isAssignableFrom(eventObject.getClass());
+    }
+
+    @Override
+    protected void doInit() throws Exception {
+        super.doInit();
+        if (meter == null) {
+            this.meter = 
CamelContextHelper.findSingleByType(getCamelContext(), Meter.class);
+        }
+        if (meter == null) {
+            this.meter = GlobalOpenTelemetry.get().getMeter("camel");
+        }
+        if (meter == null) {
+            throw new RuntimeCamelException("Could not find any OpenTelemetry 
meter!");
+        }
+        ManagementStrategy ms = getCamelContext().getManagementStrategy();
+        if (ms != null && ms.getManagementAgent() != null) {
+            registerKamelets = 
ms.getManagementAgent().getRegisterRoutesCreateByKamelet();
+            registerTemplates = 
ms.getManagementAgent().getRegisterRoutesCreateByTemplate();
+        }
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        addedCounter = 
meter.upDownCounterBuilder(namingStrategy.getRouteAddedName())
+                .setUnit("routes").build();
+        runningCounter = 
meter.upDownCounterBuilder(namingStrategy.getRouteRunningName())
+                .setUnit("routes").build();
+        reloadedCounter = 
meter.counterBuilder(namingStrategy.getRouteReloadedName())
+                .setUnit("routes").build();
+    }
+
+    @Override
+    public void notify(CamelEvent event) {
+        if (event instanceof RouteEvent re) {
+            // skip routes that should not be included
+            boolean skip = (re.getRoute().isCreatedByKamelet() && 
!registerKamelets)
+                    || (re.getRoute().isCreatedByRouteTemplate() && 
!registerTemplates);
+            if (skip) {
+                return;
+            }
+        }
+        if (event instanceof CamelEvent.RouteAddedEvent) {
+            addedCounter.add(1L, attributes);
+        } else if (event instanceof CamelEvent.RouteRemovedEvent) {
+            addedCounter.add(-1L, attributes);
+        } else if (event instanceof CamelEvent.RouteStartedEvent) {
+            runningCounter.add(1L, attributes);
+        } else if (event instanceof CamelEvent.RouteStoppedEvent) {
+            runningCounter.add(-1L, attributes);
+        } else if (event instanceof CamelEvent.RouteReloadedEvent) {
+            reloadedCounter.add(1L, attributes);
+        }
+    }
+}
diff --git 
a/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierNamingStrategy.java
 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierNamingStrategy.java
new file mode 100644
index 000000000000..87d99fffdea1
--- /dev/null
+++ 
b/components/camel-opentelemetry-metrics/src/main/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierNamingStrategy.java
@@ -0,0 +1,57 @@
+/*
+ * 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.opentelemetry.metrics.eventnotifier;
+
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.DEFAULT_CAMEL_ROUTES_ADDED;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.DEFAULT_CAMEL_ROUTES_RELOADED;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.DEFAULT_CAMEL_ROUTES_RUNNING;
+
+public interface OpenTelemetryRouteEventNotifierNamingStrategy {
+
+    /**
+     * Default naming strategy.
+     */
+    OpenTelemetryRouteEventNotifierNamingStrategy DEFAULT = new 
OpenTelemetryRouteEventNotifierNamingStrategy() {
+        @Override
+        public String formatName(String name) {
+            return name;
+        }
+
+        @Override
+        public String getRouteAddedName() {
+            return DEFAULT_CAMEL_ROUTES_ADDED;
+        }
+
+        @Override
+        public String getRouteRunningName() {
+            return DEFAULT_CAMEL_ROUTES_RUNNING;
+        }
+
+        @Override
+        public String getRouteReloadedName() {
+            return DEFAULT_CAMEL_ROUTES_RELOADED;
+        }
+    };
+
+    String formatName(String name);
+
+    String getRouteAddedName();
+
+    String getRouteRunningName();
+
+    String getRouteReloadedName();
+}
diff --git 
a/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/AbstractOpenTelemetryTest.java
 
b/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/AbstractOpenTelemetryTest.java
new file mode 100644
index 000000000000..06c32d7a8470
--- /dev/null
+++ 
b/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/AbstractOpenTelemetryTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.opentelemetry.metrics;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.sdk.metrics.data.LongPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.data.PointData;
+import org.apache.camel.test.junit5.CamelTestSupport;
+
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.ROUTE_ID_ATTRIBUTE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AbstractOpenTelemetryTest extends CamelTestSupport {
+
+    public final CamelOpenTelemetryExtension otelExtension = 
CamelOpenTelemetryExtension.create();
+
+    protected List<MetricData> getMetricData(String metricName) {
+        return otelExtension.getMetrics().stream()
+                .filter(d -> d.getName().equals(metricName))
+                .collect(Collectors.toList());
+    }
+
+    protected PointData getPointDataForRouteId(String metricName, String 
routeId) {
+        List<PointData> pointDataList = getAllPointDataForRouteId(metricName, 
routeId);
+        assertEquals(1, pointDataList.size(),
+                "Should have one metric for routeId " + routeId + " and 
metricName " + metricName);
+        return pointDataList.get(0);
+    }
+
+    protected LongPointData getSingleLongPointData(String meterName, String 
routeId) {
+        List<PointData> pdList = getAllPointDataForRouteId(meterName, routeId);
+        assertEquals(1, pdList.size(), "Should have one metric for routeId " + 
routeId + " and meterName " + meterName);
+        PointData pd = pdList.get(0);
+        assertInstanceOf(LongPointData.class, pd);
+        return (LongPointData) pd;
+    }
+
+    protected List<PointData> getAllPointDataForRouteId(String metricName, 
String routeId) {
+        return otelExtension.getMetrics().stream()
+                .filter(d -> d.getName().equals(metricName))
+                .map(metricData -> metricData.getData().getPoints())
+                .flatMap(Collection::stream)
+                .filter(point -> routeId.equals(getRouteId(point)))
+                .collect(Collectors.toList());
+    }
+
+    protected List<PointData> getAllPointData(String metricName) {
+        return otelExtension.getMetrics().stream()
+                .filter(d -> d.getName().equals(metricName))
+                .map(metricData -> metricData.getData().getPoints())
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
+    }
+
+    protected String getRouteId(PointData pd) {
+        Map<AttributeKey<?>, Object> m = pd.getAttributes().asMap();
+        assertTrue(m.containsKey(AttributeKey.stringKey(ROUTE_ID_ATTRIBUTE)));
+        return (String) m.get(AttributeKey.stringKey(ROUTE_ID_ATTRIBUTE));
+    }
+}
diff --git 
a/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierTest.java
 
b/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierTest.java
new file mode 100644
index 000000000000..8807ebaabc1c
--- /dev/null
+++ 
b/components/camel-opentelemetry-metrics/src/test/java/org/apache/camel/opentelemetry/metrics/eventnotifier/OpenTelemetryRouteEventNotifierTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.opentelemetry.metrics.eventnotifier;
+
+import java.util.List;
+
+import io.opentelemetry.sdk.metrics.data.LongPointData;
+import io.opentelemetry.sdk.metrics.data.PointData;
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.opentelemetry.metrics.AbstractOpenTelemetryTest;
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.DEFAULT_CAMEL_ROUTES_ADDED;
+import static 
org.apache.camel.opentelemetry.metrics.OpenTelemetryConstants.DEFAULT_CAMEL_ROUTES_RUNNING;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class OpenTelemetryRouteEventNotifierTest extends 
AbstractOpenTelemetryTest {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        OpenTelemetryRouteEventNotifier ren = new 
OpenTelemetryRouteEventNotifier();
+        ren.setMeter(otelExtension.getOpenTelemetry().getMeter("meterTest"));
+        context.getManagementStrategy().addEventNotifier(ren);
+        ren.init();
+        return context;
+    }
+
+    @Test
+    public void testCamelRouteEvents() throws Exception {
+
+        verifyMetric(DEFAULT_CAMEL_ROUTES_ADDED, 1L);
+        verifyMetric(DEFAULT_CAMEL_ROUTES_RUNNING, 1L);
+
+        context.getRouteController().stopRoute("test");
+
+        verifyMetric(DEFAULT_CAMEL_ROUTES_ADDED, 1L);
+        verifyMetric(DEFAULT_CAMEL_ROUTES_RUNNING, 0L);
+
+        context.removeRoute("test");
+
+        verifyMetric(DEFAULT_CAMEL_ROUTES_ADDED, 0L);
+        verifyMetric(DEFAULT_CAMEL_ROUTES_RUNNING, 0L);
+    }
+
+    private void verifyMetric(String metricName, long expected) {
+        List<PointData> ls = getAllPointData(metricName);
+        assertEquals(1, ls.size(), "Expected one point data");
+        PointData pd = ls.get(0);
+
+        assertInstanceOf(LongPointData.class, pd);
+        assertEquals(expected, ((LongPointData) pd).getValue());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:in").routeId("test").to("mock:out");
+            }
+        };
+    }
+}

Reply via email to