Repository: prometheus/prometheus (language: go)

Issue:
UTF-8: scrape fails with quoted "utf8" key in exemplar
### What did you do?

Expose an a metric with exemplar from client_go (via opentelemetry), where one of the labels features a non-traditional character in the label (e.g. `http.scheme`),

### What did you expect to see?

This is exported in sccraping as:
```
metric_total{n="1"} 0 # {"net.http"="abc"} 1.0
```
Prometheus then fails to scrape this due to the presence of the quote:
```
 expected label name, got "\"" ("INVALID") while parsing: "metric_total{n=\"1\"} 0 # {\""
```

### What did you see instead? Under which circumstances?

Prometheus should successfully scrape the quote label name.

### System information

Linux 6.11.5-arch1-1 x86_64

### Prometheus version

```text
tested on 
commit: dd4eb4590dc7f4163afb452fd3f2f189caf2d20f

discovered via opentelemetry collector using 

github.com/prometheus/prometheus v0.54.1
```
```


### Prometheus configuration file

_No response_

### Alertmanager version

_No response_

### Alertmanager configuration file

_No response_

### Logs

_No response_


Discussion / hints:
cc @ywwg 
This is from someone trying out https://github.com/open-telemetry/opentelemetry-go/pull/5755.

Fix patch:
diff --git a/model/textparse/openmetricslex.l b/model/textparse/openmetricslex.l
index 9afbbbd8bd5..09106c52ced 100644
--- a/model/textparse/openmetricslex.l
+++ b/model/textparse/openmetricslex.l
@@ -69,6 +69,7 @@ S     [ ]
 <sTimestamp>{S}#{S}\{                 l.state = sExemplar; return tComment
 
 <sExemplar>{L}({L}|{D})*              return tLName
+<sExemplar>\"(\\.|[^\\"\n])*\"        l.state = sExemplar; return tQString
 <sExemplar>\}                         l.state = sEValue; return tBraceClose
 <sExemplar>=                          l.state = sEValue; return tEqual
 <sEValue>\"(\\.|[^\\"\n])*\"          l.state = sExemplar; return tLValue
diff --git a/model/textparse/openmetricslex.l.go b/model/textparse/openmetricslex.l.go
index c8789ef60d4..c0b2fcdb4d8 100644
--- a/model/textparse/openmetricslex.l.go
+++ b/model/textparse/openmetricslex.l.go
@@ -53,9 +53,9 @@ yystate0:
 	case 8: // start condition: sExemplar
 		goto yystart57
 	case 9: // start condition: sEValue
-		goto yystart62
+		goto yystart65
 	case 10: // start condition: sETimestamp
-		goto yystart68
+		goto yystart71
 	}
 
 yystate1:
@@ -538,125 +538,153 @@ yystart57:
 	switch {
 	default:
 		goto yyabort
-	case c == ',':
+	case c == '"':
 		goto yystate58
+	case c == ',':
+		goto yystate61
 	case c == '=':
-		goto yystate59
+		goto yystate62
 	case c == '}':
-		goto yystate61
+		goto yystate64
 	case c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
-		goto yystate60
+		goto yystate63
 	}
 
 yystate58:
 	c = l.next()
-	goto yyrule26
+	switch {
+	default:
+		goto yyabort
+	case c == '"':
+		goto yystate59
+	case c == '\\':
+		goto yystate60
+	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '!' || c >= '#' && c <= '[' || c >= ']' && c <= 'ÿ':
+		goto yystate58
+	}
 
 yystate59:
 	c = l.next()
-	goto yyrule24
+	goto yyrule23
 
 yystate60:
 	c = l.next()
 	switch {
 	default:
-		goto yyrule22
-	case c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
-		goto yystate60
+		goto yyabort
+	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= 'ÿ':
+		goto yystate58
 	}
 
 yystate61:
 	c = l.next()
-	goto yyrule23
+	goto yyrule27
 
 yystate62:
 	c = l.next()
-yystart62:
+	goto yyrule25
+
+yystate63:
+	c = l.next()
+	switch {
+	default:
+		goto yyrule22
+	case c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z':
+		goto yystate63
+	}
+
+yystate64:
+	c = l.next()
+	goto yyrule24
+
+yystate65:
+	c = l.next()
+yystart65:
 	switch {
 	default:
 		goto yyabort
 	case c == ' ':
-		goto yystate63
+		goto yystate66
 	case c == '"':
-		goto yystate65
+		goto yystate68
 	}
 
-yystate63:
+yystate66:
 	c = l.next()
 	switch {
 	default:
 		goto yyabort
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ':
-		goto yystate64
+		goto yystate67
 	}
 
-yystate64:
+yystate67:
 	c = l.next()
 	switch {
 	default:
-		goto yyrule27
+		goto yyrule28
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ':
-		goto yystate64
+		goto yystate67
 	}
 
-yystate65:
+yystate68:
 	c = l.next()
 	switch {
 	default:
 		goto yyabort
 	case c == '"':
-		goto yystate66
+		goto yystate69
 	case c == '\\':
-		goto yystate67
+		goto yystate70
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '!' || c >= '#' && c <= '[' || c >= ']' && c <= 'ÿ':
-		goto yystate65
+		goto yystate68
 	}
 
-yystate66:
+yystate69:
 	c = l.next()
-	goto yyrule25
+	goto yyrule26
 
-yystate67:
+yystate70:
 	c = l.next()
 	switch {
 	default:
 		goto yyabort
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= 'ÿ':
-		goto yystate65
+		goto yystate68
 	}
 
-yystate68:
+yystate71:
 	c = l.next()
-yystart68:
+yystart71:
 	switch {
 	default:
 		goto yyabort
 	case c == ' ':
-		goto yystate70
+		goto yystate73
 	case c == '\n':
-		goto yystate69
+		goto yystate72
 	}
 
-yystate69:
+yystate72:
 	c = l.next()
-	goto yyrule29
+	goto yyrule30
 
-yystate70:
+yystate73:
 	c = l.next()
 	switch {
 	default:
 		goto yyabort
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ':
-		goto yystate71
+		goto yystate74
 	}
 
-yystate71:
+yystate74:
 	c = l.next()
 	switch {
 	default:
-		goto yyrule28
+		goto yyrule29
 	case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ':
-		goto yystate71
+		goto yystate74
 	}
 
 yyrule1: // #{S}
@@ -782,39 +810,45 @@ yyrule22: // {L}({L}|{D})*
 	{
 		return tLName
 	}
-yyrule23: // \}
+yyrule23: // \"(\\.|[^\\"\n])*\"
+	{
+		l.state = sExemplar
+		return tQString
+		goto yystate0
+	}
+yyrule24: // \}
 	{
 		l.state = sEValue
 		return tBraceClose
 		goto yystate0
 	}
-yyrule24: // =
+yyrule25: // =
 	{
 		l.state = sEValue
 		return tEqual
 		goto yystate0
 	}
-yyrule25: // \"(\\.|[^\\"\n])*\"
+yyrule26: // \"(\\.|[^\\"\n])*\"
 	{
 		l.state = sExemplar
 		return tLValue
 		goto yystate0
 	}
-yyrule26: // ,
+yyrule27: // ,
 	{
 		return tComma
 	}
-yyrule27: // {S}[^ \n]+
+yyrule28: // {S}[^ \n]+
 	{
 		l.state = sETimestamp
 		return tValue
 		goto yystate0
 	}
-yyrule28: // {S}[^ \n]+
+yyrule29: // {S}[^ \n]+
 	{
 		return tTimestamp
 	}
-yyrule29: // \n
+yyrule30: // \n
 	if true { // avoid go vet determining the below panic will not be reached
 		l.state = sInit
 		return tLinebreak
@@ -859,10 +893,10 @@ yyabort: // no lexem recognized
 			goto yystate57
 		}
 		if false {
-			goto yystate62
+			goto yystate65
 		}
 		if false {
-			goto yystate68
+			goto yystate71
 		}
 	}
 


Test patch:
diff --git a/model/textparse/openmetricsparse_test.go b/model/textparse/openmetricsparse_test.go
index 9c3c679ab53..a09c56a7ba9 100644
--- a/model/textparse/openmetricsparse_test.go
+++ b/model/textparse/openmetricsparse_test.go
@@ -486,9 +486,12 @@ func TestUTF8OpenMetricsParse(t *testing.T) {
 {"http.status",q="0.9",a="b"} 8.3835e-05
 {q="0.9","http.status",a="b"} 8.3835e-05
 {"go.gc_duration_seconds_sum"} 0.004304266
-{"Heizölrückstoßabdämpfung 10€ metric with \"interesting\" {character\nchoices}","strange©™\n'quoted' \"name\""="6"} 10.0`
+{"Heizölrückstoßabdämpfung 10€ metric with \"interesting\" {character\nchoices}","strange©™\n'quoted' \"name\""="6"} 10.0
+quotedexemplar_count 1 # {"id.thing"="histogram-count-test"} 4
+quotedexemplar2_count 1 # {"id.thing"="histogram-count-test",other="hello"} 4
+`
 
-	input += "\n# EOF\n"
+	input += "# EOF\n"
 
 	exp := []parsedEntry{
 		{
@@ -535,6 +538,20 @@ func TestUTF8OpenMetricsParse(t *testing.T) {
 			v: 10.0,
 			lset: labels.FromStrings("__name__", `Heizölrückstoßabdämpfung 10€ metric with "interesting" {character
 choices}`, "strange©™\n'quoted' \"name\"", "6"),
+		}, {
+			m:    `quotedexemplar_count`,
+			v:    1,
+			lset: labels.FromStrings("__name__", "quotedexemplar_count"),
+			es: []exemplar.Exemplar{
+				{Labels: labels.FromStrings("id.thing", "histogram-count-test"), Value: 4},
+			},
+		}, {
+			m:    `quotedexemplar2_count`,
+			v:    1,
+			lset: labels.FromStrings("__name__", "quotedexemplar2_count"),
+			es: []exemplar.Exemplar{
+				{Labels: labels.FromStrings("id.thing", "histogram-count-test", "other", "hello"), Value: 4},
+			},
 		},
 	}