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

squakez 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 19202ffbfd36 feat(components): micrometer supports prometheus format
19202ffbfd36 is described below

commit 19202ffbfd3680506179e9851f634a0015dd29ef
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
---
 .../org/apache/camel/catalog/docs/main.adoc        |   3 +-
 .../main/camel-main-configuration-metadata.json    |   1 +
 .../prometheus/MicrometerPrometheus.java           |  53 +----
 .../AbstractMicrometerEventNotifier.java           |  10 +
 .../micrometer/json/AbstractMicrometerService.java | 190 +++++++++++++++-
 ...tractMicrometerServicePrometheusFormatTest.java | 248 +++++++++++++++++++++
 .../MetricsConfigurationPropertiesConfigurer.java  |   7 +
 .../camel-main-configuration-metadata.json         |   1 +
 core/camel-main/src/main/docs/main.adoc            |   3 +-
 .../camel/main/MetricsConfigurationProperties.java |  21 ++
 10 files changed, 477 insertions(+), 60 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
index db7bd0eb0a19..3fe96305eab4 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
@@ -646,7 +646,7 @@ The camel.mdc supports 3 options, which are listed below.
 
 
 === Camel Micrometer Metrics configurations
-The camel.metrics supports 16 options, which are listed below.
+The camel.metrics supports 17 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -662,6 +662,7 @@ The camel.metrics supports 16 options, which are listed 
below.
 | *camel.metrics.enableRoutePolicy* | Set whether to enable the 
MicrometerRoutePolicyFactory for capturing metrics on route processing times. | 
true | boolean
 | *camel.metrics.logMetricsOnShutdown* | Log metrics when application is 
shutting down. (default, false). | false | boolean
 | *camel.metrics.logMetricsOnShutdownFilters* | List of metrics (comma 
separated) to log when application is shutting down. You can use character to 
log any metrics containing the wildcard, for example camel.exchanges. (default 
to all metrics available). |  | String
+| *camel.metrics.logMetricsOnShutdownFormat* | Format used to log metrics when 
application is shutting down. Either json (default) or prometheus format. | 
json | String
 | *camel.metrics.namingStrategy* | Controls the name style to use for metrics. 
Default = uses micrometer naming convention. Legacy = uses the classic naming 
style (camelCase) | default | String
 | *camel.metrics.path* | The path endpoint used to expose the metrics. | 
/observe/metrics | String
 | *camel.metrics.routePolicyLevel* | Sets the level of information to capture. 
