Skip to content

EscapeMetricFamily incorrectly preserves : in label names #18380

Description

@juli4n

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions