This is an automated email from the ASF dual-hosted git repository.

squakez pushed a commit to branch feat/CAMEL-24356
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e149a2e35453357a1579a076a061bac89e267742
Author: Pasquale Congiusti <[email protected]>
AuthorDate: Mon Aug 10 14:48:46 2026 +0200

    feat(components): micrometer supports prometheus format
    
    Add a conversion to support the feature from this component instead of the 
prometheus one
    
    Ref CAMEL-24356
---
 .../micrometer/json/AbstractMicrometerService.java | 152 ++++++++++++--
 ...tractMicrometerServicePrometheusFormatTest.java | 225 +++++++++++++++++++++
 2 files changed, 364 insertions(+), 13 deletions(-)

diff --git 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/json/AbstractMicrometerService.java
 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/json/AbstractMicrometerService.java
index fe1c5d942163..65c306bed186 100644
--- 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/json/AbstractMicrometerService.java
+++ 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/json/AbstractMicrometerService.java
@@ -16,7 +16,9 @@
  */
 package org.apache.camel.component.micrometer.json;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
@@ -31,6 +33,7 @@ import io.micrometer.core.instrument.DistributionSummary;
 import io.micrometer.core.instrument.FunctionCounter;
 import io.micrometer.core.instrument.FunctionTimer;
 import io.micrometer.core.instrument.Gauge;
