Repository: apache/hertzbeat (language: java)

Issue:
[BUG] The Prometheus metric response data parsing is abnormal
### Is there an existing issue for this?

- [x] I have searched the existing issues

### Current Behavior

在添加 Prometheus 监控时，`OnlineParser # parseLabels`解析字节流异常，造成的异常为响应指标值返回格式并不统一，部分指标返回 `xxx_xxx_xxx{key=value,}` 后缀带有 `,` ，导致无法正确退出循环，解析抛出异常，进而导致新增监控宕机。
```java
  if (i == '}') {
        break;
  }
```
这个报错监控的 Prometheus 指标示例如下：
```
# HELP jvm_gc_pause_seconds Time spent in GC pause
# TYPE jvm_gc_pause_seconds summary
jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.139
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.02
jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 5.0
jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.082
```

### Expected Behavior

正确并成功添加 PrometheusAuto 监控

### Steps To Reproduce

Prometheus 响应指标值格式如：`xxx_xxx_xxx{key=value,}` ，复现该错误

### Environment

```markdown
HertzBeat version(s): 1.7.x master分支
```

### Debug logs
```java
ERROR org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser Line:71  - prometheus parser failed because of wrong input format. null
ERROR org.apache.hertzbeat.collector.dispatch.MetricsCollect Line:452 - [Collect Failed] Response metrics data is null.
```



Discussion / hints:

