Add exemplar support to the openmetrics parser (#6292)

* Add exemplar support to the openmetrics parser

Signed-off-by: Shreyas Srivatsan <shreyas@chronosphere.io>
This commit is contained in:
shreyassrivatsan 2019-11-19 01:33:30 -08:00 committed by Brian Brazil
parent ad4bc5701e
commit e825282dd1
8 changed files with 487 additions and 50 deletions

24
pkg/exemplar/exemplar.go Normal file
View file

@ -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
}

View file

@ -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)

View file

@ -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 [ ]
<sLValue>\"(\\.|[^\\"\n])*\" l.state = sLabels; return tLValue
<sValue>{S}[^ \n]+ l.state = sTimestamp; return tValue
<sTimestamp>{S}[^ \n]+ return tTimestamp
<sTimestamp>{S}#{S}{C}*\n l.state = sInit; return tLinebreak
<sTimestamp>\n l.state = sInit; return tLinebreak
<sTimestamp>{S}#{S}\{ l.state = sExemplar; return tComment
<sExemplar>{L}({L}|{D})* return tLName
<sExemplar>\} l.state = sEValue; return tBraceClose
<sExemplar>= l.state = sEValue; return tEqual
<sEValue>\"(\\.|[^\\"\n])*\" l.state = sExemplar; return tLValue
<sExemplar>, return tComma
<sEValue>{S}[^ \n]+ l.state = sETimestamp; return tValue
<sETimestamp>{S}[^ \n]+ return tTimestamp
<sETimestamp>\n l.state = sInit; return tLinebreak
%%

View file

@ -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

View file

@ -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))
}

View file

@ -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 {

View file

@ -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

View file

@ -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 {