From e825282dd1b7a62999ed50d5c8039c5fdda89bea Mon Sep 17 00:00:00 2001 From: shreyassrivatsan Date: Tue, 19 Nov 2019 01:33:30 -0800 Subject: [PATCH] Add exemplar support to the openmetrics parser (#6292) * Add exemplar support to the openmetrics parser Signed-off-by: Shreyas Srivatsan --- pkg/exemplar/exemplar.go | 24 +++ pkg/textparse/interface.go | 5 + pkg/textparse/openmetricslex.l | 13 +- pkg/textparse/openmetricslex.l.go | 196 +++++++++++++++++++++++-- pkg/textparse/openmetricsparse.go | 184 +++++++++++++++++++---- pkg/textparse/openmetricsparse_test.go | 105 +++++++++++-- pkg/textparse/promlex.l.go | 3 + pkg/textparse/promparse.go | 7 + 8 files changed, 487 insertions(+), 50 deletions(-) create mode 100644 pkg/exemplar/exemplar.go diff --git a/pkg/exemplar/exemplar.go b/pkg/exemplar/exemplar.go new file mode 100644 index 0000000000..c6ea0db94d --- /dev/null +++ b/pkg/exemplar/exemplar.go @@ -0,0 +1,24 @@ +// Copyright 2019 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exemplar + +import "github.com/prometheus/prometheus/pkg/labels" + +// Exemplar is additional information associated with a time series. +type Exemplar struct { + Labels labels.Labels + Value float64 + HasTs bool + Ts int64 +} diff --git a/pkg/textparse/interface.go b/pkg/textparse/interface.go index 330dffa8ee..cfcd05e210 100644 --- a/pkg/textparse/interface.go +++ b/pkg/textparse/interface.go @@ -16,6 +16,7 @@ package textparse import ( "mime" + "github.com/prometheus/prometheus/pkg/exemplar" "github.com/prometheus/prometheus/pkg/labels" ) @@ -50,6 +51,10 @@ type Parser interface { // It returns the string from which the metric was parsed. Metric(l *labels.Labels) string + // Exemplar writes the exemplar of the current sample into the passed + // exemplar. It returns if an exemplar exists or not. + Exemplar(l *exemplar.Exemplar) bool + // Next advances the parser to the next sample. It returns false if no // more samples were read or an error occurred. Next() (Entry, error) diff --git a/pkg/textparse/openmetricslex.l b/pkg/textparse/openmetricslex.l index a259885f10..b02e975835 100644 --- a/pkg/textparse/openmetricslex.l +++ b/pkg/textparse/openmetricslex.l @@ -36,7 +36,7 @@ M [a-zA-Z_:] C [^\n] S [ ] -%x sComment sMeta1 sMeta2 sLabels sLValue sValue sTimestamp +%x sComment sMeta1 sMeta2 sLabels sLValue sValue sTimestamp sExemplar sEValue sETimestamp %yyc c %yyn c = l.next() @@ -62,8 +62,17 @@ S [ ] \"(\\.|[^\\"\n])*\" l.state = sLabels; return tLValue {S}[^ \n]+ l.state = sTimestamp; return tValue {S}[^ \n]+ return tTimestamp -{S}#{S}{C}*\n l.state = sInit; return tLinebreak \n l.state = sInit; return tLinebreak +{S}#{S}\{ l.state = sExemplar; return tComment + +{L}({L}|{D})* return tLName +\} l.state = sEValue; return tBraceClose += l.state = sEValue; return tEqual +\"(\\.|[^\\"\n])*\" l.state = sExemplar; return tLValue +, return tComma +{S}[^ \n]+ l.state = sETimestamp; return tValue +{S}[^ \n]+ return tTimestamp +\n l.state = sInit; return tLinebreak %% diff --git a/pkg/textparse/openmetricslex.l.go b/pkg/textparse/openmetricslex.l.go index 150d44dd05..e20d127cda 100644 --- a/pkg/textparse/openmetricslex.l.go +++ b/pkg/textparse/openmetricslex.l.go @@ -1,4 +1,4 @@ -// CAUTION: Generated file - DO NOT EDIT. +// Code generated by golex. DO NOT EDIT. // Copyright 2018 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +16,7 @@ package textparse import ( - "github.com/pkg/errors" + "fmt" ) // Lex is called by the parser generated by "go tool yacc" to obtain each @@ -33,7 +33,7 @@ yystate0: switch yyt := l.state; yyt { default: - panic(errors.Errorf(`invalid start condition %d`, yyt)) + panic(fmt.Errorf(`invalid start condition %d`, yyt)) case 0: // start condition: INITIAL goto yystart1 case 1: // start condition: sComment @@ -50,6 +50,12 @@ yystate0: goto yystart39 case 7: // start condition: sTimestamp goto yystart43 + case 8: // start condition: sExemplar + goto yystart50 + case 9: // start condition: sEValue + goto yystart55 + case 10: // start condition: sETimestamp + goto yystart61 } goto yystate0 // silence unused label error @@ -427,7 +433,7 @@ yystart43: yystate44: c = l.next() - goto yyrule18 + goto yyrule17 yystate45: c = l.next() @@ -465,15 +471,143 @@ yystate48: switch { default: goto yyabort - case c == '\n': + case c == '{': goto yystate49 - case c >= '\x01' && c <= '\t' || c >= '\v' && c <= 'ÿ': - goto yystate48 } yystate49: c = l.next() - goto yyrule17 + goto yyrule18 + + goto yystate50 // silence unused label error +yystate50: + c = l.next() +yystart50: + switch { + default: + goto yyabort + case c == ',': + goto yystate51 + case c == '=': + goto yystate52 + case c == '}': + goto yystate54 + case c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z': + goto yystate53 + } + +yystate51: + c = l.next() + goto yyrule23 + +yystate52: + c = l.next() + goto yyrule21 + +yystate53: + c = l.next() + switch { + default: + goto yyrule19 + case c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c == '_' || c >= 'a' && c <= 'z': + goto yystate53 + } + +yystate54: + c = l.next() + goto yyrule20 + + goto yystate55 // silence unused label error +yystate55: + c = l.next() +yystart55: + switch { + default: + goto yyabort + case c == ' ': + goto yystate56 + case c == '"': + goto yystate58 + } + +yystate56: + c = l.next() + switch { + default: + goto yyabort + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ': + goto yystate57 + } + +yystate57: + c = l.next() + switch { + default: + goto yyrule24 + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ': + goto yystate57 + } + +yystate58: + c = l.next() + switch { + default: + goto yyabort + case c == '"': + goto yystate59 + case c == '\\': + goto yystate60 + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '!' || c >= '#' && c <= '[' || c >= ']' && c <= 'ÿ': + goto yystate58 + } + +yystate59: + c = l.next() + goto yyrule22 + +yystate60: + c = l.next() + switch { + default: + goto yyabort + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= 'ÿ': + goto yystate58 + } + + goto yystate61 // silence unused label error +yystate61: + c = l.next() +yystart61: + switch { + default: + goto yyabort + case c == ' ': + goto yystate63 + case c == '\n': + goto yystate62 + } + +yystate62: + c = l.next() + goto yyrule26 + +yystate63: + c = l.next() + switch { + default: + goto yyabort + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ': + goto yystate64 + } + +yystate64: + c = l.next() + switch { + default: + goto yyrule25 + case c >= '\x01' && c <= '\t' || c >= '\v' && c <= '\x1f' || c >= '!' && c <= 'ÿ': + goto yystate64 + } yyrule1: // #{S} { @@ -564,13 +698,55 @@ yyrule16: // {S}[^ \n]+ { return tTimestamp } -yyrule17: // {S}#{S}{C}*\n +yyrule17: // \n { l.state = sInit return tLinebreak goto yystate0 } -yyrule18: // \n +yyrule18: // {S}#{S}\{ + { + l.state = sExemplar + return tComment + goto yystate0 + } +yyrule19: // {L}({L}|{D})* + { + return tLName + } +yyrule20: // \} + { + l.state = sEValue + return tBraceClose + goto yystate0 + } +yyrule21: // = + { + l.state = sEValue + return tEqual + goto yystate0 + } +yyrule22: // \"(\\.|[^\\"\n])*\" + { + l.state = sExemplar + return tLValue + goto yystate0 + } +yyrule23: // , + { + return tComma + } +yyrule24: // {S}[^ \n]+ + { + l.state = sETimestamp + return tValue + goto yystate0 + } +yyrule25: // {S}[^ \n]+ + { + return tTimestamp + } +yyrule26: // \n { l.state = sInit return tLinebreak diff --git a/pkg/textparse/openmetricsparse.go b/pkg/textparse/openmetricsparse.go index ed76bc3957..19a8a32ea8 100644 --- a/pkg/textparse/openmetricsparse.go +++ b/pkg/textparse/openmetricsparse.go @@ -17,6 +17,8 @@ package textparse import ( + "bytes" + "fmt" "io" "math" "sort" @@ -25,10 +27,13 @@ import ( "github.com/pkg/errors" + "github.com/prometheus/prometheus/pkg/exemplar" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/value" ) +var allowedSuffixes = [][]byte{[]byte("_total"), []byte("_bucket")} + type openMetricsLexer struct { b []byte i int @@ -85,6 +90,12 @@ type OpenMetricsParser struct { hasTS bool start int offsets []int + + eOffsets []int + exemplar []byte + exemplarVal float64 + exemplarTs int64 + hasExemplarTs bool } // NewOpenMetricsParser returns a new parser of the byte slice. @@ -96,7 +107,8 @@ func NewOpenMetricsParser(b []byte) Parser { // of the current sample. func (p *OpenMetricsParser) Series() ([]byte, *int64, float64) { if p.hasTS { - return p.series, &p.ts, p.val + ts := p.ts + return p.series, &ts, p.val } return p.series, nil, p.val } @@ -170,6 +182,38 @@ func (p *OpenMetricsParser) Metric(l *labels.Labels) string { return s } +// Exemplar writes the exemplar of the current sample into the passed +// exemplar. It returns the whether an exemplar exists. +func (p *OpenMetricsParser) Exemplar(e *exemplar.Exemplar) bool { + if len(p.exemplar) == 0 { + return false + } + + // Allocate the full immutable string immediately, so we just + // have to create references on it below. + s := string(p.exemplar) + + e.Value = p.exemplarVal + if p.hasExemplarTs { + e.HasTs = true + e.Ts = p.exemplarTs + } + + for i := 0; i < len(p.eOffsets); i += 4 { + a := p.eOffsets[i] - p.start + b := p.eOffsets[i+1] - p.start + c := p.eOffsets[i+2] - p.start + d := p.eOffsets[i+3] - p.start + + e.Labels = append(e.Labels, labels.Label{Name: s[a:b], Value: s[c:d]}) + } + + // Sort the labels. + sort.Sort(e.Labels) + + return true +} + // nextToken returns the next token from the openMetricsLexer. func (p *OpenMetricsParser) nextToken() token { tok := p.l.Lex() @@ -183,6 +227,10 @@ func (p *OpenMetricsParser) Next() (Entry, error) { p.start = p.l.i p.offsets = p.offsets[:0] + p.eOffsets = p.eOffsets[:0] + p.exemplar = p.exemplar[:0] + p.exemplarVal = 0 + p.hasExemplarTs = false switch t := p.nextToken(); t { case tEofWord: @@ -191,7 +239,7 @@ func (p *OpenMetricsParser) Next() (Entry, error) { } return EntryInvalid, io.EOF case tEOF: - return EntryInvalid, parseError("unexpected end of data", t) + return EntryInvalid, io.EOF case tHelp, tType, tUnit: switch t := p.nextToken(); t { case tMName: @@ -258,26 +306,29 @@ func (p *OpenMetricsParser) Next() (Entry, error) { t2 := p.nextToken() if t2 == tBraceOpen { - if err := p.parseLVals(); err != nil { + offsets, err := p.parseLVals() + if err != nil { return EntryInvalid, err } + p.offsets = append(p.offsets, offsets...) p.series = p.l.b[p.start:p.l.i] t2 = p.nextToken() } - if t2 != tValue { - return EntryInvalid, parseError("expected value after metric", t) - } - if p.val, err = parseFloat(yoloString(p.l.buf()[1:])); err != nil { + p.val, err = p.getFloatValue(t2, "metric") + if err != nil { return EntryInvalid, err } - // Ensure canonical NaN value. - if math.IsNaN(p.val) { - p.val = math.Float64frombits(value.NormalNaN) - } + p.hasTS = false - switch p.nextToken() { + switch t2 := p.nextToken(); t2 { + case tEOF: + return EntryInvalid, io.EOF case tLinebreak: break + case tComment: + if err := p.parseComment(); err != nil { + return EntryInvalid, err + } case tTimestamp: p.hasTS = true var ts float64 @@ -286,11 +337,17 @@ func (p *OpenMetricsParser) Next() (Entry, error) { return EntryInvalid, err } p.ts = int64(ts * 1000) - if t2 := p.nextToken(); t2 != tLinebreak { - return EntryInvalid, parseError("expected next entry after timestamp", t) + switch t3 := p.nextToken(); t3 { + case tLinebreak: + case tComment: + if err := p.parseComment(); err != nil { + return EntryInvalid, err + } + default: + return EntryInvalid, parseError("expected next entry after timestamp", t3) } default: - return EntryInvalid, parseError("expected timestamp or new record", t) + return EntryInvalid, parseError("expected timestamp or # symbol", t2) } return EntrySeries, nil @@ -300,50 +357,121 @@ func (p *OpenMetricsParser) Next() (Entry, error) { return EntryInvalid, err } -func (p *OpenMetricsParser) parseLVals() error { +func (p *OpenMetricsParser) parseComment() error { + // Validate the name of the metric. It must have _total or _bucket as + // suffix for exemplars to be supported. + if err := p.validateNameForExemplar(p.series[:p.offsets[0]-p.start]); err != nil { + return err + } + + // Parse the labels. + offsets, err := p.parseLVals() + if err != nil { + return err + } + p.eOffsets = append(p.eOffsets, offsets...) + p.exemplar = p.l.b[p.start:p.l.i] + + // Get the value. + p.exemplarVal, err = p.getFloatValue(p.nextToken(), "exemplar labels") + if err != nil { + return err + } + + // Read the optional timestamp. + p.hasExemplarTs = false + switch t2 := p.nextToken(); t2 { + case tEOF: + return io.EOF + case tLinebreak: + break + case tTimestamp: + p.hasExemplarTs = true + var ts float64 + // A float is enough to hold what we need for millisecond resolution. + if ts, err = parseFloat(yoloString(p.l.buf()[1:])); err != nil { + return err + } + p.exemplarTs = int64(ts * 1000) + switch t3 := p.nextToken(); t3 { + case tLinebreak: + default: + return parseError("expected next entry after exemplar timestamp", t3) + } + default: + return parseError("expected timestamp or comment", t2) + } + return nil +} + +func (p *OpenMetricsParser) parseLVals() ([]int, error) { + var offsets []int first := true for { t := p.nextToken() switch t { case tBraceClose: - return nil + return offsets, nil case tComma: if first { - return parseError("expected label name or left brace", t) + return nil, parseError("expected label name or left brace", t) } t = p.nextToken() if t != tLName { - return parseError("expected label name", t) + return nil, parseError("expected label name", t) } case tLName: if !first { - return parseError("expected comma", t) + return nil, parseError("expected comma", t) } default: if first { - return parseError("expected label name or left brace", t) + return nil, parseError("expected label name or left brace", t) } - return parseError("expected comma or left brace", t) + return nil, parseError("expected comma or left brace", t) } first = false // t is now a label name. - p.offsets = append(p.offsets, p.l.start, p.l.i) + offsets = append(offsets, p.l.start, p.l.i) if t := p.nextToken(); t != tEqual { - return parseError("expected equal", t) + return nil, parseError("expected equal", t) } if t := p.nextToken(); t != tLValue { - return parseError("expected label value", t) + return nil, parseError("expected label value", t) } if !utf8.Valid(p.l.buf()) { - return errors.New("invalid UTF-8 label value") + return nil, errors.New("invalid UTF-8 label value") } // The openMetricsLexer ensures the value string is quoted. Strip first // and last character. - p.offsets = append(p.offsets, p.l.start+1, p.l.i-1) - + offsets = append(offsets, p.l.start+1, p.l.i-1) } } + +func (p *OpenMetricsParser) getFloatValue(t token, after string) (float64, error) { + if t != tValue { + return 0, parseError(fmt.Sprintf("expected value after %v", after), t) + } + val, err := parseFloat(yoloString(p.l.buf()[1:])) + if err != nil { + return 0, err + } + // Ensure canonical NaN value. + if math.IsNaN(p.exemplarVal) { + val = math.Float64frombits(value.NormalNaN) + } + return val, nil +} + +func (p *OpenMetricsParser) validateNameForExemplar(name []byte) error { + for _, suffix := range allowedSuffixes { + if bytes.HasSuffix(name, suffix) { + return nil + } + } + return fmt.Errorf("metric name %v does not support exemplars", string(name)) +} diff --git a/pkg/textparse/openmetricsparse_test.go b/pkg/textparse/openmetricsparse_test.go index 6bb6bdfab9..63532ffb25 100644 --- a/pkg/textparse/openmetricsparse_test.go +++ b/pkg/textparse/openmetricsparse_test.go @@ -17,6 +17,7 @@ import ( "io" "testing" + "github.com/prometheus/prometheus/pkg/exemplar" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/util/testutil" ) @@ -38,9 +39,13 @@ some:aggregate:rate5m{a_b="c"} 1 # TYPE go_goroutines gauge go_goroutines 33 123.123 # TYPE hh histogram -hh_bucket{le="+Inf"} 1 # {} 4 +hh_bucket{le="+Inf"} 1 # TYPE gh gaugehistogram -gh_bucket{le="+Inf"} 1 # {} 4 +gh_bucket{le="+Inf"} 1 +# TYPE hhh histogram +hhh_bucket{le="+Inf"} 1 # {aa="bb"} 4 +# TYPE ggh gaugehistogram +ggh_bucket{le="+Inf"} 1 # {cc="dd",xx="yy"} 4 123.123 # TYPE ii info ii{foo="bar"} 1 # TYPE ss stateset @@ -49,7 +54,9 @@ ss{ss="bar"} 0 # TYPE un unknown _metric_starting_with_underscore 1 testmetric{_label_starting_with_underscore="foo"} 1 -testmetric{label="\"bar\""} 1` +testmetric{label="\"bar\""} 1 +# TYPE foo counter +foo_total 17.0 1520879607.789 # {xx="yy"} 5` input += "\n# HELP metric foo\x00bar" input += "\nnull_byte_metric{a=\"abc\x00\"} 1" @@ -66,6 +73,7 @@ testmetric{label="\"bar\""} 1` help string unit string comment string + e *exemplar.Exemplar }{ { m: "go_gc_duration_seconds", @@ -134,6 +142,22 @@ testmetric{label="\"bar\""} 1` m: `gh_bucket{le="+Inf"}`, v: 1, lset: labels.FromStrings("__name__", "gh_bucket", "le", "+Inf"), + }, { + m: "hhh", + typ: MetricTypeHistogram, + }, { + m: `hhh_bucket{le="+Inf"}`, + v: 1, + lset: labels.FromStrings("__name__", "hhh_bucket", "le", "+Inf"), + e: &exemplar.Exemplar{Labels: labels.FromStrings("aa", "bb"), Value: 4}, + }, { + m: "ggh", + typ: MetricTypeGaugeHistogram, + }, { + m: `ggh_bucket{le="+Inf"}`, + v: 1, + lset: labels.FromStrings("__name__", "ggh_bucket", "le", "+Inf"), + e: &exemplar.Exemplar{Labels: labels.FromStrings("cc", "dd", "xx", "yy"), Value: 4, HasTs: true, Ts: 123123}, }, { m: "ii", typ: MetricTypeInfo, @@ -167,6 +191,15 @@ testmetric{label="\"bar\""} 1` m: "testmetric{label=\"\\\"bar\\\"\"}", v: 1, lset: labels.FromStrings("__name__", "testmetric", "label", `"bar"`), + }, { + m: "foo", + typ: MetricTypeCounter, + }, { + m: "foo_total", + v: 17, + lset: labels.FromStrings("__name__", "foo_total"), + t: int64p(1520879607789), + e: &exemplar.Exemplar{Labels: labels.FromStrings("xx", "yy"), Value: 5}, }, { m: "metric", help: "foo\x00bar", @@ -193,12 +226,20 @@ testmetric{label="\"bar\""} 1` case EntrySeries: m, ts, v := p.Series() + var e exemplar.Exemplar p.Metric(&res) + found := p.Exemplar(&e) testutil.Equals(t, exp[i].m, string(m)) testutil.Equals(t, exp[i].t, ts) testutil.Equals(t, exp[i].v, v) testutil.Equals(t, exp[i].lset, res) + if exp[i].e == nil { + testutil.Equals(t, false, found) + } else { + testutil.Equals(t, true, found) + testutil.Equals(t, *exp[i].e, e) + } res = res[:0] case EntryType: @@ -232,11 +273,11 @@ func TestOpenMetricsParseErrors(t *testing.T) { }{ { input: "", - err: "unexpected end of data, got \"EOF\"", + err: "EOF", }, { input: "a", - err: "expected value after metric, got \"MNAME\"", + err: "expected value after metric, got \"EOF\"", }, { input: "\n", @@ -280,7 +321,7 @@ func TestOpenMetricsParseErrors(t *testing.T) { }, { input: "a\t1\n", - err: "expected value after metric, got \"MNAME\"", + err: "expected value after metric, got \"INVALID\"", }, { input: "a 1\t2\n", @@ -288,11 +329,11 @@ func TestOpenMetricsParseErrors(t *testing.T) { }, { input: "a 1 2 \n", - err: "expected next entry after timestamp, got \"MNAME\"", + err: "expected next entry after timestamp, got \"INVALID\"", }, { input: "a 1 2 #\n", - err: "expected next entry after timestamp, got \"MNAME\"", + err: "expected next entry after timestamp, got \"TIMESTAMP\"", }, { input: "a 1 1z\n", @@ -324,7 +365,7 @@ func TestOpenMetricsParseErrors(t *testing.T) { }, { input: "a 1 1 1\n", - err: "expected next entry after timestamp, got \"MNAME\"", + err: "expected next entry after timestamp, got \"TIMESTAMP\"", }, { input: "a{b='c'} 1\n", @@ -386,6 +427,42 @@ func TestOpenMetricsParseErrors(t *testing.T) { input: "foo 0 1_2\n", err: "unsupported character in float", }, + { + input: "custom_metric_total 1 # {aa=bb}", + err: "expected label value, got \"INVALID\"", + }, + { + input: `custom_metric_total 1 # {aa="bb"}`, + err: "expected value after exemplar labels, got \"EOF\"", + }, + { + input: `custom_metric 1 # {aa="bb"}`, + err: "metric name custom_metric does not support exemplars", + }, + { + input: `custom_metric_total 1 # {aa="bb",,cc="dd"} 1`, + err: "expected label name, got \"COMMA\"", + }, + { + input: `custom_metric_total 1 # {aa="bb"} 1_2`, + err: "unsupported character in float", + }, + { + input: `custom_metric_total 1 # {aa="bb"} 0x1p-3`, + err: "unsupported character in float", + }, + { + input: `custom_metric_total 1 # {aa="bb"} true`, + err: "strconv.ParseFloat: parsing \"true\": invalid syntax", + }, + { + input: `custom_metric_total 1 # {aa="bb",cc=}`, + err: "expected label value, got \"INVALID\"", + }, + { + input: `custom_metric_total 1 # {aa=\"\xff\"} 9.0`, + err: "expected label value, got \"INVALID\"", + }, } for i, c := range cases { @@ -433,7 +510,7 @@ func TestOMNullByteHandling(t *testing.T) { }, { input: "a\x00{b=\"ddd\"} 1", - err: "expected value after metric, got \"MNAME\"", + err: "expected value after metric, got \"INVALID\"", }, { input: "#", @@ -443,6 +520,14 @@ func TestOMNullByteHandling(t *testing.T) { input: "# H", err: "\"INVALID\" \" \" is not a valid start token", }, + { + input: "custom_metric_total 1 # {b=\x00\"ssss\"} 1\n", + err: "expected label value, got \"INVALID\"", + }, + { + input: "custom_metric_total 1 # {b=\"\x00ss\"} 1\n", + err: "expected label value, got \"INVALID\"", + }, } for i, c := range cases { diff --git a/pkg/textparse/promlex.l.go b/pkg/textparse/promlex.l.go index f24f0452fd..690ec4e05b 100644 --- a/pkg/textparse/promlex.l.go +++ b/pkg/textparse/promlex.l.go @@ -28,6 +28,9 @@ const ( sLValue sValue sTimestamp + sExemplar + sEValue + sETimestamp ) // Lex is called by the parser generated by "go tool yacc" to obtain each diff --git a/pkg/textparse/promparse.go b/pkg/textparse/promparse.go index 69fa51c3dd..6c254b5261 100644 --- a/pkg/textparse/promparse.go +++ b/pkg/textparse/promparse.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" + "github.com/prometheus/prometheus/pkg/exemplar" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/value" ) @@ -234,6 +235,12 @@ func (p *PromParser) Metric(l *labels.Labels) string { return s } +// Exemplar writes the exemplar of the current sample into the passed +// exemplar. It returns if an exemplar exists. +func (p *PromParser) Exemplar(e *exemplar.Exemplar) bool { + return false +} + // nextToken returns the next token from the promlexer. It skips over tabs // and spaces. func (p *PromParser) nextToken() token {