model/textparse: improve error outputs

Parsing errors in the Prometheus HTTP format parser are very hard to
investigate since they only approximately indicate what is going wrong
in the parser and don't provide any information about the incorrect
input. As such it is very hard to tell what is wrong in the format
exposed by the application.

Signed-off-by: Damien Grisonnet <dgrisonn@redhat.com>
This commit is contained in:
Damien Grisonnet 2022-12-07 14:32:03 +01:00
parent c70d85baed
commit e3b5416d05
3 changed files with 44 additions and 36 deletions

View file

@ -223,6 +223,10 @@ func (p *OpenMetricsParser) nextToken() token {
return tok
}
func parseError(exp string, got token) error {
return fmt.Errorf("%s, got %q", exp, got)
}
// Next advances the parser to the next sample. It returns false if no
// more samples were read or an error occurred.
func (p *OpenMetricsParser) Next() (Entry, error) {

View file

@ -254,8 +254,12 @@ func (p *PromParser) nextToken() token {
}
}
func parseError(exp string, got token) error {
return fmt.Errorf("%s, got %q", exp, got)
func (p *PromParser) parseError(exp string, got token) error {
e := p.l.i + 1
if len(p.l.b) <= e {
e = len(p.l.b)
}
return fmt.Errorf("%s, got %q (%q) while parsing: %q", exp, p.l.b[p.l.start:e], got, p.l.b[p.start:e])
}
// Next advances the parser to the next sample. It returns false if no
@ -278,7 +282,7 @@ func (p *PromParser) Next() (Entry, error) {
case tMName:
p.offsets = append(p.offsets, p.l.start, p.l.i)
default:
return EntryInvalid, parseError("expected metric name after "+t.String(), t2)
return EntryInvalid, p.parseError("expected metric name after "+t.String(), t2)
}
switch t2 := p.nextToken(); t2 {
case tText:
@ -308,11 +312,11 @@ func (p *PromParser) Next() (Entry, error) {
}
case tHelp:
if !utf8.Valid(p.text) {
return EntryInvalid, fmt.Errorf("help text is not a valid utf8 string")
return EntryInvalid, fmt.Errorf("help text %q is not a valid utf8 string", p.text)
}
}
if t := p.nextToken(); t != tLinebreak {
return EntryInvalid, parseError("linebreak expected after metadata", t)
return EntryInvalid, p.parseError("linebreak expected after metadata", t)
}
switch t {
case tHelp:
@ -323,7 +327,7 @@ func (p *PromParser) Next() (Entry, error) {
case tComment:
p.text = p.l.buf()
if t := p.nextToken(); t != tLinebreak {
return EntryInvalid, parseError("linebreak expected after comment", t)
return EntryInvalid, p.parseError("linebreak expected after comment", t)
}
return EntryComment, nil
@ -340,10 +344,10 @@ func (p *PromParser) Next() (Entry, error) {
t2 = p.nextToken()
}
if t2 != tValue {
return EntryInvalid, parseError("expected value after metric", t2)
return EntryInvalid, p.parseError("expected value after metric", t2)
}
if p.val, err = parseFloat(yoloString(p.l.buf())); err != nil {
return EntryInvalid, err
return EntryInvalid, fmt.Errorf("%v while parsing: %q", err, p.l.b[p.start:p.l.i])
}
// Ensure canonical NaN value.
if math.IsNaN(p.val) {
@ -356,18 +360,18 @@ func (p *PromParser) Next() (Entry, error) {
case tTimestamp:
p.hasTS = true
if p.ts, err = strconv.ParseInt(yoloString(p.l.buf()), 10, 64); err != nil {
return EntryInvalid, err
return EntryInvalid, fmt.Errorf("%v while parsing: %q", err, p.l.b[p.start:p.l.i])
}
if t2 := p.nextToken(); t2 != tLinebreak {
return EntryInvalid, parseError("expected next entry after timestamp", t2)
return EntryInvalid, p.parseError("expected next entry after timestamp", t2)
}
default:
return EntryInvalid, parseError("expected timestamp or new record", t)
return EntryInvalid, p.parseError("expected timestamp or new record", t)
}
return EntrySeries, nil
default:
err = fmt.Errorf("%q is not a valid start token", t)
err = p.parseError("expected a valid start token", t)
}
return EntryInvalid, err
}
@ -380,18 +384,18 @@ func (p *PromParser) parseLVals() error {
return nil
case tLName:
default:
return parseError("expected label name", t)
return p.parseError("expected label name", t)
}
p.offsets = append(p.offsets, p.l.start, p.l.i)
if t := p.nextToken(); t != tEqual {
return parseError("expected equal", t)
return p.parseError("expected equal", t)
}
if t := p.nextToken(); t != tLValue {
return parseError("expected label value", t)
return p.parseError("expected label value", t)
}
if !utf8.Valid(p.l.buf()) {
return fmt.Errorf("invalid UTF-8 label value")
return fmt.Errorf("invalid UTF-8 label value: %q", p.l.buf())
}
// The promlexer ensures the value string is quoted. Strip first

View file

@ -219,63 +219,63 @@ func TestPromParseErrors(t *testing.T) {
}{
{
input: "a",
err: "expected value after metric, got \"INVALID\"",
err: "expected value after metric, got \"\\n\" (\"INVALID\") while parsing: \"a\\n\"",
},
{
input: "a{b='c'} 1\n",
err: "expected label value, got \"INVALID\"",
err: "expected label value, got \"'\" (\"INVALID\") while parsing: \"a{b='\"",
},
{
input: "a{b=\n",
err: "expected label value, got \"INVALID\"",
err: "expected label value, got \"\\n\" (\"INVALID\") while parsing: \"a{b=\\n\"",
},
{
input: "a{\xff=\"foo\"} 1\n",
err: "expected label name, got \"INVALID\"",
err: "expected label name, got \"\\xff\" (\"INVALID\") while parsing: \"a{\\xff\"",
},
{
input: "a{b=\"\xff\"} 1\n",
err: "invalid UTF-8 label value",
err: "invalid UTF-8 label value: \"\\\"\\xff\\\"\"",
},
{
input: "a true\n",
err: "strconv.ParseFloat: parsing \"true\": invalid syntax",
err: "strconv.ParseFloat: parsing \"true\": invalid syntax while parsing: \"a true\"",
},
{
input: "something_weird{problem=\"",
err: "expected label value, got \"INVALID\"",
err: "expected label value, got \"\\\"\\n\" (\"INVALID\") while parsing: \"something_weird{problem=\\\"\\n\"",
},
{
input: "empty_label_name{=\"\"} 0",
err: "expected label name, got \"EQUAL\"",
err: "expected label name, got \"=\\\"\" (\"EQUAL\") while parsing: \"empty_label_name{=\\\"\"",
},
{
input: "foo 1_2\n",
err: "unsupported character in float",
err: "unsupported character in float while parsing: \"foo 1_2\"",
},
{
input: "foo 0x1p-3\n",
err: "unsupported character in float",
err: "unsupported character in float while parsing: \"foo 0x1p-3\"",
},
{
input: "foo 0x1P-3\n",
err: "unsupported character in float",
err: "unsupported character in float while parsing: \"foo 0x1P-3\"",
},
{
input: "foo 0 1_2\n",
err: "expected next entry after timestamp, got \"INVALID\"",
err: "expected next entry after timestamp, got \"_\" (\"INVALID\") while parsing: \"foo 0 1_\"",
},
{
input: `{a="ok"} 1`,
err: `"INVALID" is not a valid start token`,
err: "expected a valid start token, got \"{\" (\"INVALID\") while parsing: \"{\"",
},
{
input: "# TYPE #\n#EOF\n",
err: "expected metric name after TYPE, got \"INVALID\"",
err: "expected metric name after TYPE, got \"#\" (\"INVALID\") while parsing: \"# TYPE #\"",
},
{
input: "# HELP #\n#EOF\n",
err: "expected metric name after HELP, got \"INVALID\"",
err: "expected metric name after HELP, got \"#\" (\"INVALID\") while parsing: \"# HELP #\"",
},
}
@ -313,23 +313,23 @@ func TestPromNullByteHandling(t *testing.T) {
},
{
input: "a{b=\x00\"ssss\"} 1\n",
err: "expected label value, got \"INVALID\"",
err: "expected label value, got \"\\x00\" (\"INVALID\") while parsing: \"a{b=\\x00\"",
},
{
input: "a{b=\"\x00",
err: "expected label value, got \"INVALID\"",
err: "expected label value, got \"\\\"\\x00\\n\" (\"INVALID\") while parsing: \"a{b=\\\"\\x00\\n\"",
},
{
input: "a{b\x00=\"hiih\"} 1",
err: "expected equal, got \"INVALID\"",
err: "expected equal, got \"\\x00\" (\"INVALID\") while parsing: \"a{b\\x00\"",
},
{
input: "a\x00{b=\"ddd\"} 1",
err: "expected value after metric, got \"INVALID\"",
err: "expected value after metric, got \"\\x00\" (\"INVALID\") while parsing: \"a\\x00\"",
},
{
input: "a 0 1\x00",
err: "expected next entry after timestamp, got \"INVALID\"",
err: "expected next entry after timestamp, got \"\\x00\" (\"INVALID\") while parsing: \"a 0 1\\x00\"",
},
}