Repository: vitessio/vitess (Go)

Issue:
Bug Report: Prometheus metrics reporting doesn't snake case single label setups
### Overview of the Issue

Prometheus metrics are normally emitted using `snake_case` for labels. This doesn't happen though for the single label cases. 

### Reproduction Steps

See the following code for example:

```go
 	m1 := stats.NewCountersWithSingleLabel("ThisIsMetric1", "helpstring1", "ThisIsALabel")
 	m1.Add("labelvalue1", 420)

 	m2 := stats.NewCountersWithMultiLabels("ThisIsMetric2", "helpstring2", []string{"ThisIsALabel"})
 	m2.Add([]string{"labelvalue2"}, 420)
```

The expectation is that both metrics emit the same labels, namely `this_is_a_label` in `snake_case`. Today they create two different labels, the `stats.NewCountersWithMultiLabels` creates `snake_case` but `stats.NewCountersWithSingleLabel` creates `CamelCase`.

### Binary Version

```sh
n/a
```


### Operating System and Environment details

```sh
n/a
```


### Log Fragments

_No response_


Discussion:

Patch:
diff --git a/doc/releasenotes/16_0_0_summary.md b/doc/releasenotes/16_0_0_summary.md
index bca1e18c5bd..a79cc727468 100644
--- a/doc/releasenotes/16_0_0_summary.md
+++ b/doc/releasenotes/16_0_0_summary.md
@@ -75,6 +75,10 @@ The default value for `remote_operation_timeout` has also changed from 30 second
 During upgrades, if the users want to preserve the same behaviour as previous releases, then they should provide the `remote_operation_timeout` flag explicitly before upgrading.
 After the upgrade, they should then alter their configuration to also specify `lock-timeout` explicitly.
 
+#### Normalized labels in the Prometheus Exporter
+
+The Prometheus metrics exporter now properly normalizes _all_ label names into their `snake_case` form, as it is idiomatic for Prometheus metrics. Previously, Vitess instances were emitting inconsistent labels for their metrics, with some of them being `CamelCase` and others being `snake_case`.
+
 ### New command line flags and behavior
 
 #### VTGate: Support query timeout --query-timeout
diff --git a/go/stats/prometheusbackend/collectors.go b/go/stats/prometheusbackend/collectors.go
index 206e8dfcec3..a1126c0d211 100644
--- a/go/stats/prometheusbackend/collectors.go
+++ b/go/stats/prometheusbackend/collectors.go
@@ -74,7 +74,7 @@ func newCountersWithSingleLabelCollector(c *stats.CountersWithSingleLabel, name
 		desc: prometheus.NewDesc(
 			name,
 			c.Help(),
-			[]string{labelName},
+			[]string{normalizeMetric(labelName)},
 			nil),
 		vt: vt}
 
@@ -111,7 +111,7 @@ func newGaugesWithSingleLabelCollector(g *stats.GaugesWithSingleLabel, name stri
 		desc: prometheus.NewDesc(
 			name,
 			g.Help(),
-			[]string{labelName},
+			[]string{normalizeMetric(labelName)},
 			nil),
 		vt: vt}
 
@@ -266,7 +266,7 @@ func newTimingsCollector(t *stats.Timings, name string) {
 		desc: prometheus.NewDesc(
 			name,
 			t.Help(),
-			[]string{t.Label()},
+			[]string{normalizeMetric(t.Label())},
 			nil),
 	}
 


Test patch:
diff --git a/go/stats/prometheusbackend/prometheusbackend_test.go b/go/stats/prometheusbackend/prometheusbackend_test.go
index 438e678bb3e..888dd630941 100644
--- a/go/stats/prometheusbackend/prometheusbackend_test.go
+++ b/go/stats/prometheusbackend/prometheusbackend_test.go
@@ -357,6 +357,26 @@ func testMetricsHandler(t *testing.T) *httptest.ResponseRecorder {
 	return response
 }
 
+func TestPrometheusLabels(t *testing.T) {
+	m1 := stats.NewCountersWithSingleLabel("ThisIsMetric1", "helpstring1", "ThisIsALabel")
+	m1.Add("labelvalue1", 420)
+
+	m2 := stats.NewCountersWithMultiLabels("ThisIsMetric2", "helpstring2", []string{"ThisIsALabel"})
+	m2.Add([]string{"labelvalue2"}, 420)
+
+	response := testMetricsHandler(t)
+
+	expect := []string{
+		"namespace_this_is_metric1{this_is_a_label=\"labelvalue1\"} 420",
+		"namespace_this_is_metric2{this_is_a_label=\"labelvalue2\"} 420",
+	}
+	for _, line := range expect {
+		if !strings.Contains(response.Body.String(), line) {
+			t.Fatalf("Expected result to contain %s, got %s", line, response.Body.String())
+		}
+	}
+}
+
 func TestMain(m *testing.M) {
 	Init(namespace)
 	os.Exit(m.Run())