What did you do?
When escaping label names, EscapeMetricFamily and EscapeName treat : (colon) as a
legacy-valid character and leave it unescaped. This is correct for metric names, but
wrong for label names. The Prometheus data model has always reserved : for metric
names only; it has never been valid in label names.
The result: a label named app:instance-id is rendered as app:instance_id under
UnderscoreEscaping (the default) instead of the correct app_instance_id.
How to reproduce
package main
import (
"fmt"
"log"
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/model"
)
func main() {
// Direct escaping — shows the bug in model.EscapeName
fmt.Println(model.EscapeName("app:instance-id", model.UnderscoreEscaping))
// Got: "app:instance_id" (colon preserved — BUG)
// Expected: "app_instance_id" (colon should be escaped for a label name)
// HTTP endpoint — shows the bug in EscapeMetricFamily via promhttp
counter := prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "requests_total", Help: "Total requests."},
[]string{"app:instance-id"},
)
prometheus.MustRegister(counter)
counter.WithLabelValues("my-server").Inc()
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":9090", nil))
}
$ curl -s http://localhost:9090/metrics | grep requests_total{
requests_total{app:instance_id="my-server"} 1
# ^^^--- colon preserved, hyphen correctly escaped to _
$ curl -s -H 'Accept: text/plain; version=0.0.4; escaping=allow-utf-8' \
http://localhost:9090/metrics | grep requests_total{
requests_total{"app:instance-id"="my-server"} 1
# ^^^^^^^^^^^^^^^^^^--- verbatim original (correct for allow-utf-8)
Affected escaping schemes
All schemes that go through EscapeName are affected wherever isValidLegacyRune decides
whether to leave a character in place:
| Scheme |
Input app:instance-id |
Got |
Expected |
underscores (default) |
app:instance-id |
app:instance_id |
app_instance_id |
dots |
app:instance-id |
app:instance__id |
app__instance__id |
values |
app:instance-id |
U__app:instance_2d_id |
U__app_3a_instance_2d_id |
allow-utf-8 |
app:instance-id |
"app:instance-id" |
"app:instance-id" ✓ |
allow-utf-8 (NoEscaping) is not affected because it returns the name verbatim by
design.
Comparison with the Python client
prometheus_client 0.24.1 gets this right. It defines two separate rune validators and
two separate escape entry points:
# openmetrics/exposition.py:263
def _is_legacy_metric_rune(b, i):
return _is_legacy_labelname_rune(b, i) or b == ':' # colon OK for metric names
def _is_legacy_labelname_rune(b, i):
return ('a' <= b <= 'z') or ('A' <= b <= 'Z') or b == '_' or \
(b.isdigit() and i > 0) # NO colon
def escape_metric_name(s, escaping=UNDERSCORES): ... # uses _is_legacy_metric_rune
def escape_label_name(s, escaping=UNDERSCORES): ... # uses _is_legacy_labelname_rune
Side-by-side output for UnderscoreEscaping:
| Input |
Python (label) |
Go (label) |
Agree? |
app:instance-id |
app_instance_id |
app:instance_id |
No |
http.status:sum |
http_status_sum |
http_status:sum |
No |
my-metric-name |
my_metric_name |
my_metric_name |
Yes |
my_metric:name |
my_metric_name |
my_metric_name |
Yes |
my.metric.name |
my_metric_name |
my_metric_name |
Yes |
What did you expect to see?
A label named app:instance-id escaped as app_instance_id under UnderscoreEscaping
— colon and hyphen both replaced with _, matching the legacy label name character set
[a-zA-Z_][a-zA-Z0-9_]*.
What did you see instead? Under which circumstances?
app:instance_id — the colon is preserved, only the hyphen is replaced. This happens
with any escaping scheme other than allow-utf-8, whenever a label name contains a colon.
What did you do?
When escaping label names,
EscapeMetricFamilyandEscapeNametreat:(colon) as alegacy-valid character and leave it unescaped. This is correct for metric names, but
wrong for label names. The Prometheus data model has always reserved
:for metricnames only; it has never been valid in label names.
The result: a label named
app:instance-idis rendered asapp:instance_idunderUnderscoreEscaping(the default) instead of the correctapp_instance_id.How to reproduce
Affected escaping schemes
All schemes that go through
EscapeNameare affected whereverisValidLegacyRunedecideswhether to leave a character in place:
app:instance-idunderscores(default)app:instance-idapp:instance_idapp_instance_iddotsapp:instance-idapp:instance__idapp__instance__idvaluesapp:instance-idU__app:instance_2d_idU__app_3a_instance_2d_idallow-utf-8app:instance-id"app:instance-id""app:instance-id"✓allow-utf-8(NoEscaping) is not affected because it returns the name verbatim bydesign.
Comparison with the Python client
prometheus_client0.24.1 gets this right. It defines two separate rune validators andtwo separate escape entry points:
Side-by-side output for
UnderscoreEscaping:app:instance-idapp_instance_idapp:instance_idhttp.status:sumhttp_status_sumhttp_status:summy-metric-namemy_metric_namemy_metric_namemy_metric:namemy_metric_namemy_metric_namemy.metric.namemy_metric_namemy_metric_nameWhat did you expect to see?
A label named
app:instance-idescaped asapp_instance_idunderUnderscoreEscaping— colon and hyphen both replaced with
_, matching the legacy label name character set[a-zA-Z_][a-zA-Z0-9_]*.What did you see instead? Under which circumstances?
app:instance_id— the colon is preserved, only the hyphen is replaced. This happenswith any escaping scheme other than
allow-utf-8, whenever a label name contains a colon.