mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
lib/protoparser/prometheus: properly unescape label values in Prometheus exposition format
Unescape only `\n`, `\"` and `\\` sequences as Prometheus does. Other escape sequences shouldn't be unescaped.
This commit is contained in:
parent
019d8e88d8
commit
937f382938
3 changed files with 61 additions and 41 deletions
|
@ -31,6 +31,7 @@
|
|||
* BUGFIX: fix arm64 builds due to the issue in `github.com/golang/snappy`. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1074
|
||||
* BUGFIX: fix `index out of range [1024819115206086200] with length 27` panic, which could occur when `1e-9` value is passed to VictoriaMetrics histogram. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1096
|
||||
* BUGFIX: fix parsing for Graphite line with empty tags such as `foo; 123 456`. See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1100
|
||||
* BUGFIX: unescape only `\\`, `\n` and `\"` in label names when parsing Prometheus text exposition format as Prometheus does. Previously other escape sequences could be improperly unescaped.
|
||||
|
||||
|
||||
# [v1.54.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.54.1)
|
||||
|
|
|
@ -2,7 +2,6 @@ package prometheus
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
|
||||
|
@ -264,11 +263,7 @@ func unmarshalTags(dst []Tag, s string, noEscapes bool) (string, []Tag, error) {
|
|||
if n < 0 {
|
||||
return s, dst, fmt.Errorf("missing closing quote for tag value %q", s)
|
||||
}
|
||||
var err error
|
||||
value, err = unescapeValue(s[:n+1])
|
||||
if err != nil {
|
||||
return s, dst, fmt.Errorf("cannot unescape value %q for tag %q: %w", s[:n+1], key, err)
|
||||
}
|
||||
value = unescapeValue(s[1:n])
|
||||
s = s[n+1:]
|
||||
}
|
||||
if len(key) > 0 {
|
||||
|
@ -324,16 +319,41 @@ func findClosingQuote(s string) int {
|
|||
}
|
||||
}
|
||||
|
||||
func unescapeValue(s string) (string, error) {
|
||||
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
|
||||
return "", fmt.Errorf("unexpected tag value: %q", s)
|
||||
}
|
||||
func unescapeValue(s string) string {
|
||||
n := strings.IndexByte(s, '\\')
|
||||
if n < 0 {
|
||||
// Fast path - nothing to unescape
|
||||
return s[1 : len(s)-1], nil
|
||||
return s
|
||||
}
|
||||
return strconv.Unquote(s)
|
||||
b := make([]byte, 0, len(s))
|
||||
for {
|
||||
b = append(b, s[:n]...)
|
||||
s = s[n+1:]
|
||||
if len(s) == 0 {
|
||||
b = append(b, '\\')
|
||||
break
|
||||
}
|
||||
// label_value can be any sequence of UTF-8 characters, but the backslash (\), double-quote ("),
|
||||
// and line feed (\n) characters have to be escaped as \\, \", and \n, respectively.
|
||||
// See https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md
|
||||
switch s[0] {
|
||||
case '\\':
|
||||
b = append(b, '\\')
|
||||
case '"':
|
||||
b = append(b, '"')
|
||||
case 'n':
|
||||
b = append(b, '\n')
|
||||
default:
|
||||
b = append(b, '\\', s[0])
|
||||
}
|
||||
s = s[1:]
|
||||
n = strings.IndexByte(s, '\\')
|
||||
if n < 0 {
|
||||
b = append(b, s...)
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func prevBackslashesCount(s string) int {
|
||||
|
|
|
@ -46,41 +46,22 @@ func TestFindClosingQuote(t *testing.T) {
|
|||
f(`"foo\"bar\"baz"`, 14)
|
||||
}
|
||||
|
||||
func TestUnescapeValueFailure(t *testing.T) {
|
||||
f := func(s string) {
|
||||
t.Helper()
|
||||
ss, err := unescapeValue(s)
|
||||
if err == nil {
|
||||
t.Fatalf("expecting error")
|
||||
}
|
||||
if ss != "" {
|
||||
t.Fatalf("expecting empty string; got %q", ss)
|
||||
}
|
||||
}
|
||||
f(``)
|
||||
f(`foobar`)
|
||||
f(`"foobar`)
|
||||
f(`foobar"`)
|
||||
f(`"foobar\"`)
|
||||
f(` "foobar"`)
|
||||
f(`"foobar" `)
|
||||
}
|
||||
|
||||
func TestUnescapeValueSuccess(t *testing.T) {
|
||||
func TestUnescapeValue(t *testing.T) {
|
||||
f := func(s, resultExpected string) {
|
||||
t.Helper()
|
||||
result, err := unescapeValue(s)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
result := unescapeValue(s)
|
||||
if result != resultExpected {
|
||||
t.Fatalf("unexpected result; got %q; want %q", result, resultExpected)
|
||||
}
|
||||
}
|
||||
f(`""`, "")
|
||||
f(`"f"`, "f")
|
||||
f(`"foobar"`, "foobar")
|
||||
f(`"\"\n\t"`, "\"\n\t")
|
||||
f(``, "")
|
||||
f(`f`, "f")
|
||||
f(`foobar`, "foobar")
|
||||
f(`\"\n\t`, "\"\n\\t")
|
||||
|
||||
// Edge cases
|
||||
f(`foo\bar`, "foo\\bar")
|
||||
f(`foo\`, "foo\\")
|
||||
}
|
||||
|
||||
func TestRowsUnmarshalFailure(t *testing.T) {
|
||||
|
@ -203,6 +184,24 @@ cassandra_token_ownership_ratio 78.9`, &Rows{
|
|||
}},
|
||||
})
|
||||
|
||||
// Incorrectly escaped backlash. This is real-world case, which must be supported.
|
||||
f(`mssql_sql_server_active_transactions_sec{loginname="domain\somelogin",env="develop"} 56`, &Rows{
|
||||
Rows: []Row{{
|
||||
Metric: "mssql_sql_server_active_transactions_sec",
|
||||
Tags: []Tag{
|
||||
{
|
||||
Key: "loginname",
|
||||
Value: "domain\\somelogin",
|
||||
},
|
||||
{
|
||||
Key: "env",
|
||||
Value: "develop",
|
||||
},
|
||||
},
|
||||
Value: 56,
|
||||
}},
|
||||
})
|
||||
|
||||
// Exemplars - see https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1
|
||||
f(`foo_bucket{le="10",a="#b"} 17 # {trace_id="oHg5SJ#YRHA0"} 9.8 1520879607.789
|
||||
abc 123 456 # foobar
|
||||
|
|
Loading…
Reference in a new issue