Skip to content

Commit fc488aa

Browse files
authored
Decoder: Remove use of global name validation and add validation (#808)
This change removes the use of the global variable to determine name validation, using the provided format instead. Also: * adds validity checks to the text parser, which previously did not check for name validity. * Returns error if the caller tries to create a decoder for prototext format, which is not supported. Signed-off-by: Owen Williams <[email protected]>
1 parent 918f899 commit fc488aa

File tree

6 files changed

+270
-88
lines changed

6 files changed

+270
-88
lines changed

expfmt/bench_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626

2727
dto "github.com/prometheus/client_model/go"
2828
"github.com/stretchr/testify/require"
29+
30+
"github.com/prometheus/common/model"
2931
)
3032

31-
var parser TextParser
33+
var parser = TextParser{scheme: model.UTF8Validation}
3234

3335
// Benchmarks to show how much penalty text format parsing actually inflicts.
3436
//

expfmt/decode.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,34 @@ func ResponseFormat(h http.Header) Format {
7070
return FmtUnknown
7171
}
7272

73-
// NewDecoder returns a new decoder based on the given input format.
74-
// If the input format does not imply otherwise, a text format decoder is returned.
73+
// NewDecoder returns a new decoder based on the given input format. Metric
74+
// names are validated based on the provided Format -- if the format requires
75+
// escaping, raditional Prometheues validity checking is used. Otherwise, names
76+
// are checked for UTF-8 validity. Supported formats include delimited protobuf
77+
// and Prometheus text format. For historical reasons, this decoder fallbacks
78+
// to classic text decoding for any other format. This decoder does not fully
79+
// support OpenMetrics although it may often succeed due to the similarities
80+
// between the formats. This decoder may not support the latest features of
81+
// Prometheus text format and is not intended for high-performance applications.
82+
// See: https://github.com/prometheus/common/issues/812
7583
func NewDecoder(r io.Reader, format Format) Decoder {
84+
scheme := model.LegacyValidation
85+
if format.ToEscapingScheme() == model.NoEscaping {
86+
scheme = model.UTF8Validation
87+
}
7688
switch format.FormatType() {
7789
case TypeProtoDelim:
78-
return &protoDecoder{r: bufio.NewReader(r)}
90+
return &protoDecoder{r: bufio.NewReader(r), s: scheme}
91+
case TypeProtoText, TypeProtoCompact:
92+
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
7993
}
80-
return &textDecoder{r: r}
94+
return &textDecoder{r: r, s: scheme}
8195
}
8296

8397
// protoDecoder implements the Decoder interface for protocol buffers.
8498
type protoDecoder struct {
8599
r protodelim.Reader
100+
s model.ValidationScheme
86101
}
87102

88103
// Decode implements the Decoder interface.
@@ -93,8 +108,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
93108
if err := opts.UnmarshalFrom(d.r, v); err != nil {
94109
return err
95110
}
96-
//nolint:staticcheck // model.IsValidMetricName is deprecated.
97-
if !model.IsValidMetricName(model.LabelValue(v.GetName())) {
111+
if !d.s.IsValidMetricName(v.GetName()) {
98112
return fmt.Errorf("invalid metric name %q", v.GetName())
99113
}
100114
for _, m := range v.GetMetric() {
@@ -108,27 +122,36 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
108122
if !model.LabelValue(l.GetValue()).IsValid() {
109123
return fmt.Errorf("invalid label value %q", l.GetValue())
110124
}
111-
//nolint:staticcheck // model.LabelName.IsValid is deprecated.
112-
if !model.LabelName(l.GetName()).IsValid() {
125+
if !d.s.IsValidLabelName(l.GetName()) {
113126
return fmt.Errorf("invalid label name %q", l.GetName())
114127
}
115128
}
116129
}
117130
return nil
118131
}
119132

133+
// errDecoder is an error-state decoder that always returns the same error.
134+
type errDecoder struct {
135+
err error
136+
}
137+
138+
func (d *errDecoder) Decode(v *dto.MetricFamily) error {
139+
return d.err
140+
}
141+
120142
// textDecoder implements the Decoder interface for the text protocol.
121143
type textDecoder struct {
122144
r io.Reader
123145
fams map[string]*dto.MetricFamily
146+
s model.ValidationScheme
124147
err error
125148
}
126149