Fix patch:
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
index 7152ff0ce8b..cfc87527ce3 100644
--- a/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/main/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParser.java
@@ -38,7 +38,10 @@
 public class OnlineParser {
 
     private static final Map<Integer, Integer> escapeMap = new HashMap<>(8);
-    
+
+    private static final char RIGHT_BRACKET = '}';
+    private static final char LEFT_BRACKET = '{';
+
     static {
         escapeMap.put((int) 'n', (int) '\n');
         escapeMap.put((int) 'b', (int) '\b');
@@ -94,14 +97,14 @@ private static class CharChecker {
         }
 
         private CharChecker maybeLeftBracket() {
-            if (i == '{') {
+            if (i == LEFT_BRACKET) {
                 satisfied = true;
             }
             return this;
         }
 
         private CharChecker maybeRightBracket() {
-            if (i == '}') {
+            if (i == RIGHT_BRACKET) {
                 satisfied = true;
             }
             return this;
@@ -264,35 +267,52 @@ private static Double toDouble(String string) throws FormatException {
         }
     }
 
-    private static CharChecker parseLabels(InputStream inputStream, StringBuilder stringBuilder, List<MetricFamily.Label> labelList) throws IOException, FormatException {
+
+    /**
+     * parse single label like  label_name="label_value"
+     */
+    private static CharChecker parseLabel(InputStream inputStream, StringBuilder stringBuilder, List<MetricFamily.Label> labelList) throws IOException, FormatException {
         int i;
-        while (true) {
-            MetricFamily.Label label = new MetricFamily.Label();
-            i = skipSpaces(inputStream).getInt();
-            stringBuilder.append((char) i);
-            i = parseLabelName(inputStream, stringBuilder).maybeSpace().maybeEqualsSign().noElse();
-            label.setName(stringBuilder.toString());
-            stringBuilder.delete(0, stringBuilder.length());
-            if (i == ' ') {
-                skipSpaces(inputStream).maybeEqualsSign().noElse();
-            }
+        MetricFamily.Label label = new MetricFamily.Label();
+        i = skipSpaces(inputStream).getInt();
+        if (i == RIGHT_BRACKET) {
+            return new CharChecker(i);
+        }
+        stringBuilder.append((char) i);
+        i = parseLabelName(inputStream, stringBuilder).maybeSpace().maybeEqualsSign().noElse();
+        label.setName(stringBuilder.toString());
+        stringBuilder.delete(0, stringBuilder.length());
+        if (i == ' ') {
+            skipSpaces(inputStream).maybeEqualsSign().noElse();
+        }
+
+        skipSpaces(inputStream).maybeQuotationMark().noElse();
+        parseLabelValue(inputStream, stringBuilder).maybeQuotationMark().noElse();
+        String labelValue = stringBuilder.toString();
+        if (!labelValue.equals(new String(labelValue.getBytes(StandardCharsets.UTF_8)))) {
+            throw new FormatException();
+        }
+        label.setValue(labelValue);
+        stringBuilder.delete(0, stringBuilder.length());
+        labelList.add(label);
+        return new CharChecker(i);
+    }
 
-            skipSpaces(inputStream).maybeQuotationMark().noElse();
-            parseLabelValue(inputStream, stringBuilder).maybeQuotationMark().noElse();
-            String labelValue = stringBuilder.toString();
-            if (!labelValue.equals(new String(labelValue.getBytes(StandardCharsets.UTF_8)))) {
-                throw new FormatException();
-            }
-            label.setValue(labelValue);
-            stringBuilder.delete(0, stringBuilder.length());
 
+    private static void parseLabels(InputStream inputStream, StringBuilder stringBuilder, List<MetricFamily.Label> labelList) throws IOException, FormatException {
+        int i;
+        while (true) {
+            // deal with labels like {label1="aaa",label2=bbb",}
+            CharChecker charChecker = parseLabel(inputStream, stringBuilder, labelList);
+            if (charChecker.i == RIGHT_BRACKET) {
+                return;
+            }
+            //deal with labels like {label1="aaa",label2=bbb"}
             i = skipSpaces(inputStream).maybeSpace().maybeComma().maybeRightBracket().noElse();
-            labelList.add(label);
-            if (i == '}') {
-                break;
+            if (i == RIGHT_BRACKET) {
+                return;
             }
         }
-        return new CharChecker(i);
     }
 
     private static CharChecker parseMetric(InputStream inputStream, Map<String, MetricFamily> metricFamilyMap, StringBuilder stringBuilder) throws IOException, FormatException {


Test patch:
diff --git a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
index 161b9a2431c..cac93020611 100644
--- a/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
+++ b/hertzbeat-collector/hertzbeat-collector-basic/src/test/java/org/apache/hertzbeat/collector/collect/prometheus/parser/OnlineParserTest.java
@@ -48,6 +48,13 @@ void parseMetrics2() throws Exception {
         String str = """
                 # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
                 # TYPE go_gc_duration_seconds summary
+                jvm_gc_pause_seconds_count{action="end of major GC",cause="Metadata GC Threshold",} 1.0
+                jvm_gc_pause_seconds_sum{action="end of major GC",cause="Metadata GC Threshold",} 0.139
+                jvm_gc_pause_seconds_count{action="end of minor GC",cause="Metadata GC Threshold",} 1.0
+                jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Metadata GC Threshold",} 0.02
+                jvm_gc_pause_seconds_count{action="end of minor GC",cause="Allocation Failure",} 5.0
+                jvm_gc_pause_seconds_sum{action="end of minor GC",cause="Allocation Failure",} 0.082
+                go_gc_duration_seconds{quantile="0"} 2.0209e-05
                 go_gc_duration_seconds{quantile="0"} 2.0209e-05
                 go_gc_duration_seconds{quantile="0.25"} 6.6917e-05
                 go_gc_duration_seconds{quantile="0.5"} -Inf
@@ -87,7 +94,6 @@ void parseMetrics2() throws Exception {
                 fail("parse failed, different result from two parser.");
             }
             MetricFamily metricFamily1 = metricFamilyMap1.get(metricFamilyName);
-            assertEquals(metricFamily1.getName(), metricFamily1.getName());
             Set<Double> metricValueSet = metricFamily2.getMetricList().stream().map(MetricFamily.Metric::getValue).collect(Collectors.toSet());
             metricFamily1.getMetricList().forEach(metric -> {
                 // this is for something different between two algorithms above, and both of them is current on this parsing behavior.
@@ -99,4 +105,4 @@ void parseMetrics2() throws Exception {
             });
         });
     }
-}
\ No newline at end of file
+}