app/vmselect/prometheus: properly encode Prometheus label values at /federate endpoint

Prometheus spec says that only \, \n and " must be escaped inside label values.
See 995743836e/content/docs/instrumenting/exposition_formats.md (L90)

See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
This commit is contained in:
Aliaksandr Valialkin 2023-12-07 15:26:57 +02:00
parent 77b9047a40
commit 9f79342e6a
No known key found for this signature in database
GPG Key ID: 52C003EE2BCDB9EB
5 changed files with 179 additions and 4 deletions

View File

@ -149,11 +149,11 @@
{% if len(mn.Tags) > 0 %}
{
{% code tags := mn.Tags %}
{%z= tags[0].Key %}={%qz= tags[0].Value %}
{%z= tags[0].Key %}={%= escapePrometheusLabel(tags[0].Value) %}
{% code tags = tags[1:] %}
{% for i := range tags %}
{% code tag := &tags[i] %}
,{%z= tag.Key %}={%qz= tag.Value %}
,{%z= tag.Key %}={%= escapePrometheusLabel(tag.Value) %}
{% endfor %}
}
{% endif %}
@ -173,4 +173,26 @@
{% endif %}
{% endfunc %}
{% func escapePrometheusLabel(b []byte) %}
"
{% for len(b) > 0 %}
{% code n := bytes.IndexAny(b, "\\\n\"") %}
{% if n < 0 %}
{%z= b %}
{% break %}
{% endif %}
{%z= b[:n] %}
{% switch b[n] %}
{% case '\\' %}
\\
{% case '\n' %}
\n
{% case '"' %}
\"
{% endswitch %}
{% code b = b[n+1:] %}
{% endfor %}
"
{% endfunc %}
{% endstripspace %}

View File

