diff --git a/tsweb/varz/varz.go b/tsweb/varz/varz.go index 15a3be551..243cc0782 100644 --- a/tsweb/varz/varz.go +++ b/tsweb/varz/varz.go @@ -17,6 +17,8 @@ import ( "strings" "sync" "time" + "unicode" + "unicode/utf8" "tailscale.com/metrics" "tailscale.com/version" @@ -85,8 +87,29 @@ func prometheusMetric(prefix string, key string) (string, string, string) { label, key = a, b } } + + // Convert the metric to a valid Prometheus metric name. + // "Metric names may contain ASCII letters, digits, underscores, and colons. + // It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*" + mapInvalidMetricRunes := func(r rune) rune { + if r >= 'a' && r <= 'z' || + r >= 'A' && r <= 'Z' || + r >= '0' && r <= '9' || + r == '_' || r == ':' { + return r + } + if r < utf8.RuneSelf && unicode.IsPrint(r) { + return '_' + } + return -1 + } + metricName := strings.Map(mapInvalidMetricRunes, prefix+key) + if metricName == "" || unicode.IsDigit(rune(metricName[0])) { + metricName = "_" + metricName + } + d := &prometheusMetricDetails{ - Name: strings.ReplaceAll(prefix+key, "-", "_"), + Name: metricName, Type: typ, Label: label, } diff --git a/tsweb/varz/varz_test.go b/tsweb/varz/varz_test.go index 4fe91479b..6105f571b 100644 --- a/tsweb/varz/varz_test.go +++ b/tsweb/varz/varz_test.go @@ -43,6 +43,24 @@ func TestVarzHandler(t *testing.T) { new(expvar.Int), "# TYPE foo_bar counter\nfoo_bar 0\n", }, + { + "slash_in_metric_name", + "counter_foo/bar", + new(expvar.Int), + "# TYPE foo_bar counter\nfoo_bar 0\n", + }, + { + "metric_name_start_digit", + "0abc", + new(expvar.Int), + "# TYPE _0abc counter\n_0abc 0\n", + }, + { + "metric_name_have_bogus_bytes", + "abc\x10defügh", + new(expvar.Int), + "# TYPE abcdefgh counter\nabcdefgh 0\n", + }, { "int_with_type_counter", "counter_foo",