all = both context and routes. | all | String
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index c03dd1557f00..1146c05cd1cc 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -334,6 +334,7 @@
     { "name": "camel.metrics.enableRoutePolicy", "required": false, 
"description": "Set whether to enable the MicrometerRoutePolicyFactory for 
capturing metrics on route processing times.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": true, "secret": false },
     { "name": "camel.metrics.logMetricsOnShutdown", "required": false, 
"description": "Log metrics when application is shutting down. (default, 
false).", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
     { "name": "camel.metrics.logMetricsOnShutdownFilters", "required": false, 
"description": "List of metrics (comma separated) to log when application is 
shutting down. You can use character to log any metrics containing the 
wildcard, for example camel.exchanges. (default to all metrics available).", 
"sourceType": "org.apache.camel.main.MetricsConfigurationProperties", "type": 
"string", "javaType": "java.lang.String", "secret": false },
+    { "name": "camel.metrics.logMetricsOnShutdownFormat", "required": false, 
"description": "Format used to log metrics when application is shutting down. 
Either json (default) or prometheus format.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "json", "secret": false, 
"enum": [ "json", "prometheus" ] },
     { "name": "camel.metrics.namingStrategy", "required": false, 
"description": "Controls the name style to use for metrics. Default = uses 
micrometer naming convention. Legacy = uses the classic naming style 
(camelCase)", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, 
"enum": [ "default", "legacy" ] },
     { "name": "camel.metrics.path", "required": false, "description": "The 
path endpoint used to expose the metrics.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/observe\/metrics", "secret": 
false },
     { "name": "camel.metrics.routePolicyLevel", "required": false, 
"description": "Sets the level of information to capture. all = both context 
and routes.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "all", "secret": false, "enum": 
[ "all", "route", "context" ] },
diff --git 
a/components/camel-micrometer-prometheus/src/main/java/org/apache/camel/component/micrometer/prometheus/MicrometerPrometheus.java
 
b/components/camel-micrometer-prometheus/src/main/java/org/apache/camel/component/micrometer/prometheus/MicrometerPrometheus.java
index d4f8ad9feb02..d9df02a1ea43 100644
--- 
a/components/camel-micrometer-prometheus/src/main/java/org/apache/camel/component/micrometer/prometheus/MicrometerPrometheus.java
+++ 
b/components/camel-micrometer-prometheus/src/main/java/org/apache/camel/component/micrometer/prometheus/MicrometerPrometheus.java
@@ -371,14 +371,13 @@ public class MicrometerPrometheus extends ServiceSupport 
implements CamelMetrics
         if (isEnableExchangeEventNotifier()) {
             MicrometerExchangeEventNotifier notifier = new 
MicrometerExchangeEventNotifier();
             notifier.setSkipCamelInfo(isSkipCamelInfo());
-            // We delegate the print to the notifier only when the format
-            // configuration is json. If prometheus, we must take care in this 
component instead
-            if (getLogMetricsOnShutdownFormat().equals("json")) {
-                notifier.setLogMetricsOnShutdown(isLogMetricsOnShutdown());
-                if (getLogMetricsOnShutdownFilters() != null) {
-                    String[] meterFilters = 
getLogMetricsOnShutdownFilters().split(",");
-                    notifier.setLogMetricsOnShutdownFilters(meterFilters);
-                }
+            notifier.setLogMetricsOnShutdown(isLogMetricsOnShutdown());
+            if (getLogMetricsOnShutdownFormat() != null) {
+                
notifier.setLogMetricsOnShutdownFormat(getLogMetricsOnShutdownFormat());
+            }
+            if (getLogMetricsOnShutdownFilters() != null) {
+                String[] meterFilters = 
getLogMetricsOnShutdownFilters().split(",");
+                notifier.setLogMetricsOnShutdownFilters(meterFilters);
             }
             
notifier.setBaseEndpointURI(isBaseEndpointURIExchangeEventNotifier());
             if ("legacy".equalsIgnoreCase(namingStrategy)) {
@@ -499,44 +498,6 @@ public class MicrometerPrometheus extends ServiceSupport 
implements CamelMetrics
         }
     }
 
-    @Override
-    protected void doStop() {
-        // NOTE: this components only takes care to trace when we set the 
"prometheus" format
-        if (logMetricsOnShutdown && 
logMetricsOnShutdownFormat.equals("prometheus")) {
-            LOG.warn("Micrometer service is stopping, here a list of metrics 
collected so far.");
-            // Default: all metrics
-            logMetricsOnShutdown(
-                    logMetricsOnShutdownFilters == null ? new String[] { "*" } 
: logMetricsOnShutdownFilters.split(","));
-        }
-    }
-
-    void logMetricsOnShutdown(String... filters) {
-        String[] scrapes = meterRegistry.scrape().split("\n");
-        for (String s : scrapes) {
-            if (matchesFilter(s, filters)) {
-                // we include a start and end tag to make sure the
-                // scraper can more easily identify the metric content.
-                LOG.info("#METRIC-START#" + s + "#METRIC-END#");
-            }
-        }
-    }
-
-    static boolean matchesFilter(String line, String... filters) {
-        for (String filter : filters) {
-            // Prometheus format does not use . but _ instead
-            filter = filter.replaceAll("\\.", "_");
-
-            if (filter.contains("*")) {
-                if (line.contains(filter.replace("*", ""))) {
-                    return true;
-                }
-            } else if (line.contains(filter)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @Override
     protected void doShutdown() throws Exception {
         super.doShutdown();
diff --git 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/AbstractMicrometerEventNotifier.java
 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/AbstractMicrometerEventNotifier.java
index 1666d7e67f14..06badf11a35b 100644
--- 
a/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/AbstractMicrometerEventNotifier.java
+++ 
b/components/camel-micrometer/src/main/java/org/apache/camel/component/micrometer/eventnotifier/AbstractMicrometerEventNotifier.java
@@ -41,6 +41,7 @@ public abstract class AbstractMicrometerEventNotifier<T 
extends CamelEvent> exte
     private boolean skipCamelInfo = false;
     private boolean logMetricsOnShutdown = false;
     private String logMetricsOnShutdownFilters[];
+    private String logMetricsOnShutdownFormat = "json";
     private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
 
     protected AbstractMicrometerEventNotifier(Class<T> eventType) {
@@ -89,6 +90,14 @@ public abstract class AbstractMicrometerEventNotifier<T 
extends CamelEvent> exte
         this.logMetricsOnShutdown = logMetricsOnShutdown;
     }
 
+    public String getLogMetricsOnShutdownFormat() {
+        return logMetricsOnShutdownFormat;
+    }
+
+    public void setLogMetricsOnShutdownFormat(String 
logMetricsOnShutdownFormat) {
+        this.logMetricsOnShutdownFormat = logMetricsOnShutdownFormat;
+    }
+
     public String[] getLogMetricsOnShutdownFilters() {
         return logMetricsOnShutdownFilters;
     }
@@ -127,6 +136,7 @@ public abstract class AbstractMicrometerEventNotifier<T 
extends CamelEvent> exte
                 registryService.setSkipCamelInfo(isSkipCamelInfo());
                 
registryService.setLogMetricsOnShutdown(isLogMetricsOnShutdown());
                 
registryService.setLogMetricsOnShutdownFilters(getLogMetricsOnShutdownFilters());
+                
registryService.setLogMetricsOnShutdownFormat(getLogMetricsOnShutdownFormat());
                 registryService.setDurationUnit(getDurationUnit());
                 registryService.setMatchingTags(Tags.of(KIND, KIND_EXCHANGE));
                 camelContext.addService(registryService);
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..0e36dc942a35 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,10 @@
  */
 package org.apache.camel.component.micrometer.json;
 
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
@@ -31,6 +34,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 +60,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 +108,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 +264,164 @@ 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)) {
+            List<Meter> meters = meterRegistry.getMeters().stream()
+                    .filter(m -> AbstractMicrometerService.matchesFilter(
+                            m.getId().getName(), filters))
+                    
.sorted(Comparator.comparing(AbstractMicrometerService::normalizePrometheusName))
+                    .toList();
+
+            String previousPromName = null;
+
+            for (Meter meter : meters) {
+                String promName = normalizePrometheusName(meter);
+
+                List<String> lines = convertMeterToPrometheusLines(promName, 
meter);
+                boolean newMetric = !promName.equals(previousPromName);
+
+                for (String line : lines) {
+                    if (!newMetric
+                            && (line.startsWith("# HELP ")
+                                    || line.startsWith("# TYPE "))) {
+                        continue;
                     }
-                });
+
+                    LOG.info("#METRIC-START#{}#METRIC-END#", line);
+                }
+
+                previousPromName = promName;
+            }
+
+        } 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);
+        }
+    }
+
+    /**
+     * 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(String promName, Meter 
meter) {
+        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 + "_seconds " + description);
+            lines.add("# TYPE " + promName + "_seconds gauge");
+            lines.add(promName + "_seconds_count" + labels + " "
+                      + ltt.activeTasks());
+            lines.add(promName + "_seconds_sum" + labels + " "
+                      + 
formatPrometheusDouble(ltt.duration(TimeUnit.SECONDS)));
+            lines.add(promName + "_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;
+    }
+
+    static String normalizePrometheusName(Meter meter) {
+        String rawName = meter.getId().getName();
+        String name = rawName.replace('.', '_').replace('-', '_');
+        String baseUnit = meter.getId().getBaseUnit();
+
+        if (baseUnit != null && !baseUnit.isBlank()) {
+            String unit = baseUnit
+                    .replace('.', '_')
+                    .replace('-', '_');
+
+            if (!name.endsWith("_" + unit)) {
+                name += "_" + unit;
+            }
+        }
+
+        if (meter instanceof Counter || meter instanceof FunctionCounter) {
+            name = name.replaceFirst("_total$", "");
+        }
+
+        return name;
+    }
+
+    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..7f93b158056c
--- /dev/null
+++ 
b/components/camel-micrometer/src/test/java/org/apache/camel/component/micrometer/json/AbstractMicrometerServicePrometheusFormatTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.Counter;
+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.Tags;
+import io.micrometer.core.instrument.Timer;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class AbstractMicrometerServicePrometheusFormatTest {
+
+    // ── field default 
────────────────────────────────────────────────────────
+
+    @Test
+    void defaultFormatIsJson() throws IOException {
+        try (// verify default without instantiating a full service context
+             // by exercising the getter through a concrete subclass stub
+             AbstractMicrometerService svc = new AbstractMicrometerService() {
+             }) {
+            assertThat(svc.getLogMetricsOnShutdownFormat()).isEqualTo("json");
+        }
+    }
+
+    @Test
+    void setFormatPrometheusRoundTrips() throws IOException {
+        try (AbstractMicrometerService svc = new AbstractMicrometerService() {
+        }) {
+            svc.setLogMetricsOnShutdownFormat("prometheus");
+            
assertThat(svc.getLogMetricsOnShutdownFormat()).isEqualTo("prometheus");
+        }
+    }
+
+    // ── 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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(gauge);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, gauge);
+
+        assertThat(lines.get(0)).isEqualTo("# HELP app_info Application info");
+        assertThat(lines.get(1)).isEqualTo("# TYPE app_info gauge");
+        assertThat(lines.get(2)).isEqualTo("app_info 1.0");
+        assertThat(lines.size()).isEqualTo(3);
+    }
+
+    @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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(gauge);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, gauge);
+
+        assertThat(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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(counter);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, counter);
+
+        assertThat(lines.get(0)).isEqualTo("# HELP 
camel_exchanges_completed_total camel_exchanges_completed");
+        assertThat(lines.get(1)).isEqualTo("# TYPE 
camel_exchanges_completed_total counter");
+        assertThat(lines.get(2)).isEqualTo("camel_exchanges_completed_total 
42.0");
+        assertThat(lines.size()).isEqualTo(3);
+    }
+
+    // ── 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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(timer);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, timer);
+
+        assertThat(lines.get(0)).isEqualTo("# HELP 
camel_route_duration_seconds Route duration");
+        assertThat(lines.get(1)).isEqualTo("# TYPE 
camel_route_duration_seconds summary");
+        assertThat(lines.get(2)).isEqualTo("camel_route_duration_seconds_count 
10");
+        assertThat(lines.get(3)).isEqualTo("camel_route_duration_seconds_sum 
2.5");
+        assertThat(lines.get(4)).isEqualTo("camel_route_duration_seconds_max 
0.8");
+        assertThat(lines.size()).isEqualTo(5);
+    }
+
+    // ── 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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(ds);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, ds);
+
+        assertThat(lines.get(1)).isEqualTo("# TYPE payload_size summary");
+        assertThat(lines.get(2)).isEqualTo("payload_size_count 5");
+        assertThat(lines.get(3)).isEqualTo("payload_size_sum 500.0");
+        assertThat(lines.get(4)).isEqualTo("payload_size_max 200.0");
+    }
+
+    // ── 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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(fc);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, fc);
+
+        assertThat(lines).contains("# TYPE my_fc_total counter");
+        assertThat(lines).contains("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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(ltt);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, ltt);
+
+        assertThat(lines.get(0)).isEqualTo("# HELP camel_long_task_seconds 
Long running task");
+        assertThat(lines.get(1)).isEqualTo("# TYPE camel_long_task_seconds 
gauge");
+        assertThat(lines.get(3)).isEqualTo("camel_long_task_seconds_sum 12.5");
+        assertThat(lines.get(4)).isEqualTo("camel_long_task_seconds_max 6.0");
+        assertThat(lines).hasSize(5);
+    }
+
+    // ── 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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(ft);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, ft);
+
+        assertThat(lines).contains("my_ft_seconds_count 3.0");
+        assertThat(lines).contains("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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(gauge);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, gauge);
+
+        String valueLine = lines.get(2);
+        assertThat(valueLine)
+                .as("value line should carry labels")
+                .startsWith("app_info{")
+                .contains(
+                        "camel_version=\"4.0.0\"",
+                        "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);
+
+        String promName = 
AbstractMicrometerService.normalizePrometheusName(gauge);
+        List<String> lines = 
AbstractMicrometerService.convertMeterToPrometheusLines(promName, gauge);
+        String valueLine = lines.get(2);
+
+        assertThat(valueLine)
+                .as("value line should escape special characters")
+                .contains("\\\\", "\\\"", "\\n");
+    }
+}
diff --git 
a/core/camel-main/src/generated/java/org/apache/camel/main/MetricsConfigurationPropertiesConfigurer.java
 
b/core/camel-main/src/generated/java/org/apache/camel/main/MetricsConfigurationPropertiesConfigurer.java
index dafd87e4da31..fb7d451b591a 100644
--- 
a/core/camel-main/src/generated/java/org/apache/camel/main/MetricsConfigurationPropertiesConfigurer.java
+++ 
b/core/camel-main/src/generated/java/org/apache/camel/main/MetricsConfigurationPropertiesConfigurer.java
@@ -33,6 +33,7 @@ public class MetricsConfigurationPropertiesConfigurer extends 
org.apache.camel.s
         map.put("Enabled", boolean.class);
         map.put("LogMetricsOnShutdown", boolean.class);
         map.put("LogMetricsOnShutdownFilters", java.lang.String.class);
+        map.put("LogMetricsOnShutdownFormat", java.lang.String.class);
         map.put("NamingStrategy", java.lang.String.class);
         map.put("Path", java.lang.String.class);
         map.put("RoutePolicyLevel", java.lang.String.class);
@@ -65,6 +66,8 @@ public class MetricsConfigurationPropertiesConfigurer extends 
org.apache.camel.s
         case "logMetricsOnShutdown": 
target.setLogMetricsOnShutdown(property(camelContext, boolean.class, value)); 
return true;
         case "logmetricsonshutdownfilters":
         case "logMetricsOnShutdownFilters": 
target.setLogMetricsOnShutdownFilters(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "logmetricsonshutdownformat":
+        case "logMetricsOnShutdownFormat": 
target.setLogMetricsOnShutdownFormat(property(camelContext, 
java.lang.String.class, value)); return true;
         case "namingstrategy":
         case "namingStrategy": target.setNamingStrategy(property(camelContext, 
java.lang.String.class, value)); return true;
         case "path": target.setPath(property(camelContext, 
java.lang.String.class, value)); return true;
@@ -106,6 +109,8 @@ public class MetricsConfigurationPropertiesConfigurer 
extends org.apache.camel.s
         case "logMetricsOnShutdown": return boolean.class;
         case "logmetricsonshutdownfilters":
         case "logMetricsOnShutdownFilters": return java.lang.String.class;
+        case "logmetricsonshutdownformat":
+        case "logMetricsOnShutdownFormat": return java.lang.String.class;
         case "namingstrategy":
         case "namingStrategy": return java.lang.String.class;
         case "path": return java.lang.String.class;
@@ -143,6 +148,8 @@ public class MetricsConfigurationPropertiesConfigurer 
extends org.apache.camel.s
         case "logMetricsOnShutdown": return target.isLogMetricsOnShutdown();
         case "logmetricsonshutdownfilters":
         case "logMetricsOnShutdownFilters": return 
target.getLogMetricsOnShutdownFilters();
+        case "logmetricsonshutdownformat":
+        case "logMetricsOnShutdownFormat": return 
target.getLogMetricsOnShutdownFormat();
         case "namingstrategy":
         case "namingStrategy": return target.getNamingStrategy();
         case "path": return target.getPath();
diff --git 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index c03dd1557f00..1146c05cd1cc 100644
--- 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++ 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -334,6 +334,7 @@
     { "name": "camel.metrics.enableRoutePolicy", "required": false, 
"description": "Set whether to enable the MicrometerRoutePolicyFactory for 
capturing metrics on route processing times.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "boolean", 
"javaType": "boolean", "defaultValue": true, "secret": false },
     { "name": "camel.metrics.logMetricsOnShutdown", "required": false, 
"description": "Log metrics when application is shutting down. (default, 
false).", "sourceType": "org.apache.camel.main.MetricsConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
     { "name": "camel.metrics.logMetricsOnShutdownFilters", "required": false, 
"description": "List of metrics (comma separated) to log when application is 
shutting down. You can use character to log any metrics containing the 
wildcard, for example camel.exchanges. (default to all metrics available).", 
"sourceType": "org.apache.camel.main.MetricsConfigurationProperties", "type": 
"string", "javaType": "java.lang.String", "secret": false },
+    { "name": "camel.metrics.logMetricsOnShutdownFormat", "required": false, 
"description": "Format used to log metrics when application is shutting down. 
Either json (default) or prometheus format.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "json", "secret": false, 
"enum": [ "json", "prometheus" ] },
     { "name": "camel.metrics.namingStrategy", "required": false, 
"description": "Controls the name style to use for metrics. Default = uses 
micrometer naming convention. Legacy = uses the classic naming style 
(camelCase)", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, 
"enum": [ "default", "legacy" ] },
     { "name": "camel.metrics.path", "required": false, "description": "The 
path endpoint used to expose the metrics.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "string", 
"javaType": "java.lang.String", "defaultValue": "\/observe\/metrics", "secret": 
false },
     { "name": "camel.metrics.routePolicyLevel", "required": false, 
"description": "Sets the level of information to capture. all = both context 
and routes.", "sourceType": 
"org.apache.camel.main.MetricsConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "all", "secret": false, "enum": 
[ "all", "route", "context" ] },
diff --git a/core/camel-main/src/main/docs/main.adoc 
b/core/camel-main/src/main/docs/main.adoc
index db7bd0eb0a19..3fe96305eab4 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -646,7 +646,7 @@ The camel.mdc supports 3 options, which are listed below.
 
 
 === Camel Micrometer Metrics configurations
-The camel.metrics supports 16 options, which are listed below.
+The camel.metrics supports 17 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -662,6 +662,7 @@ The camel.metrics supports 16 options, which are listed 
below.
 | *camel.metrics.enableRoutePolicy* | Set whether to enable the 
MicrometerRoutePolicyFactory for capturing metrics on route processing times. | 
true | boolean
 | *camel.metrics.logMetricsOnShutdown* | Log metrics when application is 
shutting down. (default, false). | false | boolean
 | *camel.metrics.logMetricsOnShutdownFilters* | List of metrics (comma 
separated) to log when application is shutting down. You can use character to 
log any metrics containing the wildcard, for example camel.exchanges. (default 
to all metrics available). |  | String
+| *camel.metrics.logMetricsOnShutdownFormat* | Format used to log metrics when 
application is shutting down. Either json (default) or prometheus format. | 
json | String
 | *camel.metrics.namingStrategy* | Controls the name style to use for metrics. 
Default = uses micrometer naming convention. Legacy = uses the classic naming 
style (camelCase) | default | String
 | *camel.metrics.path* | The path endpoint used to expose the metrics. | 
/observe/metrics | String
 | *camel.metrics.routePolicyLevel* | Sets the level of information to capture. 
all = both context and routes. | all | String
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/MetricsConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/MetricsConfigurationProperties.java
index 79cb48c8cf83..a58bd6ee3f78 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/MetricsConfigurationProperties.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/MetricsConfigurationProperties.java
@@ -50,6 +50,8 @@ public class MetricsConfigurationProperties implements 
BootstrapCloseable {
     private boolean skipCamelInfo = false;
     @Metadata(defaultValue = "false")
     private boolean logMetricsOnShutdown = false;
+    @Metadata(defaultValue = "json", enums = "json,prometheus")
+    private String logMetricsOnShutdownFormat = "json";
     @Metadata(defaultValue = "0.0.4", enums = "0.0.4,1.0.0")
     private String textFormatVersion = "0.0.4";
     @Metadata
@@ -212,6 +214,17 @@ public class MetricsConfigurationProperties implements 
BootstrapCloseable {
         this.logMetricsOnShutdown = logMetricsOnShutdown;
     }
 
+    public String getLogMetricsOnShutdownFormat() {
+        return logMetricsOnShutdownFormat;
+    }
+
+    /**
+     * Format used to log metrics when application is shutting down. Either 
`json` (default) or `prometheus` format.
+     */
+    public void setLogMetricsOnShutdownFormat(String 
logMetricsOnShutdownFormat) {
+        this.logMetricsOnShutdownFormat = logMetricsOnShutdownFormat;
+    }
+
     public String getTextFormatVersion() {
         return textFormatVersion;
     }
@@ -366,6 +379,14 @@ public class MetricsConfigurationProperties implements 
BootstrapCloseable {
         return this;
     }
 
+    /**
+     * Format used to log metrics when application is shutting down. Either 
`json` (default) or `prometheus` format.
+     */
+    public MetricsConfigurationProperties 
withLogMetricsOnShutdownFormat(String logMetricsOnShutdownFormat) {
+        this.logMetricsOnShutdownFormat = logMetricsOnShutdownFormat;
+        return this;
+    }
+
     /**
      * The text-format version to use with Prometheus scraping.
      *

Reply via email to