+import io.micrometer.core.instrument.LongTaskTimer;
 import io.micrometer.core.instrument.Meter;
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.Tag;
@@ -56,6 +59,7 @@ public class AbstractMicrometerService extends ServiceSupport 
{
     private boolean prettyPrint = true;
     private boolean skipCamelInfo = false;
     private boolean logMetricsOnShutdown = false;
+    private String logMetricsOnShutdownFormat = "json";
     private String logMetricsOnShutdownFilters[];
     private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
     private Iterable<Tag> matchingTags = Tags.empty();
@@ -103,6 +107,22 @@ public class AbstractMicrometerService extends 
ServiceSupport {
         this.logMetricsOnShutdown = logMetricsOnShutdown;
     }
 
+    /**
+     * Returns the output format used when logging metrics on shutdown. 
Accepted values are {@code "json"} (default) and
+     * {@code "prometheus"}.
+     */
+    public String getLogMetricsOnShutdownFormat() {
+        return logMetricsOnShutdownFormat;
+    }
+
+    /**
+     * Sets the output format used when logging metrics on shutdown. Accepted 
values are {@code "json"} (default) and
+     * {@code "prometheus"}.
+     */
+    public void setLogMetricsOnShutdownFormat(String 
logMetricsOnShutdownFormat) {
+        this.logMetricsOnShutdownFormat = logMetricsOnShutdownFormat;
+    }
+
     public String[] getLogMetricsOnShutdownFilters() {
         return logMetricsOnShutdownFilters;
     }
@@ -243,19 +263,125 @@ public class AbstractMicrometerService extends 
ServiceSupport {
     }
 
     void logMetricsOnShutdown(String... filters) {
-        meterRegistry.getMeters().stream()
-                .filter(m -> 
AbstractMicrometerService.matchesFilter(m.getId().getName(), filters))
-                .map(AbstractMicrometerService::convertMeterToMap)
-                .forEach(logEntry -> {
-                    try {
-                        // we include a start and end tag to make sure the
-                        // scraper can more easily identify the metric content.
-                        String metric = "#METRIC-START#" + 
mapper.writeValueAsString(logEntry) + "#METRIC-END#";
-                        LOG.info(metric);
-                    } catch (Exception e) {
-                        LOG.error("Error logging metric " + 
logEntry.get("name"), e);
-                    }
-                });
+        if ("prometheus".equalsIgnoreCase(logMetricsOnShutdownFormat)) {
+            meterRegistry.getMeters().stream()
+                    .filter(m -> 
AbstractMicrometerService.matchesFilter(m.getId().getName(), filters))
+                    .forEach(this::logMetricsAsPrometheus);
+        } else {
+            meterRegistry.getMeters().stream()
+                    .filter(m -> 
AbstractMicrometerService.matchesFilter(m.getId().getName(), filters))
+                    .map(AbstractMicrometerService::convertMeterToMap)
+                    .forEach(this::logMetricsAsJson);
+        }
+    }
+
+    private void logMetricsAsJson(Map<String, Object> logEntry) {
+        try {
+            // we include a start and end tag to make sure the
+            // scraper can more easily identify the metric content.
+            String metric = "#METRIC-START#" + 
mapper.writeValueAsString(logEntry) + "#METRIC-END#";
+            LOG.info(metric);
+        } catch (Exception e) {
+            LOG.error("Error logging metric {}", logEntry.get("name"), e);
+        }
+    }
+
+    private void logMetricsAsPrometheus(Meter meter) {
+        for (String line : convertMeterToPrometheusLines(meter)) {
+            LOG.info("#METRIC-START#{}#METRIC-END#", line);
+        }
+    }
+
+    /**
+     * Converts a single {@link Meter} into one or more Prometheus 
text-exposition lines.
+     * <p>
+     * The output follows the Prometheus text format specification:
+     *
+     * <pre>
+     * # HELP &lt;name&gt; &lt;description&gt;
+     * # TYPE &lt;name&gt; &lt;type&gt;
+     * &lt;name&gt;{labels} &lt;value&gt;
+     * </pre>
+     *
+     * Metric names use underscores in place of dots/hyphens as required by 
Prometheus naming rules.
+     */
+    static List<String> convertMeterToPrometheusLines(Meter meter) {
+        String rawName = meter.getId().getName();
+        String promName = rawName.replace('.', '_').replace('-', '_');
+        String description = meter.getId().getDescription() != null ? 
meter.getId().getDescription() : promName;
+        String labels = buildPrometheusLabels(meter.getId().getTags());
+
+        List<String> lines = new ArrayList<>();
+
+        if (meter instanceof Gauge g) {
+            lines.add("# HELP " + promName + " " + description);
+            lines.add("# TYPE " + promName + " gauge");
+            lines.add(promName + labels + " " + 
formatPrometheusDouble(g.value()));
+        } else if (meter instanceof Counter c) {
+            lines.add("# HELP " + promName + "_total " + description);
+            lines.add("# TYPE " + promName + "_total counter");
+            lines.add(promName + "_total" + labels + " " + 
formatPrometheusDouble(c.count()));
+        } else if (meter instanceof Timer t) {
+            lines.add("# HELP " + promName + "_seconds " + description);
+            lines.add("# TYPE " + promName + "_seconds summary");
+            lines.add(promName + "_seconds_count" + labels + " " + t.count());
+            lines.add(promName + "_seconds_sum" + labels + " " + 
formatPrometheusDouble(t.totalTime(TimeUnit.SECONDS)));
+            lines.add(promName + "_seconds_max" + labels + " " + 
formatPrometheusDouble(t.max(TimeUnit.SECONDS)));
+        } else if (meter instanceof DistributionSummary ds) {
+            lines.add("# HELP " + promName + " " + description);
+            lines.add("# TYPE " + promName + " summary");
+            lines.add(promName + "_count" + labels + " " + ds.count());
+            lines.add(promName + "_sum" + labels + " " + 
formatPrometheusDouble(ds.totalAmount()));
+            lines.add(promName + "_max" + labels + " " + 
formatPrometheusDouble(ds.max()));
+        } else if (meter instanceof FunctionCounter fc) {
+            lines.add("# HELP " + promName + "_total " + description);
+            lines.add("# TYPE " + promName + "_total counter");
+            lines.add(promName + "_total" + labels + " " + 
formatPrometheusDouble(fc.count()));
+        } else if (meter instanceof FunctionTimer ft) {
+            lines.add("# HELP " + promName + "_seconds " + description);
+            lines.add("# TYPE " + promName + "_seconds summary");
+            lines.add(promName + "_seconds_count" + labels + " " + 
formatPrometheusDouble(ft.count()));
+            lines.add(promName + "_seconds_sum" + labels + " " + 
formatPrometheusDouble(ft.totalTime(TimeUnit.SECONDS)));
+        } else if (meter instanceof LongTaskTimer ltt) {
+            lines.add("# HELP " + promName + "_active_seconds " + description);
+            lines.add("# TYPE " + promName + "_active_seconds gauge");
+            lines.add(promName + "_active_seconds_active" + labels + " " + 
ltt.activeTasks());
+            lines.add(promName + "_active_seconds_duration" + labels + " "
+                      + 
formatPrometheusDouble(ltt.duration(TimeUnit.SECONDS)));
+            lines.add(promName + "_active_seconds_max" + labels + " "
+                      + formatPrometheusDouble(ltt.max(TimeUnit.SECONDS)));
+        } else {
+            // Generic fallback for unknown meter types
+            lines.add("# HELP " + promName + " " + description);
+            lines.add("# TYPE " + promName + " untyped");
+            lines.add(promName + labels + " 0");
+        }
+
+        return lines;
+    }
+
+    private static String buildPrometheusLabels(Iterable<Tag> tags) {
+        StringBuilder sb = new StringBuilder();
+        for (Tag tag : tags) {
+            if (sb.length() > 0) {
+                sb.append(",");
+            }
+            sb.append(tag.getKey().replace('.', '_').replace('-', '_'));
+            sb.append("=\"");
+            sb.append(tag.getValue().replace("\\", "\\\\").replace("\"", 
"\\\"").replace("\n", "\\n"));
+            sb.append("\"");
+        }
+        return sb.length() == 0 ? "" : "{" + sb + "}";
+    }
+
+    private static String formatPrometheusDouble(double value) {
+        if (Double.isNaN(value)) {
+            return "NaN";
+        }
+        if (Double.isInfinite(value)) {
+            return value > 0 ? "+Inf" : "-Inf";
+        }
+        return Double.toString(value);
     }
 
     // This method does a best effort attempt to recover information about 
versioning of the runtime.
diff --git 
a/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/json/AbstractMicrometerServicePrometheusFormatTest.java
 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/json/AbstractMicrometerServicePrometheusFormatTest.java
new file mode 100644
index 000000000000..a4a157c69ac7
--- /dev/null
+++ 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/json/AbstractMicrometerServicePrometheusFormatTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.json;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.*;
+import io.micrometer.core.instrument.LongTaskTimer;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class AbstractMicrometerServicePrometheusFormatTest {
+
+    // ── field default 
────────────────────────────────────────────────────────
+
+    @Test
+    void defaultFormatIsJson() {
+        // verify default without instantiating a full service context
+        // by exercising the getter through a concrete subclass stub
+        AbstractMicrometerService svc = new AbstractMicrometerService() {
+        };
+        assertEquals("json", svc.getLogMetricsOnShutdownFormat());
+    }
+
+    @Test
+    void setFormatPrometheusRoundTrips() {
+        AbstractMicrometerService svc = new AbstractMicrometerService() {
+        };
+        svc.setLogMetricsOnShutdownFormat("prometheus");
+        assertEquals("prometheus", svc.getLogMetricsOnShutdownFormat());
+    }
+
+    // ── Gauge 
────────────────────────────────────────────────────────────────
+
+    @Test
+    void gaugeProducesHelpTypeAndValue() {
+        Gauge gauge = mock(Gauge.class);
+        Meter.Id id = new Meter.Id("app.info", Tags.empty(), null, 
"Application info", Meter.Type.GAUGE);
+        when(gauge.getId()).thenReturn(id);
+        when(gauge.value()).thenReturn(1.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(gauge);
+
+        assertEquals("# HELP app_info Application info", lines.get(0));
+        assertEquals("# TYPE app_info gauge", lines.get(1));
+        assertEquals("app_info 1.0", lines.get(2));
+        assertEquals(3, lines.size());
+    }
+
+    @Test
+    void gaugeNaNIsRenderedAsNaN() {
+        Gauge gauge = mock(Gauge.class);
+        Meter.Id id = new Meter.Id("my.gauge", Tags.empty(), null, null, 
Meter.Type.GAUGE);
+        when(gauge.getId()).thenReturn(id);
+        when(gauge.value()).thenReturn(Double.NaN);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(gauge);
+
+        assertTrue(lines.get(2).endsWith(" NaN"));
+    }
+
+    // ── Counter 
──────────────────────────────────────────────────────────────
+
+    @Test
+    void counterProducesTotalSuffix() {
+        Counter counter = mock(Counter.class);
+        Meter.Id id = new Meter.Id("camel.exchanges.completed", Tags.empty(), 
null, null, Meter.Type.COUNTER);
+        when(counter.getId()).thenReturn(id);
+        when(counter.count()).thenReturn(42.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(counter);
+
+        assertEquals("# HELP camel_exchanges_completed_total 
camel_exchanges_completed", lines.get(0));
+        assertEquals("# TYPE camel_exchanges_completed_total counter", 
lines.get(1));
+        assertEquals("camel_exchanges_completed_total 42.0", lines.get(2));
+        assertEquals(3, lines.size());
+    }
+
+    // ── Timer 
────────────────────────────────────────────────────────────────
+
+    @Test
+    void timerProducesCountSumMaxInSeconds() {
+        Timer timer = mock(Timer.class);
+        Meter.Id id = new Meter.Id("camel.route.duration", Tags.empty(), null, 
"Route duration", Meter.Type.TIMER);
+        when(timer.getId()).thenReturn(id);
+        when(timer.count()).thenReturn(10L);
+        when(timer.totalTime(TimeUnit.SECONDS)).thenReturn(2.5);
+        when(timer.max(TimeUnit.SECONDS)).thenReturn(0.8);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(timer);
+
+        assertEquals("# HELP camel_route_duration_seconds Route duration", 
lines.get(0));
+        assertEquals("# TYPE camel_route_duration_seconds summary", 
lines.get(1));
+        assertEquals("camel_route_duration_seconds_count 10", lines.get(2));
+        assertEquals("camel_route_duration_seconds_sum 2.5", lines.get(3));
+        assertEquals("camel_route_duration_seconds_max 0.8", lines.get(4));
+        assertEquals(5, lines.size());
+    }
+
+    // ── DistributionSummary 
───────────────────────────────────────────────────
+
+    @Test
+    void distributionSummaryProducesCountSumMax() {
+        DistributionSummary ds = mock(DistributionSummary.class);
+        Meter.Id id = new Meter.Id("payload.size", Tags.empty(), null, null, 
Meter.Type.DISTRIBUTION_SUMMARY);
+        when(ds.getId()).thenReturn(id);
+        when(ds.count()).thenReturn(5L);
+        when(ds.totalAmount()).thenReturn(500.0);
+        when(ds.max()).thenReturn(200.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(ds);
+
+        assertEquals("# TYPE payload_size summary", lines.get(1));
+        assertEquals("payload_size_count 5", lines.get(2));
+        assertEquals("payload_size_sum 500.0", lines.get(3));
+        assertEquals("payload_size_max 200.0", lines.get(4));
+    }
+
+    // ── FunctionCounter 
───────────────────────────────────────────────────────
+
+    @Test
+    void functionCounterProducesTotalSuffix() {
+        FunctionCounter fc = mock(FunctionCounter.class);
+        Meter.Id id = new Meter.Id("my.fc", Tags.empty(), null, null, 
Meter.Type.COUNTER);
+        when(fc.getId()).thenReturn(id);
+        when(fc.count()).thenReturn(7.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(fc);
+
+        assertTrue(lines.stream().anyMatch(l -> l.equals("# TYPE my_fc_total 
counter")));
+        assertTrue(lines.stream().anyMatch(l -> l.equals("my_fc_total 7.0")));
+    }
+
+    // ── LongTaskTimer 
─────────────────────────────────────────────────────────
+
+    @Test
+    void longTaskTimerProducesActiveTasksDurationMax() {
+        LongTaskTimer ltt = mock(LongTaskTimer.class);
+        Meter.Id id = new Meter.Id("camel.long.task", Tags.empty(), null, 
"Long running task", Meter.Type.LONG_TASK_TIMER);
+        when(ltt.getId()).thenReturn(id);
+        when(ltt.activeTasks()).thenReturn(3);
+        when(ltt.duration(TimeUnit.SECONDS)).thenReturn(12.5);
+        when(ltt.max(TimeUnit.SECONDS)).thenReturn(6.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(ltt);
+
+        assertEquals("# HELP camel_long_task_active_seconds Long running 
task", lines.get(0));
+        assertEquals("# TYPE camel_long_task_active_seconds gauge", 
lines.get(1));
+        assertEquals("camel_long_task_active_seconds_active 3", lines.get(2));
+        assertEquals("camel_long_task_active_seconds_duration 12.5", 
lines.get(3));
+        assertEquals("camel_long_task_active_seconds_max 6.0", lines.get(4));
+        assertEquals(5, lines.size());
+    }
+
+    // ── FunctionTimer 
─────────────────────────────────────────────────────────
+
+    @Test
+    void functionTimerProducesCountAndSum() {
+        FunctionTimer ft = mock(FunctionTimer.class);
+        Meter.Id id = new Meter.Id("my.ft", Tags.empty(), null, null, 
Meter.Type.TIMER);
+        when(ft.getId()).thenReturn(id);
+        when(ft.count()).thenReturn(3.0);
+        when(ft.totalTime(TimeUnit.SECONDS)).thenReturn(1.5);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(ft);
+
+        assertTrue(lines.stream().anyMatch(l -> l.equals("my_ft_seconds_count 
3.0")));
+        assertTrue(lines.stream().anyMatch(l -> l.equals("my_ft_seconds_sum 
1.5")));
+    }
+
+    // ── label encoding 
────────────────────────────────────────────────────────
+
+    @Test
+    void tagsAreRenderedAsPrometheusLabels() {
+        Gauge gauge = mock(Gauge.class);
+        Meter.Id id = new Meter.Id(
+                "app.info",
+                Tags.of("camel.version", "4.0.0", "camel.context", "my-ctx"),
+                null, null, Meter.Type.GAUGE);
+        when(gauge.getId()).thenReturn(id);
+        when(gauge.value()).thenReturn(0.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(gauge);
+
+        String valueLine = lines.get(2);
+        assertTrue(valueLine.startsWith("app_info{"), "value line should carry 
labels");
+        assertTrue(valueLine.contains("camel_version=\"4.0.0\""));
+        assertTrue(valueLine.contains("camel_context=\"my-ctx\""));
+    }
+
+    @Test
+    void specialCharsInTagValuesAreEscaped() {
+        Gauge gauge = mock(Gauge.class);
+        Meter.Id id = new Meter.Id(
+                "my.gauge",
+                Tags.of("path", "say\\\"hello\"\nworld"),
+                null, null, Meter.Type.GAUGE);
+        when(gauge.getId()).thenReturn(id);
+        when(gauge.value()).thenReturn(1.0);
+
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(gauge);
+        String valueLine = lines.get(2);
+
+        assertTrue(valueLine.contains("\\\\"), "backslash must be escaped");
+        assertTrue(valueLine.contains("\\\""), "quote must be escaped");
+        assertTrue(valueLine.contains("\\n"), "newline must be escaped");
+    }
+}

Reply via email to