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