Add support for values unescaping on `/v1/label/:name/values` endpoint

Signed-off-by: Owen Williams <owen.williams@grafana.com>
pull/15399/head
Owen Williams 2024-11-14 10:51:59 -05:00
parent 88675710f9
commit 12577e3851
3 changed files with 52 additions and 14 deletions

View File

@ -433,18 +433,40 @@ URL query parameters:
series from which to read the label values. Optional. series from which to read the label values. Optional.
- `limit=<number>`: Maximum number of returned series. Optional. 0 means disabled. - `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. 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 ```json
$ curl http://localhost:9090/api/v1/label/job/values $ curl http://localhost:9090/api/v1/label/http_status_code/values
{ {
"status" : "success", "status" : "success",
"data" : [ "data" : [
"node", "200",
"prometheus" "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"
] ]
} }
``` ```

View File

@ -744,6 +744,10 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
ctx := r.Context() ctx := r.Context()
name := route.Param(ctx, "name") name := route.Param(ctx, "name")
if strings.HasPrefix(name, "U__") {
name = model.UnescapeName(name, model.ValueEncodingEscaping)
}
label := model.LabelName(name) label := model.LabelName(name)
if !label.IsValid() { if !label.IsValid() {
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil} return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil}

View File

@ -388,6 +388,7 @@ func TestEndpoints(t *testing.T) {
test_metric4{foo="boo", dup="1"} 1+0x100 test_metric4{foo="boo", dup="1"} 1+0x100
test_metric4{foo="boo"} 1+0x100 test_metric4{foo="boo"} 1+0x100
test_metric5{"host.name"="localhost"} 1+0x100 test_metric5{"host.name"="localhost"} 1+0x100
test_metric5{"junk\n{},=: chars"="bar"} 1+0x100
`) `)
t.Cleanup(func() { storage.Close() }) t.Cleanup(func() { storage.Close() })
@ -3031,6 +3032,17 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"localhost", "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",
},
},
// Start and end before LabelValues starts. // Start and end before LabelValues starts.
{ {
endpoint: api.labelValues, endpoint: api.labelValues,
@ -3273,7 +3285,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
// Label names. // Label names.
{ {
endpoint: api.labelNames, endpoint: api.labelNames,
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Start and end before Label names starts. // Start and end before Label names starts.
{ {
@ -3291,7 +3303,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"start": []string{"1"}, "start": []string{"1"},
"end": []string{"100"}, "end": []string{"100"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Start before Label names, end within Label names. // Start before Label names, end within Label names.
{ {
@ -3300,7 +3312,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"start": []string{"-1"}, "start": []string{"-1"},
"end": []string{"10"}, "end": []string{"10"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Start before Label names starts, end after Label names ends. // Start before Label names starts, end after Label names ends.
@ -3310,7 +3322,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"start": []string{"-1"}, "start": []string{"-1"},
"end": []string{"100000"}, "end": []string{"100000"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Start with bad data for Label names, end within Label names. // Start with bad data for Label names, end within Label names.
{ {
@ -3328,7 +3340,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
"start": []string{"1"}, "start": []string{"1"},
"end": []string{"1000000006"}, "end": []string{"1000000006"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Start and end after Label names ends. // Start and end after Label names ends.
{ {
@ -3345,7 +3357,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
query: url.Values{ query: url.Values{
"start": []string{"4"}, "start": []string{"4"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Only provide End within Label names, don't provide a start time. // Only provide End within Label names, don't provide a start time.
{ {
@ -3353,7 +3365,7 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
query: url.Values{ query: url.Values{
"end": []string{"20"}, "end": []string{"20"},
}, },
response: []string{"__name__", "dup", "foo", "host.name"}, response: []string{"__name__", "dup", "foo", "host.name", "junk\n{},=: chars"},
}, },
// Label names with bad matchers. // Label names with bad matchers.
{ {
@ -3421,9 +3433,9 @@ func testEndpoints(t *testing.T, api *API, tr *testTargetRetriever, es storage.E
{ {
endpoint: api.labelNames, endpoint: api.labelNames,
query: url.Values{ 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. warningsCount: 0, // No warnings if limit isn't exceeded.
}, },
}...) }...)