Skip to content

EscapeMetricFamily incorrectly preserves : in label names #18380

@juli4n

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions