1
2
3
4
5
6
7
8
9
10
11
12
13
14 package expfmt
15
16 import (
17 "errors"
18 "math"
19 "strings"
20 "testing"
21
22 dto "github.com/prometheus/client_model/go"
23 "google.golang.org/protobuf/proto"
24 )
25
26 func testTextParse(t testing.TB) {
27 scenarios := []struct {
28 in string
29 out []*dto.MetricFamily
30 }{
31
32 {
33 in: `
34
35 `,
36 out: []*dto.MetricFamily{},
37 },
38
39 {
40 in: `
41 minimal_metric 1.234
42 another_metric -3e3 103948
43 # Even that:
44 no_labels{} 3
45 # HELP line for non-existing metric will be ignored.
46 `,
47 out: []*dto.MetricFamily{
48 {
49 Name: proto.String("minimal_metric"),
50 Type: dto.MetricType_UNTYPED.Enum(),
51 Metric: []*dto.Metric{
52 {
53 Untyped: &dto.Untyped{
54 Value: proto.Float64(1.234),
55 },
56 },
57 },
58 },
59 {
60 Name: proto.String("another_metric"),
61 Type: dto.MetricType_UNTYPED.Enum(),
62 Metric: []*dto.Metric{
63 {
64 Untyped: &dto.Untyped{
65 Value: proto.Float64(-3e3),
66 },
67 TimestampMs: proto.Int64(103948),
68 },
69 },
70 },
71 {
72 Name: proto.String("no_labels"),
73 Type: dto.MetricType_UNTYPED.Enum(),
74 Metric: []*dto.Metric{
75 {
76 Untyped: &dto.Untyped{
77 Value: proto.Float64(3),
78 },
79 },
80 },
81 },
82 },
83 },
84
85 {
86 in: `
87 # A normal comment.
88 #
89 # TYPE name counter
90 name{labelname="val1",basename="basevalue"} NaN
91 name {labelname="val2",basename="base\"v\\al\nue"} 0.23 1234567890
92 # HELP name two-line\n doc str\\ing
93
94 # HELP name2 doc str"ing 2
95 # TYPE name2 gauge
96 name2{labelname="val2" ,basename = "basevalue2" } +Inf 54321
97 name2{ labelname = "val1" , }-Inf
98 `,
99 out: []*dto.MetricFamily{
100 {
101 Name: proto.String("name"),
102 Help: proto.String("two-line\n doc str\\ing"),
103 Type: dto.MetricType_COUNTER.Enum(),
104 Metric: []*dto.Metric{
105 {
106 Label: []*dto.LabelPair{
107 {
108 Name: proto.String("labelname"),
109 Value: proto.String("val1"),
110 },
111 {
112 Name: proto.String("basename"),
113 Value: proto.String("basevalue"),
114 },
115 },
116 Counter: &dto.Counter{
117 Value: proto.Float64(math.NaN()),
118 },
119 },
120 {
121 Label: []*dto.LabelPair{
122 {
123 Name: proto.String("labelname"),
124 Value: proto.String("val2"),
125 },
126 {
127 Name: proto.String("basename"),
128 Value: proto.String("base\"v\\al\nue"),
129 },
130 },
131 Counter: &dto.Counter{
132 Value: proto.Float64(.23),
133 },
134 TimestampMs: proto.Int64(1234567890),
135 },
136 },
137 },
138 {
139 Name: proto.String("name2"),
140 Help: proto.String("doc str\"ing 2"),
141 Type: dto.MetricType_GAUGE.Enum(),
142 Metric: []*dto.Metric{
143 {
144 Label: []*dto.LabelPair{
145 {
146 Name: proto.String("labelname"),
147 Value: proto.String("val2"),
148 },
149 {
150 Name: proto.String("basename"),
151 Value: proto.String("basevalue2"),
152 },
153 },
154 Gauge: &dto.Gauge{
155 Value: proto.Float64(math.Inf(+1)),
156 },
157 TimestampMs: proto.Int64(54321),
158 },
159 {
160 Label: []*dto.LabelPair{
161 {
162 Name: proto.String("labelname"),
163 Value: proto.String("val1"),
164 },
165 },
166 Gauge: &dto.Gauge{
167 Value: proto.Float64(math.Inf(-1)),
168 },
169 },
170 },
171 },
172 },
173 },
174
175 {
176 in: `
177 # TYPE my_summary summary
178 my_summary{n1="val1",quantile="0.5"} 110
179 decoy -1 -2
180 my_summary{n1="val1",quantile="0.9"} 140 1
181 my_summary_count{n1="val1"} 42
182 # Latest timestamp wins in case of a summary.
183 my_summary_sum{n1="val1"} 4711 2
184 fake_sum{n1="val1"} 2001
185 # TYPE another_summary summary
186 another_summary_count{n2="val2",n1="val1"} 20
187 my_summary_count{n2="val2",n1="val1"} 5 5
188 another_summary{n1="val1",n2="val2",quantile=".3"} -1.2
189 my_summary_sum{n1="val2"} 08 15
190 my_summary{n1="val3", quantile="0.2"} 4711
191 my_summary{n1="val1",n2="val2",quantile="-12.34",} NaN
192 # some
193 # funny comments
194 # HELP
195 # HELP
196 # HELP my_summary
197 # HELP my_summary
198 `,
199 out: []*dto.MetricFamily{
200 {
201 Name: proto.String("fake_sum"),
202 Type: dto.MetricType_UNTYPED.Enum(),
203 Metric: []*dto.Metric{
204 {
205 Label: []*dto.LabelPair{
206 {
207 Name: proto.String("n1"),
208 Value: proto.String("val1"),
209 },
210 },
211 Untyped: &dto.Untyped{
212 Value: proto.Float64(2001),
213 },
214 },
215 },
216 },
217 {
218 Name: proto.String("decoy"),
219 Type: dto.MetricType_UNTYPED.Enum(),
220 Metric: []*dto.Metric{
221 {
222 Untyped: &dto.Untyped{
223 Value: proto.Float64(-1),
224 },
225 TimestampMs: proto.Int64(-2),
226 },
227 },
228 },
229 {
230 Name: proto.String("my_summary"),
231 Type: dto.MetricType_SUMMARY.Enum(),
232 Metric: []*dto.Metric{
233 {
234 Label: []*dto.LabelPair{
235 {
236 Name: proto.String("n1"),
237 Value: proto.String("val1"),
238 },
239 },
240 Summary: &dto.Summary{
241 SampleCount: proto.Uint64(42),
242 SampleSum: proto.Float64(4711),
243 Quantile: []*dto.Quantile{
244 {
245 Quantile: proto.Float64(0.5),
246 Value: proto.Float64(110),
247 },
248 {
249 Quantile: proto.Float64(0.9),
250 Value: proto.Float64(140),
251 },
252 },
253 },
254 TimestampMs: proto.Int64(2),
255 },
256 {
257 Label: []*dto.LabelPair{
258 {
259 Name: proto.String("n2"),
260 Value: proto.String("val2"),
261 },
262 {
263 Name: proto.String("n1"),
264 Value: proto.String("val1"),
265 },
266 },
267 Summary: &dto.Summary{
268 SampleCount: proto.Uint64(5),
269 Quantile: []*dto.Quantile{
270 {
271 Quantile: proto.Float64(-12.34),
272 Value: proto.Float64(math.NaN()),
273 },
274 },
275 },
276 TimestampMs: proto.Int64(5),
277 },
278 {
279 Label: []*dto.LabelPair{
280 {
281 Name: proto.String("n1"),
282 Value: proto.String("val2"),
283 },
284 },
285 Summary: &dto.Summary{
286 SampleSum: proto.Float64(8),
287 },
288 TimestampMs: proto.Int64(15),
289 },
290 {
291 Label: []*dto.LabelPair{
292 {
293 Name: proto.String("n1"),
294 Value: proto.String("val3"),
295 },
296 },
297 Summary: &dto.Summary{
298 Quantile: []*dto.Quantile{
299 {
300 Quantile: proto.Float64(0.2),
301 Value: proto.Float64(4711),
302 },
303 },
304 },
305 },
306 },
307 },
308 {
309 Name: proto.String("another_summary"),
310 Type: dto.MetricType_SUMMARY.Enum(),
311 Metric: []*dto.Metric{
312 {
313 Label: []*dto.LabelPair{
314 {
315 Name: proto.String("n2"),
316 Value: proto.String("val2"),
317 },
318 {
319 Name: proto.String("n1"),
320 Value: proto.String("val1"),
321 },
322 },
323 Summary: &dto.Summary{
324 SampleCount: proto.Uint64(20),
325 Quantile: []*dto.Quantile{
326 {
327 Quantile: proto.Float64(0.3),
328 Value: proto.Float64(-1.2),
329 },
330 },
331 },
332 },
333 },
334 },
335 },
336 },
337
338 {
339 in: `
340 # HELP request_duration_microseconds The response latency.
341 # TYPE request_duration_microseconds histogram
342 request_duration_microseconds_bucket{le="100"} 123
343 request_duration_microseconds_bucket{le="120"} 412
344 request_duration_microseconds_bucket{le="144"} 592
345 request_duration_microseconds_bucket{le="172.8"} 1524
346 request_duration_microseconds_bucket{le="+Inf"} 2693
347 request_duration_microseconds_sum 1.7560473e+06
348 request_duration_microseconds_count 2693
349 `,
350 out: []*dto.MetricFamily{
351 {
352 Name: proto.String("request_duration_microseconds"),
353 Help: proto.String("The response latency."),
354 Type: dto.MetricType_HISTOGRAM.Enum(),
355 Metric: []*dto.Metric{
356 {
357 Histogram: &dto.Histogram{
358 SampleCount: proto.Uint64(2693),
359 SampleSum: proto.Float64(1756047.3),
360 Bucket: []*dto.Bucket{
361 {
362 UpperBound: proto.Float64(100),
363 CumulativeCount: proto.Uint64(123),
364 },
365 {
366 UpperBound: proto.Float64(120),
367 CumulativeCount: proto.Uint64(412),
368 },
369 {
370 UpperBound: proto.Float64(144),
371 CumulativeCount: proto.Uint64(592),
372 },
373 {
374 UpperBound: proto.Float64(172.8),
375 CumulativeCount: proto.Uint64(1524),
376 },
377 {
378 UpperBound: proto.Float64(math.Inf(+1)),
379 CumulativeCount: proto.Uint64(2693),
380 },
381 },
382 },
383 },
384 },
385 },
386 },
387 },
388 }
389
390 for i, scenario := range scenarios {
391 out, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
392 if err != nil {
393 t.Errorf("%d. error: %s", i, err)
394 continue
395 }
396 if expected, got := len(scenario.out), len(out); expected != got {
397 t.Errorf(
398 "%d. expected %d MetricFamilies, got %d",
399 i, expected, got,
400 )
401 }
402 for _, expected := range scenario.out {
403 got, ok := out[expected.GetName()]
404 if !ok {
405 t.Errorf(
406 "%d. expected MetricFamily %q, found none",
407 i, expected.GetName(),
408 )
409 continue
410 }
411 if expected.String() != got.String() {
412 t.Errorf(
413 "%d. expected MetricFamily %s, got %s",
414 i, expected, got,
415 )
416 }
417 }
418 }
419 }
420
421 func TestTextParse(t *testing.T) {
422 testTextParse(t)
423 }
424
425 func BenchmarkTextParse(b *testing.B) {
426 for i := 0; i < b.N; i++ {
427 testTextParse(b)
428 }
429 }
430
431 func testTextParseError(t testing.TB) {
432 scenarios := []struct {
433 in string
434 err string
435 }{
436
437 {
438 in: `
439 bla 3.14
440 blubber 42`,
441 err: "text format parsing error in line 3: unexpected end of input stream",
442 },
443
444 {
445 in: `metric{label="\t"} 3.14`,
446 err: "text format parsing error in line 1: invalid escape sequence",
447 },
448
449 {
450 in: `
451 metric{label="new
452 line"} 3.14
453 `,
454 err: `text format parsing error in line 2: label value "new" contains unescaped new-line`,
455 },
456
457 {
458 in: `metric{@="bla"} 3.14`,
459 err: "text format parsing error in line 1: invalid label name for metric",
460 },
461
462 {
463 in: `metric{__name__="bla"} 3.14`,
464 err: `text format parsing error in line 1: label name "__name__" is reserved`,
465 },
466
467 {
468 in: `metric{label+="bla"} 3.14`,
469 err: "text format parsing error in line 1: expected '=' after label name",
470 },
471
472 {
473 in: `metric{label=bla} 3.14`,
474 err: "text format parsing error in line 1: expected '\"' at start of label value",
475 },
476
477 {
478 in: `
479 # TYPE metric summary
480 metric{quantile="bla"} 3.14
481 `,
482 err: "text format parsing error in line 3: expected float as value for 'quantile' label",
483 },
484
485 {
486 in: `metric{label="bla"+} 3.14`,
487 err: "text format parsing error in line 1: unexpected end of label value",
488 },
489
490 {
491 in: `metric{label="bla"} 3.14 2.72
492 `,
493 err: "text format parsing error in line 1: expected integer as timestamp",
494 },
495
496 {
497 in: `metric{label="bla"} 3.14 2 3
498 `,
499 err: "text format parsing error in line 1: spurious string after timestamp",
500 },
501
502 {
503 in: `metric{label="bla"} blubb
504 `,
505 err: "text format parsing error in line 1: expected float as value",
506 },
507
508 {
509 in: `
510 # HELP metric one
511 # HELP metric two
512 `,
513 err: "text format parsing error in line 3: second HELP line for metric name",
514 },
515
516 {
517 in: `
518 # TYPE metric counter
519 # TYPE metric untyped
520 `,
521 err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
522 },
523
524 {
525 in: `
526 metric 4.12
527 # TYPE metric counter
528 `,
529 err: `text format parsing error in line 3: second TYPE line for metric name "metric", or TYPE reported after samples`,
530 },
531
532 {
533 in: `
534 # TYPE metric bla
535 `,
536 err: "text format parsing error in line 2: unknown metric type",
537 },
538
539 {
540 in: `
541 # TYPE met-ric
542 `,
543 err: "text format parsing error in line 2: invalid metric name in comment",
544 },
545
546 {
547 in: `@invalidmetric{label="bla"} 3.14 2`,
548 err: "text format parsing error in line 1: invalid metric name",
549 },
550
551 {
552 in: `{label="bla"} 3.14 2`,
553 err: "text format parsing error in line 1: invalid metric name",
554 },
555
556 {
557 in: `
558 # TYPE metric histogram
559 metric_bucket{le="bla"} 3.14
560 `,
561 err: "text format parsing error in line 3: expected float as value for 'le' label",
562 },
563
564 {
565 in: "metric{l=\"\xbd\"} 3.14\n",
566 err: "text format parsing error in line 1: invalid label value \"\\xbd\"",
567 },
568
569 {
570 in: "foo 1_2\n",
571 err: "text format parsing error in line 1: expected float as value",
572 },
573
574 {
575 in: "foo 0x1p-3\n",
576 err: "text format parsing error in line 1: expected float as value",
577 },
578
579 {
580 in: "foo 0x1P-3\n",
581 err: "text format parsing error in line 1: expected float as value",
582 },
583
584 {
585 in: "foo 0B1\n",
586 err: "text format parsing error in line 1: expected float as value",
587 },
588
589 {
590 in: "foo 0O1\n",
591 err: "text format parsing error in line 1: expected float as value",
592 },
593
594 {
595 in: "foo 0X1\n",
596 err: "text format parsing error in line 1: expected float as value",
597 },
598
599 {
600 in: "foo 0x1\n",
601 err: "text format parsing error in line 1: expected float as value",
602 },
603
604 {
605 in: "foo 0b1\n",
606 err: "text format parsing error in line 1: expected float as value",
607 },
608
609 {
610 in: "foo 0o1\n",
611 err: "text format parsing error in line 1: expected float as value",
612 },
613
614 {
615 in: "foo 0x1\n",
616 err: "text format parsing error in line 1: expected float as value",
617 },
618
619 {
620 in: "foo 0x1\n",
621 err: "text format parsing error in line 1: expected float as value",
622 },
623
624 {
625 in: `
626 # TYPE metric histogram
627 metric_bucket{le="0x1p-3"} 3.14
628 `,
629 err: "text format parsing error in line 3: expected float as value for 'le' label",
630 },
631
632 {
633 in: `
634 # TYPE metric summary
635 metric{quantile="0x1p-3"} 3.14
636 `,
637 err: "text format parsing error in line 3: expected float as value for 'quantile' label",
638 },
639
640 {
641 in: `metric{label="bla",label="bla"} 3.14`,
642 err: "text format parsing error in line 1: duplicate label names for metric",
643 },
644 }
645
646 for i, scenario := range scenarios {
647 _, err := parser.TextToMetricFamilies(strings.NewReader(scenario.in))
648 if err == nil {
649 t.Errorf("%d. expected error, got nil", i)
650 continue
651 }
652 if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 {
653 t.Errorf(
654 "%d. expected error starting with %q, got %q",
655 i, expected, got,
656 )
657 }
658 }
659 }
660
661 func TestTextParseError(t *testing.T) {
662 testTextParseError(t)
663 }
664
665 func BenchmarkParseError(b *testing.B) {
666 for i := 0; i < b.N; i++ {
667 testTextParseError(b)
668 }
669 }
670
671 func TestTextParserStartOfLine(t *testing.T) {
672 t.Run("EOF", func(t *testing.T) {
673 p := TextParser{}
674 in := strings.NewReader("")
675 p.reset(in)
676 fn := p.startOfLine()
677 if fn != nil {
678 t.Errorf("Unexpected non-nil function: %v", fn)
679 }
680 if p.err != nil {
681 t.Errorf("Unexpected error: %v", p.err)
682 }
683 })
684
685 t.Run("OtherError", func(t *testing.T) {
686 p := TextParser{}
687 in := &errReader{err: errors.New("unexpected error")}
688 p.reset(in)
689 fn := p.startOfLine()
690 if fn != nil {
691 t.Errorf("Unexpected non-nil function: %v", fn)
692 }
693 if p.err != nil && !errors.Is(p.err, in.err) {
694 t.Errorf("Unexpected error: %v, expected %v", p.err, in.err)
695 }
696 })
697 }
698
699 type errReader struct {
700 err error
701 }
702
703 func (r *errReader) Read(p []byte) (int, error) {
704 return 0, r.err
705 }
706
View as plain text