mirror of https://github.com/prometheus/prometheus
Merge pull request #15399 from prometheus/labels-utf8-fix
Add support for utf8 names on /v1/label/:name/values endpointpull/15480/head^2
commit
c5c222e493
|
@ -433,18 +433,40 @@ URL query parameters:
|
|||
series from which to read the label values. Optional.
|
||||
- `limit=<number>`: Maximum number of returned series. Optional. 0 means disabled.
|
||||
|
||||
|
||||
The `data` section of the JSON response is a list of string label values.
|
||||
|
||||
This example queries for all label values for the `job` label:
|
||||
This example queries for all label values for the `http_status_code` label:
|
||||
|
||||
```json
|
||||
$ curl http://localhost:9090/api/v1/label/job/values
|
||||
$ curl http://localhost:9090/api/v1/label/http_status_code/values
|
||||
{
|
||||
"status" : "success",
|
||||
"data" : [
|
||||
"node",
|
||||
"prometheus"
|
||||
"200",
|
||||
"504"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Label names can optionally be encoded using the Values Escaping method, and is necessary if a name includes the `/` character. To encode a name in this way:
|
||||
|
||||
* Prepend the label with `U__`.
|
||||
* Letters, numbers, and colons appear as-is.
|
||||
* Convert single underscores to double underscores.
|
||||
* For all other characters, use the UTF-8 codepoint as a hex integer, surrounded
|
||||
by underscores. So ` ` becomes `_20_` and a `.` becomes `_2e_`.
|
||||
|
||||
More information about text escaping can be found in the original UTF-8 [Proposal document](https://github.com/prometheus/proposals/blob/main/proposals/2023-08-21-utf8.md#text-escaping).
|
||||
|
||||
This example queries for all label values for the `http.status_code` label:
|
||||
|
||||
```json
|
||||
$ curl http://localhost:9090/api/v1/label/U__http_2e_status_code/values
|
||||
{
|
||||
"status" : "success",
|
||||
"data" : [
|
||||
"200",
|
||||
"404"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
@ -667,10 +667,16 @@ label_set_list : label_set_list COMMA label_set_item
|
|||
|
||||
label_set_item : IDENTIFIER EQL STRING
|
||||
{ $$ = labels.Label{Name: $1.Val, Value: yylex.(*parser).unquoteString($3.Val) } }
|
||||
| string_identifier EQL STRING
|
||||
{ $$ = labels.Label{Name: $1.Val, Value: yylex.(*parser).unquoteString($3.Val) } }
|
||||
| IDENTIFIER EQL error
|
||||
{ yylex.(*parser).unexpected("label set", "string"); $$ = labels.Label{}}
|
||||
| string_identifier EQL error
|
||||
{ yylex.(*parser).unexpected("label set", "string"); $$ = labels.Label{}}
|
||||
| IDENTIFIER error
|
||||
{ yylex.(*parser).unexpected("label set", "\"=\""); $$ = labels.Label{}}
|
||||
| string_identifier error
|
||||
{ yylex.(*parser).unexpected("label set", "\"=\""); $$ = labels.Label{}}
|
||||
| error
|
||||
{ yylex.(*parser).unexpected("label set", "identifier or \"}\""); $$ = labels.Label{} }
|
||||
;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -744,7 +744,12 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
|
|||
ctx := r.Context()
|
||||
name := route.Param(ctx, "name")
|
||||
|
||||
if !model.LabelNameRE.MatchString(name) {
|
||||
if strings.HasPrefix(name, "U__") {
|
||||
name = model.UnescapeName(name, model.ValueEncodingEscaping)
|
||||
}
|
||||
|
||||
label := model.LabelName(name)
|
||||
if !label.IsValid() {
|
||||
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil}
|
||||
}
|
||||
|
||||
|
|
|
@ -387,6 +387,8 @@ func TestEndpoints(t *testing.T) {
|
|||
test_metric4{foo="bar", dup="1"} 1+0x100
|
||||
test_metric4{foo="boo", dup="1"} 1+0x100
|
||||
test_metric4{foo="boo"} 1+0x100
|
||||
test_metric5{"host.name"="localhost"} 1+0x100
|
||||
test_metric5{"junk\n{},=: chars"="bar"} 1+0x100
|
||||
`)
|
||||
t.Cleanup(func() { storage.Close() })
|
||||
|
||||
|
@ -1117,6 +1119,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
metadata []targetMetadata
|
||||
exemplars []exemplar.QueryResult
|
||||
zeroFunc func(interface{})
|
||||
nameValidationScheme model.ValidationScheme
|
||||
}
|
||||
|
||||
rulesZeroFunc := func(i interface{}) {
|
||||
|
@ -2996,6 +2999,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"test_metric2",
|
||||
"test_metric3",
|
||||
"test_metric4",
|
||||
"test_metric5",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -3008,13 +3012,36 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"boo",
|
||||
},
|
||||
},
|
||||
// Bad name parameter.
|
||||
// Bad name parameter for legacy validation.
|
||||
{
|
||||
endpoint: api.labelValues,
|
||||
params: map[string]string{
|
||||
"name": "not!!!allowed",
|
||||
"name": "host.name",
|
||||
},
|
||||
nameValidationScheme: model.LegacyValidation,
|
||||
errType: errorBadData,
|
||||
},
|
||||
// Valid utf8 name parameter for utf8 validation.
|
||||
{
|
||||
endpoint: api.labelValues,
|
||||
params: map[string]string{
|
||||
"name": "host.name",
|
||||
},
|
||||
nameValidationScheme: model.UTF8Validation,
|
||||
response: []string{
|
||||
"localhost",
|
||||
},
|
||||
},
|
||||
// Valid escaped utf8 name parameter for utf8 validation.
|
||||
{
|
||||
endpoint: api.labelValues,
|
||||
params: map[string]string{
|
||||
"name": "U__junk_0a__7b__7d__2c__3d_:_20__20_chars",
|
||||
},
|
||||
nameValidationScheme: model.UTF8Validation,
|
||||
response: []string{
|
||||
"bar",
|
||||
},
|
||||
errType: errorBadData,
|
||||
},
|
||||
// Start and end before LabelValues starts.
|
||||
{
|
||||
|
@ -3250,15 +3277,15 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"name": "__name__",
|
||||
},
|
||||
query: url.Values{
|
||||
"limit": []string{"4"},
|
||||
"limit": []string{"5"},
|
||||
},
|
||||
responseLen: 4, // API does not specify which particular values will come back.
|
||||
responseLen: 5, // API does not specify which particular values will come back.
|
||||
warningsCount: 0, // No warnings if limit isn't exceeded.
|
||||
},
|
||||
// Label names.
|
||||
{
|
||||
endpoint: api.labelNames,
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Start and end before Label names starts.
|
||||
{
|
||||
|
@ -3276,7 +3303,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"start": []string{"1"},
|
||||
"end": []string{"100"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Start before Label names, end within Label names.
|
||||
{
|
||||
|
@ -3285,7 +3312,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"start": []string{"-1"},
|
||||
"end": []string{"10"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
|
||||
// Start before Label names starts, end after Label names ends.
|
||||
|
@ -3295,7 +3322,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"start": []string{"-1"},
|
||||
"end": []string{"100000"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Start with bad data for Label names, end within Label names.
|
||||
{
|
||||
|
@ -3313,7 +3340,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
"start": []string{"1"},
|
||||
"end": []string{"1000000006"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Start and end after Label names ends.
|
||||
{
|
||||
|
@ -3330,7 +3357,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
query: url.Values{
|
||||
"start": []string{"4"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Only provide End within Label names, don't provide a start time.
|
||||
{
|
||||
|
@ -3338,7 +3365,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
query: url.Values{
|
||||
"end": []string{"20"},
|
||||
},
|
||||
response: []string{"__name__", "dup", "foo"},
|
||||
response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
|
||||
},
|
||||
// Label names with bad matchers.
|
||||
{
|
||||
|
@ -3406,9 +3433,9 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
{
|
||||
endpoint: api.labelNames,
|
||||
query: url.Values{
|
||||
"limit": []string{"3"},
|
||||
"limit": []string{"5"},
|
||||
},
|
||||
responseLen: 3, // API does not specify which particular values will come back.
|
||||
responseLen: 5, // API does not specify which particular values will come back.
|
||||
warningsCount: 0, // No warnings if limit isn't exceeded.
|
||||
},
|
||||
}...)
|
||||
|
@ -3444,6 +3471,8 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
|
|||
ctx = route.WithParam(ctx, p, v)
|
||||
}
|
||||
|
||||
model.NameValidationScheme = test.nameValidationScheme
|
||||
|
||||
req, err := request(method, test.query)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
Loading…
Reference in New Issue