127150
// Decode implements the Decoder interface.
128151
func (d *textDecoder) Decode(v *dto.MetricFamily) error {
129152
if d.err == nil {
130153
// Read all metrics in one shot.
131-
var p TextParser
154+
p := TextParser{scheme: d.s}
132155
d.fams, d.err = p.TextToMetricFamilies(d.r)
133156
// If we don't get an error, store io.EOF for the end.
134157
if d.err == nil {

expfmt/decode_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ mf2 4
8080
)
8181

8282
dec := &SampleDecoder{
83-
Dec: &textDecoder{r: strings.NewReader(in)},
83+
Dec: &textDecoder{
84+
s: model.UTF8Validation,
85+
r: strings.NewReader(in),
86+
},
8487
Opts: &DecodeOptions{
8588
Timestamp: ts,
8689
},
@@ -361,25 +364,23 @@ func TestProtoDecoder(t *testing.T) {
361364

362365
for i, scenario := range scenarios {
363366
dec := &SampleDecoder{
364-
Dec: &protoDecoder{r: strings.NewReader(scenario.in)},
367+
Dec: &protoDecoder{r: strings.NewReader(scenario.in), s: model.LegacyValidation},
365368
Opts: &DecodeOptions{
366369
Timestamp: testTime,
367370
},
368371
}
369372

370373
var all model.Vector
371374
for {
372-
model.NameValidationScheme = model.LegacyValidation //nolint:staticcheck
373375
var smpls model.Vector
374376
err := dec.Decode(&smpls)
375377
if err != nil && errors.Is(err, io.EOF) {
376378
break
377379
}
378380
if scenario.legacyNameFail {
379381
require.Errorf(t, err, "Expected error when decoding without UTF-8 support enabled but got none")
380-
model.NameValidationScheme = model.UTF8Validation //nolint:staticcheck
381382
dec = &SampleDecoder{
382-
Dec: &protoDecoder{r: strings.NewReader(scenario.in)},
383+
Dec: &protoDecoder{r: strings.NewReader(scenario.in), s: model.UTF8Validation},
383384
Opts: &DecodeOptions{
384385
Timestamp: testTime,
385386
},

expfmt/encode_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,93 @@ func TestEscapedEncode(t *testing.T) {
376376
})
377377
}
378378
}
379+
380+
func TestDottedEncode(t *testing.T) {
381+
//nolint:staticcheck
382+
model.NameValidationScheme = model.UTF8Validation
383+
metric := &dto.MetricFamily{
384+
Name: proto.String("foo.metric"),
385+
Type: dto.MetricType_COUNTER.Enum(),
386+
Metric: []*dto.Metric{
387+
{
388+
Counter: &dto.Counter{
389+
Value: proto.Float64(1.234),
390+
},
391+
},
392+
{
393+
Label: []*dto.LabelPair{
394+
{
395+
Name: proto.String("dotted.label.name"),
396+
Value: proto.String("my.label.value"),
397+
},
398+
},
399+
Counter: &dto.Counter{
400+
Value: proto.Float64(8),
401+
},
402+
},
403+
},
404+
}
405+
406+
scenarios := []struct {
407+
format Format
408+
expectMetricName string
409+
expectLabelName string
410+
}{
411+
{
412+
format: FmtProtoDelim,
413+
expectMetricName: "foo_metric",
414+
expectLabelName: "dotted_label_name",
415+
},
416+
{
417+
format: FmtProtoDelim.WithEscapingScheme(model.NoEscaping),
418+
expectMetricName: "foo.metric",
419+
expectLabelName: "dotted.label.name",
420+
},
421+
{
422+
format: FmtProtoDelim.WithEscapingScheme(model.DotsEscaping),
423+
expectMetricName: "foo_dot_metric",
424+
expectLabelName: "dotted_dot_label_dot_name",
425+
},
426+
{
427+
format: FmtText,
428+
expectMetricName: "foo_metric",
429+
expectLabelName: "dotted_label_name",
430+
},
431+
{
432+
format: FmtText.WithEscapingScheme(model.NoEscaping),
433+
expectMetricName: "foo.metric",
434+
expectLabelName: "dotted.label.name",
435+
},
436+
{
437+
format: FmtText.WithEscapingScheme(model.DotsEscaping),
438+
expectMetricName: "foo_dot_metric",
439+
expectLabelName: "dotted_dot_label_dot_name",
440+
},
441+
// common library does not support proto text or open metrics parsing so we
442+
// do not test those here.
443+
}
444+
445+
for i, scenario := range scenarios {
446+
out := bytes.NewBuffer(make([]byte, 0))
447+
enc := NewEncoder(out, scenario.format)
448+
err := enc.Encode(metric)
449+
if err != nil {
450+
t.Errorf("%d. error: %s", i, err)
451+
continue
452+
}
453+
454+
dec := NewDecoder(bytes.NewReader(out.Bytes()), scenario.format)
455+
var gotFamily dto.MetricFamily
456+
err = dec.Decode(&gotFamily)
457+
if err != nil {
458+
t.Errorf("%v: unexpected error during decode: %s", scenario.format, err.Error())
459+
}
460+
if gotFamily.GetName() != scenario.expectMetricName {
461+
t.Errorf("%v: incorrect encoded metric name, want %v, got %v", scenario.format, scenario.expectMetricName, gotFamily.GetName())
462+
}
463+
lName := gotFamily.GetMetric()[1].Label[0].GetName()
464+
if lName != scenario.expectLabelName {
465+
t.Errorf("%v: incorrect encoded label name, want %v, got %v", scenario.format, scenario.expectLabelName, lName)
466+
}
467+
}
468+
}

expfmt/text_parse.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ type TextParser struct {
7878
// These indicate if the metric name from the current line being parsed is inside
7979
// braces and if that metric name was found respectively.
8080
currentMetricIsInsideBraces, currentMetricInsideBracesIsPresent bool
81+
// scheme sets the desired ValidationScheme for names. Defaults to the invalid
82+
// UnsetValidation.
83+
scheme model.ValidationScheme
8184
}
8285

8386
// TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
@@ -126,6 +129,7 @@ func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricF
126129

127130
func (p *TextParser) reset(in io.Reader) {
128131
p.metricFamiliesByName = map[string]*dto.MetricFamily{}
132+
p.currentLabelPairs = nil
129133
if p.buf == nil {
130134
p.buf = bufio.NewReader(in)
131135
} else {
@@ -216,6 +220,9 @@ func (p *TextParser) startComment() stateFn {
216220
return nil
217221
}
218222
p.setOrCreateCurrentMF()
223+
if p.err != nil {
224+
return nil
225+
}
219226
if p.skipBlankTab(); p.err != nil {
220227
return nil // Unexpected end of input.
221228
}
@@ -244,6 +251,9 @@ func (p *TextParser) readingMetricName() stateFn {
244251
return nil
245252
}
246253
p.setOrCreateCurrentMF()
254+
if p.err != nil {
255+
return nil
256+
}
247257
// Now is the time to fix the type if it hasn't happened yet.
248258
if p.currentMF.Type == nil {
249259
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
@@ -311,6 +321,9 @@ func (p *TextParser) startLabelName() stateFn {
311321
switch p.currentByte {
312322
case ',':
313323
p.setOrCreateCurrentMF()
324+
if p.err != nil {
325+
return nil
326+
}
314327
if p.currentMF.Type == nil {
315328
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
316329
}
@@ -319,6 +332,10 @@ func (p *TextParser) startLabelName() stateFn {
319332
return p.startLabelName
320333
case '}':
321334
p.setOrCreateCurrentMF()
335+
if p.err != nil {
336+
p.currentLabelPairs = nil
337+
return nil
338+
}
322339
if p.currentMF.Type == nil {
323340
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
324341
}
@@ -341,6 +358,12 @@ func (p *TextParser) startLabelName() stateFn {
341358
p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())}
342359
if p.currentLabelPair.GetName() == string(model.MetricNameLabel) {
343360
p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
361+
p.currentLabelPairs = nil
362+
return nil
363+
}
364+
if !p.scheme.IsValidLabelName(p.currentLabelPair.GetName()) {
365+
p.parseError(fmt.Sprintf("invalid label name %q", p.currentLabelPair.GetName()))
366+
p.currentLabelPairs = nil
344367
return nil
345368
}
346369
// Special summary/histogram treatment. Don't add 'quantile' and 'le'
@@ -805,6 +828,10 @@ func (p *TextParser) setOrCreateCurrentMF() {
805828
p.currentIsHistogramCount = false
806829
p.currentIsHistogramSum = false
807830
name := p.currentToken.String()
831+
if !p.scheme.IsValidMetricName(name) {
832+
p.parseError(fmt.Sprintf("invalid metric name %q", name))
833+
return
834+
}
808835
if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil {
809836
return
810837
}

0 commit comments

Comments
 (0)