@ -495,7 +495,7 @@ func streamprometheusMetricName(qw422016 *qt422016.Writer, mn *storage.MetricNam
//line app/vmselect/prometheus/export.qtpl:152
qw422016.N().S(`=`)
//line app/vmselect/prometheus/export.qtpl:152
qw422016.N().QZ(tags[0].Value)
streamescapePrometheusLabel(qw422016, tags[0].Value)
//line app/vmselect/prometheus/export.qtpl:153
tags = tags[1:]
@ -511,7 +511,7 @@ func streamprometheusMetricName(qw422016 *qt422016.Writer, mn *storage.MetricNam
//line app/vmselect/prometheus/export.qtpl:156
qw422016.N().S(`=`)
//line app/vmselect/prometheus/export.qtpl:156
qw422016.N().QZ(tag.Value)
streamescapePrometheusLabel(qw422016, tag.Value)
//line app/vmselect/prometheus/export.qtpl:157
}
//line app/vmselect/prometheus/export.qtpl:157
@ -599,3 +599,74 @@ func convertValueToSpecialJSON(v float64) string {
return qs422016
//line app/vmselect/prometheus/export.qtpl:174
}
//line app/vmselect/prometheus/export.qtpl:176
func streamescapePrometheusLabel(qw422016 *qt422016.Writer, b []byte) {
//line app/vmselect/prometheus/export.qtpl:176
qw422016.N().S(`"`)
//line app/vmselect/prometheus/export.qtpl:178
for len(b) > 0 {
//line app/vmselect/prometheus/export.qtpl:179
n := bytes.IndexAny(b, "\\\n\"")
//line app/vmselect/prometheus/export.qtpl:180
if n < 0 {
//line app/vmselect/prometheus/export.qtpl:181
qw422016.N().Z(b)
//line app/vmselect/prometheus/export.qtpl:182
break
//line app/vmselect/prometheus/export.qtpl:183
}
//line app/vmselect/prometheus/export.qtpl:184
qw422016.N().Z(b[:n])
//line app/vmselect/prometheus/export.qtpl:185
switch b[n] {
//line app/vmselect/prometheus/export.qtpl:186
case '\\':
//line app/vmselect/prometheus/export.qtpl:186
qw422016.N().S(`\\`)
//line app/vmselect/prometheus/export.qtpl:188
case '\n':
//line app/vmselect/prometheus/export.qtpl:188
qw422016.N().S(`\n`)
//line app/vmselect/prometheus/export.qtpl:190
case '"':
//line app/vmselect/prometheus/export.qtpl:190
qw422016.N().S(`\"`)
//line app/vmselect/prometheus/export.qtpl:192
}
//line app/vmselect/prometheus/export.qtpl:193
b = b[n+1:]
//line app/vmselect/prometheus/export.qtpl:194
}
//line app/vmselect/prometheus/export.qtpl:194
qw422016.N().S(`"`)
//line app/vmselect/prometheus/export.qtpl:196
}
//line app/vmselect/prometheus/export.qtpl:196
func writeescapePrometheusLabel(qq422016 qtio422016.Writer, b []byte) {
//line app/vmselect/prometheus/export.qtpl:196
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/export.qtpl:196
streamescapePrometheusLabel(qw422016, b)
//line app/vmselect/prometheus/export.qtpl:196
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/export.qtpl:196
}
//line app/vmselect/prometheus/export.qtpl:196
func escapePrometheusLabel(b []byte) string {
//line app/vmselect/prometheus/export.qtpl:196
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/export.qtpl:196
writeescapePrometheusLabel(qb422016, b)
//line app/vmselect/prometheus/export.qtpl:196
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/export.qtpl:196
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/export.qtpl:196
return qs422016
//line app/vmselect/prometheus/export.qtpl:196
}

View File

@ -0,0 +1,43 @@
package prometheus
import (
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
func TestFederate(t *testing.T) {
f := func(rs *netstorage.Result, expectedResult string) {
t.Helper()
result := Federate(rs)
if result != expectedResult {
t.Fatalf("unexpected result; got\n%s\nwant\n%s", result, expectedResult)
}
}
f(&netstorage.Result{}, ``)
f(&netstorage.Result{
MetricName: storage.MetricName{
MetricGroup: []byte("foo"),
Tags: []storage.Tag{
{
Key: []byte("a"),
Value: []byte("b"),
},
{
Key: []byte("qqq"),
Value: []byte("\\"),
},
{
Key: []byte("abc"),
// Verify that < isn't encoded. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431
Value: []byte("a<b\"\\c"),
},
},
},
Values: []float64{1.23},
Timestamps: []int64{123},
}, `foo{a="b",qqq="\\",abc="a<b\"\\c"} 1.23 123`+"\n")
}

View File

@ -0,0 +1,38 @@
package prometheus
import (
"bytes"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/netstorage"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
)
func BenchmarkFederate(b *testing.B) {
rs := &netstorage.Result{
MetricName: storage.MetricName{
MetricGroup: []byte("foo_bar_bazaaaa_total"),
Tags: []storage.Tag{
{
Key: []byte("instance"),
Value: []byte("foobarbaz:2344"),
},
{
Key: []byte("job"),
Value: []byte("aaabbbccc"),
},
},
},
Values: []float64{112.23},
Timestamps: []int64{1234567890},
}
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
var bb bytes.Buffer
for pb.Next() {
bb.Reset()
WriteFederate(&bb, rs)
}
})
}

View File

@ -60,6 +60,7 @@ The sandbox cluster installation is running under the constant load generated by
* BUGFIX: [vmalert-tool](https://docs.victoriametrics.com/#vmalert-tool): allow using arbitrary `eval_time` in [alert_rule_test](https://docs.victoriametrics.com/vmalert-tool.html#alert_test_case) case. Previously, test cases with `eval_time` not being a multiple of `evaluation_interval` would fail.
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager.html): fix `vmbackupmanager` not deleting previous object versions from S3 when applying retention policy with `-deleteAllObjectVersions` command-line flag.
* BUGFIX: [vminsert](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): fix panic when ingesting data via [NewRelic protocol](https://docs.victoriametrics.com/#how-to-send-data-from-newrelic-agent) into VictoriaMetrics cluster. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5416).
* BUGFIX: properly escape `<` character in responses returned via [`/federate`](https://docs.victoriametrics.com/#federation) endpoint. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431).
## [v1.95.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.95.1)