From ae38a4db9778011b4f2db3ef92ab9cd472d0228a Mon Sep 17 00:00:00 2001 From: Aliaksandr Valialkin Date: Thu, 7 Dec 2023 15:26:57 +0200 Subject: [PATCH] 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 https://github.com/prometheus/docs/blob/995743836ec508e7ca714ba758ab71b6ae102f3e/content/docs/instrumenting/exposition_formats.md?plain=1#L90 See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5431 --- app/vmselect/prometheus/export.qtpl | 26 ++++++- app/vmselect/prometheus/export.qtpl.go | 75 ++++++++++++++++++- app/vmselect/prometheus/federate_test.go | 43 +++++++++++ .../prometheus/federate_timing_test.go | 38 ++++++++++ docs/CHANGELOG.md | 2 + 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 app/vmselect/prometheus/federate_test.go create mode 100644 app/vmselect/prometheus/federate_timing_test.go diff --git a/app/vmselect/prometheus/export.qtpl b/app/vmselect/prometheus/export.qtpl index 9925edd72..e12a752b6 100644 --- a/app/vmselect/prometheus/export.qtpl +++ b/app/vmselect/prometheus/export.qtpl @@ -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 %} diff --git a/app/vmselect/prometheus/export.qtpl.go b/app/vmselect/prometheus/export.qtpl.go index 47fe8561a..8018bdc86 100644 --- a/app/vmselect/prometheus/export.qtpl.go +++ b/app/vmselect/prometheus/export.qtpl.go @@ -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 +} diff --git a/app/vmselect/prometheus/federate_test.go b/app/vmselect/prometheus/federate_test.go new file mode 100644 index 000000000..9b5d3011f --- /dev/null +++ b/app/vmselect/prometheus/federate_test.go @@ -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