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 4f82bde65a
commit ae38a4db97
No known key found for this signature in database
GPG key ID: 52C003EE2BCDB9EB
5 changed files with 180 additions and 4 deletions

View file

@ -149,11 +149,11 @@
{% if len(mn.Tags) > 0 %} {% if len(mn.Tags) > 0 %}
{ {
{% code tags := mn.Tags %} {% code tags := mn.Tags %}
{%z= tags[0].Key %}={%qz= tags[0].Value %} {%z= tags[0].Key %}={%= escapePrometheusLabel(tags[0].Value) %}
{% code tags = tags[1:] %} {% code tags = tags[1:] %}
{% for i := range tags %} {% for i := range tags %}
{% code tag := &tags[i] %} {% code tag := &tags[i] %}
,{%z= tag.Key %}={%qz= tag.Value %} ,{%z= tag.Key %}={%= escapePrometheusLabel(tag.Value) %}
{% endfor %} {% endfor %}
} }
{% endif %} {% endif %}
@ -173,4 +173,26 @@
{% endif %} {% endif %}
{% endfunc %} {% 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 %} {% endstripspace %}

View file

@ -495,7 +495,7 @@ func streamprometheusMetricName(qw422016 *qt422016.Writer, mn *storage.MetricNam
//line app/vmselect/prometheus/export.qtpl:152 //line app/vmselect/prometheus/export.qtpl:152
qw422016.N().S(`=`) qw422016.N().S(`=`)
//line app/vmselect/prometheus/export.qtpl:152 //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 //line app/vmselect/prometheus/export.qtpl:153
tags = tags[1:] tags = tags[1:]
@ -511,7 +511,7 @@ func streamprometheusMetricName(qw422016 *qt422016.Writer, mn *storage.MetricNam
//line app/vmselect/prometheus/export.qtpl:156 //line app/vmselect/prometheus/export.qtpl:156
qw422016.N().S(`=`) qw422016.N().S(`=`)
//line app/vmselect/prometheus/export.qtpl:156 //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
} }
//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 return qs422016
//line app/vmselect/prometheus/export.qtpl:174 //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

@ -18,6 +18,8 @@ The following tip changes can be tested by building VictoriaMetrics components f
* SECURITY: upgrade base docker image (Alpine) from 3.18.4 to 3.18.5. See [alpine 3.18.5 release notes](https://www.alpinelinux.org/posts/Alpine-3.15.11-3.16.8-3.17.6-3.18.5-released.html). * SECURITY: upgrade base docker image (Alpine) from 3.18.4 to 3.18.5. See [alpine 3.18.5 release notes](https://www.alpinelinux.org/posts/Alpine-3.15.11-3.16.8-3.17.6-3.18.5-released.html).
* SECURITY: upgrade Go builder from Go1.21.4 to Go1.21.5. See [the list of issues addressed in Go1.21.5](https://github.com/golang/go/issues?q=milestone%3AGo1.21.5+label%3ACherryPickApproved). * SECURITY: upgrade Go builder from Go1.21.4 to Go1.21.5. See [the list of issues addressed in Go1.21.5](https://github.com/golang/go/issues?q=milestone%3AGo1.21.5+label%3ACherryPickApproved).
* 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.87.11](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.87.11) ## [v1.87.11](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.87.11)
Released at 2023-11-14 Released at 2023-11-14