1// Copyright 2014 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package expfmt
15
16import (
17	"math"
18	"strings"
19	"testing"
20
21	"github.com/golang/protobuf/proto"
22	dto "github.com/prometheus/client_model/go"
23)
24
25func testTextParse(t testing.TB) {
26	var scenarios = []struct {
27		in  string
28		out []*dto.MetricFamily
29	}{
30		// 0: Empty lines as input.
31		{
32			in: `
33
34`,
35			out: []*dto.MetricFamily{},
36		},
37		// 1: Minimal case.
38		{
39			in: `
40minimal_metric 1.234
41another_metric -3e3 103948
42# Even that:
43no_labels{} 3
44# HELP line for non-existing metric will be ignored.
45`,
46			out: []*dto.MetricFamily{
47				&dto.MetricFamily{
48					Name: proto.String("minimal_metric"),
49					Type: dto.MetricType_UNTYPED.Enum(),
50					Metric: []*dto.Metric{
51						&dto.Metric{
52							Untyped: &dto.Untyped{
53								Value: proto.Float64(1.234),
54							},
55						},
56					},
57				},
58				&dto.MetricFamily{
59					Name: proto.String("another_metric"),
60					Type: dto.MetricType_UNTYPED.Enum(),
61					Metric: []*dto.Metric{
62						&dto.Metric{
63							Untyped: &dto.Untyped{
64								Value: proto.Float64(-3e3),
65							},
66							TimestampMs: proto.Int64(103948),
67						},
68					},
69				},
70				&dto.MetricFamily{
71					Name: proto.String("no_labels"),
72					Type: dto.MetricType_UNTYPED.Enum(),
73					Metric: []*dto.Metric{
74						&dto.Metric{
75							Untyped: &dto.Untyped{
76								Value: proto.Float64(3),
77							},
78						},
79					},
80				},
81			},
82		},
83		// 2: Counters & gauges, docstrings, various whitespace, escape sequences.
84		{
85			in: `
86# A normal comment.
87#
88# TYPE name counter
89name{labelname="val1",basename="basevalue"} NaN
90name {labelname="val2",basename="base\"v\\al\nue"} 0.23 1234567890
91# HELP name two-line\n doc  str\\ing
92
93 # HELP  name2  	doc str"ing 2
94  #    TYPE    name2 gauge
95name2{labelname="val2"	,basename   =   "basevalue2"		} +Inf 54321
96name2{ labelname = "val1" , }-Inf
97`,
98			out: []*dto.MetricFamily{
99				&dto.MetricFamily{
100					Name: proto.String("name"),
101					Help: proto.String("two-line\n doc  str\\ing"),
102					Type: dto.MetricType_COUNTER.Enum(),
103					Metric: []*dto.Metric{
104						&dto.Metric{
105							Label: []*dto.LabelPair{
106								&dto.LabelPair{
107									Name:  proto.String("labelname"),
108									Value: proto.String("val1"),
109								},
110								&dto.LabelPair{
111									Name:  proto.String("basename"),
112									Value: proto.String("basevalue"),
113								},
114							},
115							Counter: &dto.Counter{
116								Value: proto.Float64(math.NaN()),
117							},
118						},
119						&dto.Metric{
120							Label: []*dto.LabelPair{
121								&dto.LabelPair{
122									Name:  proto.String("labelname"),
123									Value: proto.String("val2"),
124								},
125								&dto.LabelPair{
126									Name:  proto.String("basename"),
127									Value: proto.String("base\"v\\al\nue"),
128								},
129							},
130							Counter: &dto.Counter{
131								Value: proto.Float64(.23),
132							},
133							TimestampMs: proto.Int64(1234567890),
134						},
135					},
136				},
137				&dto.MetricFamily{
138					Name: proto.String("name2"),
139					Help: proto.String("doc str\"ing 2"),
140					Type: dto.MetricType_GAUGE.Enum(),
141					Metric: []*dto.Metric{
142						&dto.Metric{
143							Label: []*dto.LabelPair{
144								&dto.LabelPair{
145									Name:  proto.String("labelname"),
146									Value: proto.String("val2"),
147								},
148								&dto.LabelPair{
149									Name:  proto.String("basename"),
150									Value: proto.String("basevalue2"),
151								},
152							},
153							Gauge: &dto.Gauge{
154								Value: proto.Float64(math.Inf(+1)),
155							},
156							TimestampMs: proto.Int64(54321),
157						},
158						&dto.Metric{
159							Label: []*dto.LabelPair{
160								&dto.LabelPair{
161									Name:  proto.String("labelname"),
162									Value: proto.String("val1"),
163								},
164							},
165							Gauge: &dto.Gauge{
166								Value: proto.Float64(math.Inf(-1)),
167							},
168						},
169					},
170				},
171			},
172		},
173		// 3: The evil summary, mixed with other types and funny comments.
174		{
175			in: `
176# TYPE my_summary summary
177my_summary{n1="val1",quantile="0.5"} 110
178decoy -1 -2
179my_summary{n1="val1",quantile="0.9"} 140 1
180my_summary_count{n1="val1"} 42
181# Latest timestamp wins in case of a summary.
182my_summary_sum{n1="val1"} 4711 2
183fake_sum{n1="val1"} 2001
184# TYPE another_summary summary
185another_summary_count{n2="val2",n1="val1"} 20
186my_summary_count{n2="val2",n1="val1"} 5 5
187another_summary{n1="val1",n2="val2",quantile=".3"} -1.2
188my_summary_sum{n1="val2"} 08 15
189my_summary{n1="val3", quantile="0.2"} 4711
190  my_summary{n1="val1",n2="val2",quantile="-12.34",} NaN
191# some
192# funny comments
193# HELP
194# HELP
195# HELP my_summary
196# HELP my_summary
197`,
198			out: []*dto.MetricFamily{
199				&dto.MetricFamily{
200					Name: proto.String("fake_sum"),
201					Type: dto.MetricType_UNTYPED.Enum(),
202					Metric: []*dto.Metric{
203						&dto.Metric{
204							Label: []*dto.LabelPair{
205								&dto.LabelPair{
206									Name:  proto.String("n1"),
207									Value: proto.String("val1"),
208								},
209							},
210							Untyped: &dto.Untyped{
211								Value: proto.Float64(2001),
212							},
213						},
214					},
215				},
216				&dto.MetricFamily{
217					Name: proto.String("decoy"),
218					Type: dto.MetricType_UNTYPED.Enum(),
219					Metric: []*dto.Metric{
220						&dto.Metric{
221							Untyped: &dto.Untyped{
222								Value: proto.Float64(-1),
223							},
224							TimestampMs: proto.Int64(-2),
225						},
226					},
227				},
228				&dto.MetricFamily{
229					Name: proto.String("my_summary"),
230					Type: dto.MetricType_SUMMARY.Enum(),
231					Metric: []*dto.Metric{
232						&dto.Metric{
233							Label: []*dto.LabelPair{
234								&dto.LabelPair{
235									Name:  proto.String("n1"),
236									Value: proto.String("val1"),
237								},
238							},
239							Summary: &dto.Summary{
240								SampleCount: proto.Uint64(42),
241								SampleSum:   proto.Float64(4711),
242								Quantile: []*dto.Quantile{
243									&dto.Quantile{
244										Quantile: proto.Float64(0.5),
245										Value:    proto.Float64(110),
246									},
247									&dto.Quantile{
248										Quantile: proto.Float64(0.9),
249										Value:    proto.Float64(140),
250									},
251								},
252							},
253							TimestampMs: proto.Int64(2),
254						},
255						&dto.Metric{
256							Label: []*dto.LabelPair{
257								&dto.LabelPair{
258									Name:  proto.String("n2"),
259									Value: proto.String("val2"),
260								},
261								&dto.LabelPair{
262									Name:  proto.String("n1"),
263									Value: proto.String("val1"),
264								},
265							},
266							Summary: &dto.Summary{
267								SampleCount: proto.Uint64(5),
268								Quantile: []*dto.Quantile{
269									&dto.Quantile{
270										Quantile: proto.Float64(-12.34),
271										Value:    proto.Float64(math.NaN()),
272									},
273								},
274							},
275							TimestampMs: proto.Int64(5),
276						},
277						&dto.Metric{
278							Label: []*dto.LabelPair{
279								&dto.LabelPair{
280									Name:  proto.String("n1"),
281									Value: proto.String("val2"),
282								},
283							},
284							Summary: &dto.Summary{
285								SampleSum: proto.Float64(8),
286							},
287							TimestampMs: proto.Int64(15),
288						},
289						&dto.Metric{
290							Label: []*dto.LabelPair{
291								&dto.LabelPair{
292									Name:  proto.String("n1"),
293									Value: proto.String("val3"),
294								},
295							},
296							Summary: &dto.Summary{
297								Quantile: []*dto.Quantile{
298									&dto.Quantile{
299										Quantile: proto.Float64(0.2),
300										Value:    proto.Float64(4711),
301									},
302								},
303							},
304						},
305					},
306				},
307				&dto.MetricFamily{
308					Name: proto.String("another_summary"),
309					Type: dto.MetricType_SUMMARY.Enum(),
310					Metric: []*dto.Metric{
311						&dto.Metric{
312							Label: []*dto.LabelPair{
313								&dto.LabelPair{
314									Name:  proto.String("n2"),
315									Value: proto.String("val2"),
316								},
317								&dto.LabelPair{
318									Name:  proto.String("n1"),
319									Value: proto.String("val1"),
320								},
321							},
322							Summary: &dto.Summary{
323								SampleCount: proto.Uint64(20),
324								Quantile: []*dto.Quantile{
325									&dto.Quantile{
326										Quantile: proto.Float64(0.3),
327										Value:    proto.Float64(-1.2),
328									},
329								},
330							},
331						},
332					},
333				},
334			},
335		},
336		// 4: The histogram.
337		{
338			in: `
339# HELP request_duration_microseconds The response latency.
340# TYPE request_duration_microseconds histogram
341request_duration_microseconds_bucket{le="100"} 123
342request_duration_microseconds_bucket{le="120"} 412
343request_duration_microseconds_bucket{le="144"} 592
344request_duration_microseconds_bucket{le="172.8"} 1524
345request_duration_microseconds_bucket{le="+Inf"} 2693
346request_duration_microseconds_sum 1.7560473e+06
347request_duration_microseconds_count 2693
348`,
349			out: []*dto.MetricFamily{
350				{
351					Name: proto.String("request_duration_microseconds"),
352					Help: proto.String("The response latency."),
353					Type: dto.MetricType_HISTOGRAM.Enum(),
354					Metric: []*dto.Metric{
355						&dto.Metric{
356							Histogram: &dto.Histogram{
357								SampleCount: proto.Uint64(2693),
358								SampleSum:   proto.Float64(1756047.3),
359								Bucket: []*dto.Bucket{
360									&dto.Bucket{
361										UpperBound:      proto.Float64(100),
362										CumulativeCount: proto.Uint64(123),
363									},
364									&dto.Bucket{
365										UpperBound:      proto.Float64(120),
366										CumulativeCount: proto.Uint64(412),
367									},
368									&dto.Bucket{
369										UpperBound:      proto.Float64(144),
370										CumulativeCount: proto.Uint64(592),
371									},
372									&dto.Bucket{
373										UpperBound:      proto.Float64(172.8),
374										CumulativeCount: proto.Uint64(1524),
375									},
376									&dto.Bucket{
377										UpperBound:      proto.Float64(math.Inf(+1)),
378										CumulativeCount: proto.Uint64(2693),
379									},
380								},
381							},
382						},
383					},
384				},
385			},
386		},
387	}
388
389	for i, scenario := range scenarios {
390		out, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
391		if err != nil {
392			t.Errorf("%d. error: %s", i, err)
393			continue
394		}
395		if expected, got := len(scenario.out), len(out); expected != got {
396			t.Errorf(
397				"%d. expected %d MetricFamilies, got %d",
398				i, expected, got,
399			)
400		}
401		for _, expected := range scenario.out {
402			got, ok := out[expected.GetName()]
403			if !ok {
404				t.Errorf(
405					"%d. expected MetricFamily %q, found none",
406					i, expected.GetName(),
407				)
408				continue
409			}
410			if expected.String() != got.String() {
411				t.Errorf(
412					"%d. expected MetricFamily %s, got %s",
413					i, expected, got,
414				)
415			}
416		}
417	}
418}
419
420func TestTextParse(t *testing.T) {
421	testTextParse(t)
422}
423
424func BenchmarkTextParse(b *testing.B) {
425	for i := 0; i < b.N; i++ {
426		testTextParse(b)
427	}
428}
429
430func testTextParseError(t testing.TB) {
431	var scenarios = []struct {
432		in  string
433		err string
434	}{
435		// 0: No new-line at end of input.
436		{
437			in: `
438bla 3.14
439blubber 42`,
440			err: "text format parsing error in line 3: unexpected end of input stream",
441		},
442		// 1: Invalid escape sequence in label value.
443		{
444			in:  `metric{label="\t"} 3.14`,
445			err: "text format parsing error in line 1: invalid escape sequence",
446		},
447		// 2: Newline in label value.
448		{
449			in: `
450metric{label="new
451line"} 3.14
452`,
453			err: `text format parsing error in line 2: label value "new" contains unescaped new-line`,
454		},
455		// 3:
456		{
457			in:  `metric{@="bla"} 3.14`,
458			err: "text format parsing error in line 1: invalid label name for metric",
459		},
460		// 4:
461		{
462			in:  `metric{__name__="bla"} 3.14`,
463			err: `text format parsing error in line 1: label name "__name__" is reserved`,
464		},
465		// 5:
466		{
467			in:  `metric{label+="bla"} 3.14`,
468			err: "text format parsing error in line 1: expected '=' after label name",
469		},
470		// 6:
471		{
472			in:  `metric{label=bla} 3.14`,
473			err: "text format parsing error in line 1: expected '\"' at start of label value",
474		},
475		// 7:
476		{
477			in: `
478# TYPE metric summary
479metric{quantile="bla"} 3.14
480`,
481			err: "text format parsing error in line 3: expected float as value for 'quantile' label",
482		},
483		// 8:
484		{
485			in:  `metric{label="bla"+} 3.14`,
486			err: "text format parsing error in line 1: unexpected end of label value",
487		},
488		// 9:
489		{
490			in: `metric{label="bla"} 3.14 2.72
491`,
492			err: "text format parsing error in line 1: expected integer as timestamp",
493		},
494		// 10:
495		{
496			in: `metric{label="bla"} 3.14 2 3
497`,
498			err: "text format parsing error in line 1: spurious string after timestamp",
499		},
500		// 11:
501		{
502			in: `metric{label="bla"} blubb
503`,
504			err: "text format parsing error in line 1: expected float as value",
505		},
506		// 12:
507		{
508			in: `
509# HELP metric one
510# HELP metric two
511`,
512			err: "text format parsing error in line 3: second HELP line for metric name",
513		},
514		// 13:
515		{
516			in: `
517# TYPE metric counter
518# TYPE metric untyped
519`,
520			err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
521		},
522		// 14:
523		{
524			in: `
525metric 4.12
526# TYPE metric counter
527`,
528			err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
529		},
530		// 14:
531		{
532			in: `
533# TYPE metric bla
534`,
535			err: "text format parsing error in line 2: unknown metric type",
536		},
537		// 15:
538		{
539			in: `
540# TYPE met-ric
541`,
542			err: "text format parsing error in line 2: invalid metric name in comment",
543		},
544		// 16:
545		{
546			in:  `@invalidmetric{label="bla"} 3.14 2`,
547			err: "text format parsing error in line 1: invalid metric name",
548		},
549		// 17:
550		{
551			in:  `{label="bla"} 3.14 2`,
552			err: "text format parsing error in line 1: invalid metric name",
553		},
554		// 18:
555		{
556			in: `
557# TYPE metric histogram
558metric_bucket{le="bla"} 3.14
559`,
560			err: "text format parsing error in line 3: expected float as value for 'le' label",
561		},
562		// 19: Invalid UTF-8 in label value.
563		{
564			in:  "metric{l=\"\xbd\"} 3.14\n",
565			err: "text format parsing error in line 1: invalid label value \"\\xbd\"",
566		},
567	}
568
569	for i, scenario := range scenarios {
570		_, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
571		if err == nil {
572			t.Errorf("%d. expected error, got nil", i)
573			continue
574		}
575		if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
576			t.Errorf(
577				"%d. expected error starting with %q, got %q",
578				i, expected, got,
579			)
580		}
581	}
582
583}
584
585func TestTextParseError(t *testing.T) {
586	testTextParseError(t)
587}
588
589func BenchmarkParseError(b *testing.B) {
590	for i := 0; i < b.N; i++ {
591		testTextParseError(b)
592	}
593}
594