Repository: prometheus/common
Pull request #691 (2024-09-05 13:19:43)

fix(utf8): Fix multiple metric name inside braces validation
There's a bug where metrics inside braces in different lines are mistakenly considered as multiple metric names in the same line when there's no TYPE or HELP line. This PR addresses the issue.

Related to https://github.com/prometheus/common/pull/670


Patch:
diff --git a/expfmt/text_parse.go b/expfmt/text_parse.go
index 25db4f2..f085a92 100644
--- a/expfmt/text_parse.go
+++ b/expfmt/text_parse.go
@@ -75,7 +75,9 @@ type TextParser struct {
 	// count and sum of that summary/histogram.
 	currentIsSummaryCount, currentIsSummarySum     bool
 	currentIsHistogramCount, currentIsHistogramSum bool
-	currentMetricIsInsideBraces                    bool
+	// These indicate if the metric name from the current line being parsed is inside
+	// braces and if that metric name was found respectively.
+	currentMetricIsInsideBraces, currentMetricInsideBracesIsPresent bool
 }
 
 // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
@@ -147,6 +149,7 @@ func (p *TextParser) reset(in io.Reader) {
 func (p *TextParser) startOfLine() stateFn {
 	p.lineCount++
 	p.currentMetricIsInsideBraces = false
+	p.currentMetricInsideBracesIsPresent = false
 	if p.skipBlankTab(); p.err != nil {
 		// This is the only place that we expect to see io.EOF,
 		// which is not an error but the signal that we are done.
@@ -301,17 +304,24 @@ func (p *TextParser) startLabelName() stateFn {
 	}
 	if p.currentByte != '=' {
 		if p.currentMetricIsInsideBraces {
-			if p.currentMF != nil && p.currentMF.GetName() != p.currentToken.String() {
-				p.parseError(fmt.Sprintf("multiple metric names %s %s", p.currentMF.GetName(), p.currentToken.String()))
+			if p.currentMetricInsideBracesIsPresent {
+				p.parseError(fmt.Sprintf("multiple metric names for metric %q", p.currentMF.GetName()))
 				return nil
 			}
 			switch p.currentByte {
 			case ',':
 				p.setOrCreateCurrentMF()
+				if p.currentMF.Type == nil {
+					p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
+				}
 				p.currentMetric = &dto.Metric{}
+				p.currentMetricInsideBracesIsPresent = true
 				return p.startLabelName
 			case '}':
 				p.setOrCreateCurrentMF()
+				if p.currentMF.Type == nil {
+					p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
+				}
 				p.currentMetric = &dto.Metric{}
 				p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...)
 				p.currentLabelPairs = nil


Test patch:
diff --git a/expfmt/text_parse_test.go b/expfmt/text_parse_test.go
index 1ddba29..fac60ba 100644
--- a/expfmt/text_parse_test.go
+++ b/expfmt/text_parse_test.go
@@ -594,6 +594,49 @@ request_duration_microseconds_count 2693
 				},
 			},
 		},
+		// 11: Multiple minimal metrics with quoted metric names.
+		{
+			in: `
+{"name.1"} 1
+{"name.2"} 1
+{"name.3"} 1
+`,
+			out: []*dto.MetricFamily{
+				{
+					Name: proto.String("name.1"),
+					Type: dto.MetricType_UNTYPED.Enum(),
+					Metric: []*dto.Metric{
+						{
+							Untyped: &dto.Untyped{
+								Value: proto.Float64(1),
+							},
+						},
+					},
+				},
+				{
+					Name: proto.String("name.2"),
+					Type: dto.MetricType_UNTYPED.Enum(),
+					Metric: []*dto.Metric{
+						{
+							Untyped: &dto.Untyped{
+								Value: proto.Float64(1),
+							},
+						},
+					},
+				},
+				{
+					Name: proto.String("name.3"),
+					Type: dto.MetricType_UNTYPED.Enum(),
+					Metric: []*dto.Metric{
+						{
+							Untyped: &dto.Untyped{
+								Value: proto.Float64(1),
+							},
+						},
+					},
+				},
+			},
+		},
 	}
 
 	for i, scenario := range scenarios {