GERONIMO-6654 simple way to override prometheus export
diff --git a/README.adoc b/README.adoc
index b001d51..d3a7030 100644
--- a/README.adoc
+++ b/README.adoc
@@ -10,3 +10,23 @@
   <version>${metrics.version}</version>
 </parent>
 ----
+
+
+== Renaming Prometheus keys
+
+Prometheus exporter can rename metrics keys providing
+a properties file in the classpath as resource `META-INF/geronimo-metrics/prometheus-mapping.properties`
+or using system properties (with the prefix `geronimo.metrics.prometheus.mapping.`).
+
+The key is the metric key (with the registry type as prefix) and the value the replacement key:
+
+[source]
+----
+base\:my_metric = app:metric
+----
+
+TIP: since it is a properties format the `:` must be escaped.
+
+The specific key `geronimo.metrics.filter.prefix` can take
+a list (comma separated values) of metrics prefixes to filter (whitelist)
+exported metrics.
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
index 32f0315..c2fdec4 100644
--- a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/jaxrs/MetricsEndpoints.java
@@ -48,7 +48,7 @@
     private MetricRegistry vendorRegistry;
     private MetricRegistry applicationRegistry;
 
-    private final PrometheusFormatter prometheus = new PrometheusFormatter();
+    private final PrometheusFormatter prometheus = new PrometheusFormatter().enableOverriding();
 
     public void setBaseRegistry(final MetricRegistry baseRegistry) {
         this.baseRegistry = baseRegistry;
diff --git a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
index afd1e96..f9f0b3f 100644
--- a/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
+++ b/geronimo-metrics-common/src/main/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatter.java
@@ -21,9 +21,16 @@
 import static java.util.Locale.ROOT;
 import static java.util.Optional.of;
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
 import org.eclipse.microprofile.metrics.Counter;
@@ -40,62 +47,104 @@
 // this is so weird to have this format built-in but not mainstream ones,
 // todo: pby make it dropped from the spec
 // note: this is a simplified serialization flavor and it can need some more love
+// todo: cache all the keys, can easily be done decorating the registry and enriching metadata (ExtendedMetadata)
 public class PrometheusFormatter {
+    private final Map<String, String> keyMapping = new HashMap<>();
+    private Predicate<String> prefixFilter = null;
+
+    public PrometheusFormatter enableOverriding(final Properties properties) {
+        properties.stringPropertyNames().forEach(k -> keyMapping.put(k, properties.getProperty(k)));
+        afterOverride();
+        return this;
+    }
+
+    public PrometheusFormatter enableOverriding() {
+        try (final InputStream source = Thread.currentThread().getContextClassLoader()
+                                              .getResourceAsStream("META-INF/geronimo-metrics/prometheus-mapping.properties")) {
+            if (source != null) {
+                final Properties properties = new Properties();
+                properties.load(source);
+                enableOverriding(properties);
+            }
+        } catch (final IOException e) {
+            // no-op
+        }
+        System.getProperties().stringPropertyNames().stream()
+              .filter(it -> it.startsWith("geronimo.metrics.prometheus.mapping."))
+              .forEach(k -> keyMapping.put(k.substring("geronimo.metrics.prometheus.mapping.".length()), System.getProperty(k)));
+        afterOverride();
+        return this;
+    }
+
+    private void afterOverride() {
+        final String prefix = keyMapping.get("geronimo.metrics.filter.prefix");
+        if (prefix == null) {
+            prefixFilter = null;
+        } else {
+            final List<String> prefixes = Stream.of(prefix.split(","))
+                                               .map(String::trim)
+                                               .filter(it -> !it.isEmpty())
+                                               .collect(toList());
+            final Predicate<String> directPredicate = name -> prefixes.stream().anyMatch(name::startsWith);
+            prefixFilter = name -> directPredicate.test(name) || directPredicate.test(keyMapping.getOrDefault(name, name));
+        }
+    }
+
     public StringBuilder toText(final MetricRegistry registry,
                                 final String registryKey,
                                 final Map<String, Metric> entries) {
         final Map<String, Metadata> metadatas = registry.getMetadata();
         return entries.entrySet().stream()
+                .map(it -> new Entry(metadatas.get(it.getKey()), registryKey + ':' + toPrometheusKey(metadatas.get(it.getKey())), it.getValue()))
+                .filter(it -> prefixFilter == null || prefixFilter.test(it.prometheusKey))
                 .map(entry -> {
-                    final Metadata metadata = metadatas.get(entry.getKey());
-                    final Metric value = entry.getValue();
-                    switch (metadata.getTypeRaw()) {
+                    switch (entry.metadata.getTypeRaw()) {
                         case COUNTER: {
-                            final String key = toPrometheusKey(metadata);
+                            final String key = toPrometheusKey(entry.metadata);
                             return new StringBuilder()
-                                    .append(value(registryKey, key, Counter.class.cast(value).getCount(), metadata));
+                                    .append(value(registryKey, key, Counter.class.cast(entry.metric).getCount(), entry.metadata));
                         }
                         case GAUGE: {
-                            final Object val = Gauge.class.cast(value).getValue();
+                            final Object val = Gauge.class.cast(entry.metric).getValue();
                             if (Number.class.isInstance(val)) {
-                                final String key = toPrometheusKey(metadata);
+                                final String key = toPrometheusKey(entry.metadata);
                                 return new StringBuilder()
-                                        .append(value(registryKey, key, Number.class.cast(val).doubleValue(), metadata));
+                                        .append(value(registryKey, key, Number.class.cast(val).doubleValue(), entry.metadata));
                             }
                             return new StringBuilder();
                         }
                         case METERED: {
-                            final String keyBase = toPrometheus(metadata);
-                            final String key = keyBase + toUnitSuffix(metadata);
-                            final Meter meter = Meter.class.cast(value);
+                            final String keyBase = toPrometheus(entry.metadata);
+                            final String key = keyBase + toUnitSuffix(entry.metadata);
+                            final Meter meter = Meter.class.cast(entry.metric);
                             return new StringBuilder()
-                                    .append(value(registryKey, key + "_count", meter.getCount(), metadata))
-                                    .append(value(registryKey, keyBase + "_rate_per_second", meter.getMeanRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_one_min_rate_per_second", meter.getOneMinuteRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_five_min_rate_per_second", meter.getFiveMinuteRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", meter.getFifteenMinuteRate(), metadata));
+                                    .append(value(registryKey, key + "_count", meter.getCount(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_rate_per_second", meter.getMeanRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_one_min_rate_per_second", meter.getOneMinuteRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_five_min_rate_per_second", meter.getFiveMinuteRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", meter.getFifteenMinuteRate(), entry.metadata));
                         }
                         case TIMER: {
-                            final String keyBase = toPrometheus(metadata);
-                            final String keyUnit = toUnitSuffix(metadata);
-                            final Timer timer = Timer.class.cast(value);
+                            final String keyBase = toPrometheus(entry.metadata);
+                            final String keyUnit = toUnitSuffix(entry.metadata);
+                            final Timer timer = Timer.class.cast(entry.metric);
                             return new StringBuilder()
-                                    .append(type(registryKey, keyBase + keyUnit + " summary", metadata))
-                                    .append(value(registryKey, keyBase + keyUnit + "_count", timer.getCount(), metadata))
-                                    .append(value(registryKey, keyBase + "_rate_per_second", timer.getMeanRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_one_min_rate_per_second", timer.getOneMinuteRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_five_min_rate_per_second", timer.getFiveMinuteRate(), metadata))
-                                    .append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", timer.getFifteenMinuteRate(), metadata))
-                                    .append(toPrometheus(registryKey, keyBase, keyUnit, timer.getSnapshot(), metadata));
+                                    .append(type(registryKey, keyBase + keyUnit + " summary", entry.metadata))
+                                    .append(value(registryKey, keyBase + keyUnit + "_count", timer.getCount(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_rate_per_second", timer.getMeanRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_one_min_rate_per_second", timer.getOneMinuteRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_five_min_rate_per_second", timer.getFiveMinuteRate(), entry.metadata))
+                                    .append(value(registryKey, keyBase + "_fifteen_min_rate_per_second", timer.getFifteenMinuteRate(), entry.metadata))
+                                    .append(toPrometheus(registryKey, keyBase, keyUnit, timer.getSnapshot(), entry.metadata));
                         }
                         case HISTOGRAM:
-                            final String keyBase = toPrometheus(metadata);
-                            final String keyUnit = toUnitSuffix(metadata);
-                            final Histogram histogram = Histogram.class.cast(value);
+                            final String keyBase = toPrometheus(entry.metadata);
+                            final String keyUnit = toUnitSuffix(entry.metadata);
+                            final Histogram histogram = Histogram.class.cast(entry.metric);
                             return new StringBuilder()
-                                    .append(type(registryKey, keyBase + keyUnit + " summary", metadata))
-                                    .append(value(registryKey, keyBase + keyUnit + "_count", histogram.getCount(), metadata))
-                                    .append(toPrometheus(registryKey, keyBase, keyUnit, histogram.getSnapshot(), metadata));
+                                    .append(type(registryKey, keyBase + keyUnit + " summary", entry.metadata))
+                                    .append(value(registryKey, keyBase + keyUnit + "_count", histogram.getCount(), entry.metadata))
+                                    .append(toPrometheus(registryKey, keyBase, keyUnit, histogram.getSnapshot(), entry.metadata));
                         default:
                             return new StringBuilder();
                     }
@@ -133,9 +182,10 @@
 
     private StringBuilder value(final String registryKey, final String key, final double value,
                                 final Metadata metadata) {
+        final String builtKey = registryKey + ':' + key;
         return new StringBuilder()
                 .append(type(registryKey, key, metadata))
-                .append(registryKey).append(':').append(key)
+                .append(keyMapping.getOrDefault(builtKey, builtKey))
                 .append(of(metadata.getTags())
                         .filter(t -> !t.isEmpty())
                         .map(t -> t.entrySet().stream()
@@ -146,8 +196,9 @@
     }
 
     private StringBuilder type(final String registryKey, final String key, final Metadata metadata) {
+        final String builtKey = registryKey + ':' + key;
         final StringBuilder builder = new StringBuilder()
-                .append("# TYPE ").append(registryKey).append(':').append(key);
+                .append("# TYPE ").append(keyMapping.getOrDefault(builtKey, builtKey));
         if (metadata != null) {
             builder.append(' ').append(metadata.getType());
         }
@@ -240,4 +291,16 @@
                 .replace(":_", ":")
                 .toLowerCase(ROOT);
     }
+
+    private static class Entry {
+        private final Metadata metadata;
+        private final String prometheusKey;
+        private final Metric metric;
+
+        private Entry(final Metadata metadata, final String prometheusKey, final Metric metric) {
+            this.metadata = metadata;
+            this.prometheusKey = prometheusKey;
+            this.metric = metric;
+        }
+    }
 }
diff --git a/geronimo-metrics-common/src/test/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatterTest.java b/geronimo-metrics-common/src/test/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatterTest.java
new file mode 100644
index 0000000..88d3595
--- /dev/null
+++ b/geronimo-metrics-common/src/test/java/org/apache/geronimo/microprofile/metrics/common/prometheus/PrometheusFormatterTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.geronimo.microprofile.metrics.common.prometheus;
+
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.geronimo.microprofile.metrics.common.RegistryImpl;
+import org.eclipse.microprofile.metrics.Gauge;
+import org.eclipse.microprofile.metrics.Metric;
+import org.junit.Test;
+
+public class PrometheusFormatterTest {
+    @Test
+    public void rename() {
+        final PrometheusFormatter prometheusFormatter = new PrometheusFormatter().enableOverriding();
+        final RegistryImpl registry = new RegistryImpl();
+        final Map<String, Metric> metrics = singletonMap("myMetric", (Gauge<Long>) () -> 1234L);
+        metrics.forEach(registry::register);
+        assertEquals(
+                "# TYPE sample:my_metric gauge\nsample:my_metric 1234.0\n",
+                prometheusFormatter.toText(registry, "sample", metrics).toString());
+        System.setProperty("geronimo.metrics.prometheus.mapping.sample:my_metric", "renamed");
+        prometheusFormatter.enableOverriding();
+        assertEquals(
+                "# TYPE renamed gauge\nrenamed 1234.0\n",
+                prometheusFormatter.toText(registry, "sample", metrics).toString());
+        System.clearProperty("sample:my_metric");
+        System.setProperty("geronimo.metrics.prometheus.mapping.sample:my_metric", "renamed");
+        prometheusFormatter.enableOverriding(new Properties() {{
+            setProperty("sample:my_metric", "again");
+        }});
+        assertEquals(
+                "# TYPE again gauge\nagain 1234.0\n",
+                prometheusFormatter.toText(registry, "sample", metrics).toString());
+    }
+
+    @Test
+    public void filter() {
+        final PrometheusFormatter prometheusFormatter = new PrometheusFormatter().enableOverriding();
+        final RegistryImpl registry = new RegistryImpl();
+        final Map<String, Metric> metrics = new LinkedHashMap<>();
+        metrics.put("myMetric1", (Gauge<Long>) () -> 1234L);
+        metrics.put("myMetric2", (Gauge<Long>) () -> 1235L);
+        metrics.forEach(registry::register);
+        assertEquals(
+                "# TYPE sample:my_metric1 gauge\nsample:my_metric1 1234.0\n" +
+                        "# TYPE sample:my_metric2 gauge\nsample:my_metric2 1235.0\n",
+                prometheusFormatter.toText(registry, "sample", metrics).toString());
+        prometheusFormatter.enableOverriding(new Properties() {{
+            setProperty("geronimo.metrics.filter.prefix", "sample:my_metric2");
+        }});
+        assertEquals(
+                "# TYPE sample:my_metric2 gauge\nsample:my_metric2 1235.0\n",
+                prometheusFormatter.toText(registry, "sample", metrics).toString());
+    }
+}