You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/14 18:10:48 UTC

(hugegraph) branch master updated: fix(server): sanitize Prometheus metric names (#3344)

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

imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git


The following commit(s) were added to refs/heads/master by this push:
     new e1d9c696c fix(server): sanitize Prometheus metric names (#3344)
e1d9c696c is described below

commit e1d9c696c1611925f904d9d05cf39d0e058a4865
Author: Arshil <ar...@gmail.com>
AuthorDate: Wed Oct 14 23:40:43 2026 +0530

    fix(server): sanitize Prometheus metric names (#3344)
    
    Cache metric keys such as "...Cache.schema~DEFAULT-hugegraph.hits" kept
    the '~' when written to /metrics, and Prometheus rejected the whole
    scrape. Each of the two name helpers stripped a fixed set of
    characters, so any character outside that set needed its own patch.
---
 .../apache/hugegraph/api/metrics/MetricsAPI.java   |  13 ++-
 .../org/apache/hugegraph/metrics/MetricsUtil.java  |  35 +++++--
 .../org/apache/hugegraph/unit/UnitTestSuite.java   |   2 +
 .../hugegraph/unit/util/MetricsUtilTest.java       | 104 +++++++++++++++++++++
 4 files changed, 137 insertions(+), 17 deletions(-)

diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java
index b457b66bf..1357a8558 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/metrics/MetricsAPI.java
@@ -40,8 +40,7 @@ import static org.apache.hugegraph.metrics.MetricsUtil.STR_TYPE;
 import static org.apache.hugegraph.metrics.MetricsUtil.UNTYPED;
 import static org.apache.hugegraph.metrics.MetricsUtil.VERSION_STR;
 import static org.apache.hugegraph.metrics.MetricsUtil.exportSnapshot;
-import static org.apache.hugegraph.metrics.MetricsUtil.replaceDotDashInKey;
-import static org.apache.hugegraph.metrics.MetricsUtil.replaceSlashInKey;
+import static org.apache.hugegraph.metrics.MetricsUtil.sanitizePrometheusMetricName;
 
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -252,7 +251,7 @@ public class MetricsAPI extends API {
             final Gauge<?> gauge
                     = reporter.gauges().get(key);
             if (gauge != null) {
-                helpName = replaceDotDashInKey(key);
+                helpName = sanitizePrometheusMetricName(key);
                 promMetric.append(STR_HELP)
                           .append(helpName).append(END_LSTR);
                 promMetric.append(STR_TYPE)
@@ -266,7 +265,7 @@ public class MetricsAPI extends API {
         for (String histogramkey : reporter.histograms().keySet()) {
             final Histogram histogram = reporter.histograms().get(histogramkey);
             if (histogram != null) {
-                helpName = replaceDotDashInKey(histogramkey);
+                helpName = sanitizePrometheusMetricName(histogramkey);
                 promMetric.append(STR_HELP)
                           .append(helpName).append(END_LSTR);
                 promMetric.append(STR_TYPE)
@@ -285,7 +284,7 @@ public class MetricsAPI extends API {
         for (String meterkey : reporter.meters().keySet()) {
             final Meter metric = reporter.meters().get(meterkey);
             if (metric != null) {
-                helpName = replaceDotDashInKey(meterkey);
+                helpName = sanitizePrometheusMetricName(meterkey);
                 promMetric.append(STR_HELP)
                           .append(helpName).append(END_LSTR);
                 promMetric.append(STR_TYPE)
@@ -315,7 +314,7 @@ public class MetricsAPI extends API {
             final com.codahale.metrics.Timer timer = reporter.timers()
                                                              .get(timerkey);
             if (timer != null) {
-                helpName = replaceDotDashInKey(timerkey);
+                helpName = sanitizePrometheusMetricName(timerkey);
                 promMetric.append(STR_HELP)
                           .append(helpName).append(END_LSTR);
                 promMetric.append(STR_TYPE)
@@ -412,7 +411,7 @@ public class MetricsAPI extends API {
                   .append(SPACE_STR + "1.0" + END_LSTR);
 
         for (String methodKey : metricMap.keySet()) {
-            String metricName = replaceSlashInKey(methodKey);
+            String metricName = sanitizePrometheusMetricName(methodKey);
             promMetric.append(STR_HELP)
                       .append(metricName).append(END_LSTR);
             promMetric.append(STR_TYPE)
diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java
index 3721cb1e1..de32a7dba 100644
--- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java
+++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/metrics/MetricsUtil.java
@@ -17,6 +17,8 @@
 
 package org.apache.hugegraph.metrics;
 
+import java.util.regex.Pattern;
+
 import org.apache.tinkerpop.gremlin.server.util.MetricManager;
 
 import com.codahale.metrics.Counter;
@@ -63,6 +65,9 @@ public class MetricsUtil {
     public static final String RIGHT_NAME_STR = ",} ";
     public static final String PROM_HELP_NAME = "hugegraph_info";
 
+    private static final Pattern INVALID_PROMETHEUS_NAME_CHAR =
+            Pattern.compile("[^a-zA-Z0-9_:]");
+
     public static <T> Gauge<T> registerGauge(Class<?> clazz, String name,
                                              Gauge<T> gauge) {
         return REGISTRY.register(MetricRegistry.name(clazz, name), gauge);
@@ -92,19 +97,29 @@ public class MetricsUtil {
         return REGISTRY.timer(MetricRegistry.name(clazz, name));
     }
 
-    public static String replaceDotDashInKey(String orgKey) {
-        return orgKey.replace(".", "_").replace("-", "_").replace("/", "_").replace("$", "_");
-    }
-
-    public static String replaceSlashInKey(String orgKey) {
-        return orgKey.replace("/", "_");
+    /**
+     * Convert a metric key into a valid Prometheus metric name, which must
+     * match [a-zA-Z_:][a-zA-Z0-9_:]*. Every other character becomes '_',
+     * and a leading digit gets a '_' prefix. Only call this when writing
+     * Prometheus output; registry keys stay unchanged.
+     */
+    public static String sanitizePrometheusMetricName(String originalName) {
+        String sanitized = INVALID_PROMETHEUS_NAME_CHAR.matcher(originalName)
+                                                       .replaceAll("_");
+        if (sanitized.isEmpty()) {
+            return "_";
+        }
+        if (Character.isDigit(sanitized.charAt(0))) {
+            return "_" + sanitized;
+        }
+        return sanitized;
     }
 
     public static void writePrometheusFormat(StringBuilder promeMetrics, MetricRegistry registry) {
         // gauges
         registry.getGauges().forEach((key, gauge) -> {
             if (gauge != null) {
-                String helpName = replaceDotDashInKey(key);
+                String helpName = sanitizePrometheusMetricName(key);
                 promeMetrics.append(STR_HELP)
                             .append(helpName).append(END_LSTR);
                 promeMetrics.append(STR_TYPE)
@@ -117,7 +132,7 @@ public class MetricsUtil {
         // histograms
         registry.getHistograms().forEach((key, histogram) -> {
             if (histogram != null) {
-                String helpName = replaceDotDashInKey(key);
+                String helpName = sanitizePrometheusMetricName(key);
                 promeMetrics.append(STR_HELP)
                             .append(helpName).append(END_LSTR);
                 promeMetrics.append(STR_TYPE)
@@ -134,7 +149,7 @@ public class MetricsUtil {
         // meters
         registry.getMeters().forEach((key, metric) -> {
             if (metric != null) {
-                String helpName = replaceDotDashInKey(key);
+                String helpName = sanitizePrometheusMetricName(key);
                 promeMetrics.append(STR_HELP)
                             .append(helpName).append(END_LSTR);
                 promeMetrics.append(STR_TYPE)
@@ -160,7 +175,7 @@ public class MetricsUtil {
         // timer
         registry.getTimers().forEach((key, timer) -> {
             if (timer != null) {
-                String helpName = replaceDotDashInKey(key);
+                String helpName = sanitizePrometheusMetricName(key);
                 promeMetrics.append(STR_HELP)
                             .append(helpName).append(END_LSTR);
                 promeMetrics.append(STR_TYPE)
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
index 3cd65b0d8..59b025c66 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java
@@ -123,6 +123,7 @@ import org.apache.hugegraph.unit.traversal.ShortestPathTraverserTest;
 import org.apache.hugegraph.unit.util.CompressUtilTest;
 import org.apache.hugegraph.unit.util.ConsumersTest;
 import org.apache.hugegraph.unit.util.JsonUtilTest;
+import org.apache.hugegraph.unit.util.MetricsUtilTest;
 import org.apache.hugegraph.unit.util.RateLimiterTest;
 import org.apache.hugegraph.unit.util.StringEncodingTest;
 import org.apache.hugegraph.unit.util.VersionTest;
@@ -270,6 +271,7 @@ import org.junit.runners.Suite;
         JsonUtilTest.class,
         StringEncodingTest.class,
         CompressUtilTest.class,
+        MetricsUtilTest.class,
         RateLimiterTest.FixedTimerWindowRateLimiterTest.class,
         RateLimiterTest.FixedWatchWindowRateLimiterTest.class,
 
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.java
new file mode 100644
index 000000000..dfb515910
--- /dev/null
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/util/MetricsUtilTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.hugegraph.unit.util;
+
+import static org.apache.hugegraph.metrics.MetricsUtil.sanitizePrometheusMetricName;
+
+import java.util.regex.Pattern;
+
+import org.apache.hugegraph.metrics.MetricsUtil;
+import org.apache.hugegraph.testutil.Assert;
+import org.junit.Test;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+
+public class MetricsUtilTest {
+
+    private static final Pattern VALID_NAME =
+            Pattern.compile("[a-zA-Z_:][a-zA-Z0-9_:]*");
+
+    @Test
+    public void testSanitizeReplacesInvalidChars() {
+        Assert.assertEquals("a_b_c_d_e_f",
+                            sanitizePrometheusMetricName("a.b-c/d$e~f"));
+        Assert.assertEquals("org_apache_Cache_schema_DEFAULT_hugegraph_hits",
+                            sanitizePrometheusMetricName(
+                                    "org.apache.Cache.schema~DEFAULT-hugegraph.hits"));
+        Assert.assertEquals("_graphs_1_x",
+                            sanitizePrometheusMetricName("/graphs/1~x"));
+    }
+
+    @Test
+    public void testSanitizeKeepsValidName() {
+        Assert.assertEquals("valid_name:total",
+                            sanitizePrometheusMetricName("valid_name:total"));
+        Assert.assertEquals("Mixed_Case_09",
+                            sanitizePrometheusMetricName("Mixed_Case_09"));
+    }
+
+    @Test
+    public void testSanitizePrefixesLeadingDigit() {
+        Assert.assertEquals("_9cache_hits",
+                            sanitizePrometheusMetricName("9cache.hits"));
+    }
+
+    @Test
+    public void testSanitizeEmptyName() {
+        Assert.assertEquals("_", sanitizePrometheusMetricName(""));
+    }
+
+    @Test
+    public void testWritePrometheusFormatWithTildeInGaugeName() {
+        MetricRegistry registry = new MetricRegistry();
+        String key = "org.apache.hugegraph.Cache.schema~DEFAULT-hugegraph.hits";
+        registry.register(key, (Gauge<Long>) () -> 42L);
+
+        StringBuilder output = new StringBuilder();
+        MetricsUtil.writePrometheusFormat(output, registry);
+        String text = output.toString();
+
+        String name = "org_apache_hugegraph_Cache_schema_DEFAULT_hugegraph_hits";
+        Assert.assertFalse(text, text.contains("~"));
+        Assert.assertEquals("# HELP " + name + "\n" +
+                            "# TYPE " + name + " gauge\n" +
+                            name + " 42\n", text);
+    }
+
+    @Test
+    public void testWritePrometheusFormatProducesValidNames() {
+        MetricRegistry registry = new MetricRegistry();
+        registry.register("cache~a.size", (Gauge<Integer>) () -> 1);
+        registry.histogram("api/graphs/1~x");
+        registry.meter("9meter-rate");
+        registry.timer("timer$latency~p");
+
+        StringBuilder output = new StringBuilder();
+        MetricsUtil.writePrometheusFormat(output, registry);
+
+        for (String line : output.toString().split("\n")) {
+            String name;
+            if (line.startsWith("# HELP ") || line.startsWith("# TYPE ")) {
+                name = line.substring(7).split(" ")[0];
+            } else {
+                name = line.split("[{ ]")[0];
+            }
+            Assert.assertTrue(line, VALID_NAME.matcher(name).matches());
+        }
+    }
+}