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 "bytes" 18 "math" 19 "strings" 20 "testing" 21 22 "github.com/golang/protobuf/proto" //nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility. 23 24 dto "github.com/prometheus/client_model/go" 25) 26 27func TestCreate(t *testing.T) { 28 var scenarios = []struct { 29 in *dto.MetricFamily 30 out string 31 }{ 32 // 0: Counter, NaN as value, timestamp given. 33 { 34 in: &dto.MetricFamily{ 35 Name: proto.String("name"), 36 Help: proto.String("two-line\n doc str\\ing"), 37 Type: dto.MetricType_COUNTER.Enum(), 38 Metric: []*dto.Metric{ 39 &dto.Metric{ 40 Label: []*dto.LabelPair{ 41 &dto.LabelPair{ 42 Name: proto.String("labelname"), 43 Value: proto.String("val1"), 44 }, 45 &dto.LabelPair{ 46 Name: proto.String("basename"), 47 Value: proto.String("basevalue"), 48 }, 49 }, 50 Counter: &dto.Counter{ 51 Value: proto.Float64(math.NaN()), 52 }, 53 }, 54 &dto.Metric{ 55 Label: []*dto.LabelPair{ 56 &dto.LabelPair{ 57 Name: proto.String("labelname"), 58 Value: proto.String("val2"), 59 }, 60 &dto.LabelPair{ 61 Name: proto.String("basename"), 62 Value: proto.String("basevalue"), 63 }, 64 }, 65 Counter: &dto.Counter{ 66 Value: proto.Float64(.23), 67 }, 68 TimestampMs: proto.Int64(1234567890), 69 }, 70 }, 71 }, 72 out: `# HELP name two-line\n doc str\\ing 73# TYPE name counter 74name{labelname="val1",basename="basevalue"} NaN 75name{labelname="val2",basename="basevalue"} 0.23 1234567890 76`, 77 }, 78 // 1: Gauge, some escaping required, +Inf as value, multi-byte characters in label values. 79 { 80 in: &dto.MetricFamily{ 81 Name: proto.String("gauge_name"), 82 Help: proto.String("gauge\ndoc\nstr\"ing"), 83 Type: dto.MetricType_GAUGE.Enum(), 84 Metric: []*dto.Metric{ 85 &dto.Metric{ 86 Label: []*dto.LabelPair{ 87 &dto.LabelPair{ 88 Name: proto.String("name_1"), 89 Value: proto.String("val with\nnew line"), 90 }, 91 &dto.LabelPair{ 92 Name: proto.String("name_2"), 93 Value: proto.String("val with \\backslash and \"quotes\""), 94 }, 95 }, 96 Gauge: &dto.Gauge{ 97 Value: proto.Float64(math.Inf(+1)), 98 }, 99 }, 100 &dto.Metric{ 101 Label: []*dto.LabelPair{ 102 &dto.LabelPair{ 103 Name: proto.String("name_1"), 104 Value: proto.String("Björn"), 105 }, 106 &dto.LabelPair{ 107 Name: proto.String("name_2"), 108 Value: proto.String("佖佥"), 109 }, 110 }, 111 Gauge: &dto.Gauge{ 112 Value: proto.Float64(3.14e42), 113 }, 114 }, 115 }, 116 }, 117 out: `# HELP gauge_name gauge\ndoc\nstr"ing 118# TYPE gauge_name gauge 119gauge_name{name_1="val with\nnew line",name_2="val with \\backslash and \"quotes\""} +Inf 120gauge_name{name_1="Björn",name_2="佖佥"} 3.14e+42 121`, 122 }, 123 // 2: Untyped, no help, one sample with no labels and -Inf as value, another sample with one label. 124 { 125 in: &dto.MetricFamily{ 126 Name: proto.String("untyped_name"), 127 Type: dto.MetricType_UNTYPED.Enum(), 128 Metric: []*dto.Metric{ 129 &dto.Metric{ 130 Untyped: &dto.Untyped{ 131 Value: proto.Float64(math.Inf(-1)), 132 }, 133 }, 134 &dto.Metric{ 135 Label: []*dto.LabelPair{ 136 &dto.LabelPair{ 137 Name: proto.String("name_1"), 138 Value: proto.String("value 1"), 139 }, 140 }, 141 Untyped: &dto.Untyped{ 142 Value: proto.Float64(-1.23e-45), 143 }, 144 }, 145 }, 146 }, 147 out: `# TYPE untyped_name untyped 148untyped_name -Inf 149untyped_name{name_1="value 1"} -1.23e-45 150`, 151 }, 152 // 3: Summary. 153 { 154 in: &dto.MetricFamily{ 155 Name: proto.String("summary_name"), 156 Help: proto.String("summary docstring"), 157 Type: dto.MetricType_SUMMARY.Enum(), 158 Metric: []*dto.Metric{ 159 &dto.Metric{ 160 Summary: &dto.Summary{ 161 SampleCount: proto.Uint64(42), 162 SampleSum: proto.Float64(-3.4567), 163 Quantile: []*dto.Quantile{ 164 &dto.Quantile{ 165 Quantile: proto.Float64(0.5), 166 Value: proto.Float64(-1.23), 167 }, 168 &dto.Quantile{ 169 Quantile: proto.Float64(0.9), 170 Value: proto.Float64(.2342354), 171 }, 172 &dto.Quantile{ 173 Quantile: proto.Float64(0.99), 174 Value: proto.Float64(0), 175 }, 176 }, 177 }, 178 }, 179 &dto.Metric{ 180 Label: []*dto.LabelPair{ 181 &dto.LabelPair{ 182 Name: proto.String("name_1"), 183 Value: proto.String("value 1"), 184 }, 185 &dto.LabelPair{ 186 Name: proto.String("name_2"), 187 Value: proto.String("value 2"), 188 }, 189 }, 190 Summary: &dto.Summary{ 191 SampleCount: proto.Uint64(4711), 192 SampleSum: proto.Float64(2010.1971), 193 Quantile: []*dto.Quantile{ 194 &dto.Quantile{ 195 Quantile: proto.Float64(0.5), 196 Value: proto.Float64(1), 197 }, 198 &dto.Quantile{ 199 Quantile: proto.Float64(0.9), 200 Value: proto.Float64(2), 201 }, 202 &dto.Quantile{ 203 Quantile: proto.Float64(0.99), 204 Value: proto.Float64(3), 205 }, 206 }, 207 }, 208 }, 209 }, 210 }, 211 out: `# HELP summary_name summary docstring 212# TYPE summary_name summary 213summary_name{quantile="0.5"} -1.23 214summary_name{quantile="0.9"} 0.2342354 215summary_name{quantile="0.99"} 0 216summary_name_sum -3.4567 217summary_name_count 42 218summary_name{name_1="value 1",name_2="value 2",quantile="0.5"} 1 219summary_name{name_1="value 1",name_2="value 2",quantile="0.9"} 2 220summary_name{name_1="value 1",name_2="value 2",quantile="0.99"} 3 221summary_name_sum{name_1="value 1",name_2="value 2"} 2010.1971 222summary_name_count{name_1="value 1",name_2="value 2"} 4711 223`, 224 }, 225 // 4: Histogram 226 { 227 in: &dto.MetricFamily{ 228 Name: proto.String("request_duration_microseconds"), 229 Help: proto.String("The response latency."), 230 Type: dto.MetricType_HISTOGRAM.Enum(), 231 Metric: []*dto.Metric{ 232 &dto.Metric{ 233 Histogram: &dto.Histogram{ 234 SampleCount: proto.Uint64(2693), 235 SampleSum: proto.Float64(1756047.3), 236 Bucket: []*dto.Bucket{ 237 &dto.Bucket{ 238 UpperBound: proto.Float64(100), 239 CumulativeCount: proto.Uint64(123), 240 }, 241 &dto.Bucket{ 242 UpperBound: proto.Float64(120), 243 CumulativeCount: proto.Uint64(412), 244 }, 245 &dto.Bucket{ 246 UpperBound: proto.Float64(144), 247 CumulativeCount: proto.Uint64(592), 248 }, 249 &dto.Bucket{ 250 UpperBound: proto.Float64(172.8), 251 CumulativeCount: proto.Uint64(1524), 252 }, 253 &dto.Bucket{ 254 UpperBound: proto.Float64(math.Inf(+1)), 255 CumulativeCount: proto.Uint64(2693), 256 }, 257 }, 258 }, 259 }, 260 }, 261 }, 262 out: `# HELP request_duration_microseconds The response latency. 263# TYPE request_duration_microseconds histogram 264request_duration_microseconds_bucket{le="100"} 123 265request_duration_microseconds_bucket{le="120"} 412 266request_duration_microseconds_bucket{le="144"} 592 267request_duration_microseconds_bucket{le="172.8"} 1524 268request_duration_microseconds_bucket{le="+Inf"} 2693 269request_duration_microseconds_sum 1.7560473e+06 270request_duration_microseconds_count 2693 271`, 272 }, 273 // 5: Histogram with missing +Inf bucket. 274 { 275 in: &dto.MetricFamily{ 276 Name: proto.String("request_duration_microseconds"), 277 Help: proto.String("The response latency."), 278 Type: dto.MetricType_HISTOGRAM.Enum(), 279 Metric: []*dto.Metric{ 280 &dto.Metric{ 281 Histogram: &dto.Histogram{ 282 SampleCount: proto.Uint64(2693), 283 SampleSum: proto.Float64(1756047.3), 284 Bucket: []*dto.Bucket{ 285 &dto.Bucket{ 286 UpperBound: proto.Float64(100), 287 CumulativeCount: proto.Uint64(123), 288 }, 289 &dto.Bucket{ 290 UpperBound: proto.Float64(120), 291 CumulativeCount: proto.Uint64(412), 292 }, 293 &dto.Bucket{ 294 UpperBound: proto.Float64(144), 295 CumulativeCount: proto.Uint64(592), 296 }, 297 &dto.Bucket{ 298 UpperBound: proto.Float64(172.8), 299 CumulativeCount: proto.Uint64(1524), 300 }, 301 }, 302 }, 303 }, 304 }, 305 }, 306 out: `# HELP request_duration_microseconds The response latency. 307# TYPE request_duration_microseconds histogram 308request_duration_microseconds_bucket{le="100"} 123 309request_duration_microseconds_bucket{le="120"} 412 310request_duration_microseconds_bucket{le="144"} 592 311request_duration_microseconds_bucket{le="172.8"} 1524 312request_duration_microseconds_bucket{le="+Inf"} 2693 313request_duration_microseconds_sum 1.7560473e+06 314request_duration_microseconds_count 2693 315`, 316 }, 317 // 6: No metric type, should result in default type Counter. 318 { 319 in: &dto.MetricFamily{ 320 Name: proto.String("name"), 321 Help: proto.String("doc string"), 322 Metric: []*dto.Metric{ 323 &dto.Metric{ 324 Counter: &dto.Counter{ 325 Value: proto.Float64(math.Inf(-1)), 326 }, 327 }, 328 }, 329 }, 330 out: `# HELP name doc string 331# TYPE name counter 332name -Inf 333`, 334 }, 335 } 336 337 for i, scenario := range scenarios { 338 out := bytes.NewBuffer(make([]byte, 0, len(scenario.out))) 339 n, err := MetricFamilyToText(out, scenario.in) 340 if err != nil { 341 t.Errorf("%d. error: %s", i, err) 342 continue 343 } 344 if expected, got := len(scenario.out), n; expected != got { 345 t.Errorf( 346 "%d. expected %d bytes written, got %d", 347 i, expected, got, 348 ) 349 } 350 if expected, got := scenario.out, out.String(); expected != got { 351 t.Errorf( 352 "%d. expected out=%q, got %q", 353 i, expected, got, 354 ) 355 } 356 } 357 358} 359 360func BenchmarkCreate(b *testing.B) { 361 mf := &dto.MetricFamily{ 362 Name: proto.String("request_duration_microseconds"), 363 Help: proto.String("The response latency."), 364 Type: dto.MetricType_HISTOGRAM.Enum(), 365 Metric: []*dto.Metric{ 366 &dto.Metric{ 367 Label: []*dto.LabelPair{ 368 &dto.LabelPair{ 369 Name: proto.String("name_1"), 370 Value: proto.String("val with\nnew line"), 371 }, 372 &dto.LabelPair{ 373 Name: proto.String("name_2"), 374 Value: proto.String("val with \\backslash and \"quotes\""), 375 }, 376 &dto.LabelPair{ 377 Name: proto.String("name_3"), 378 Value: proto.String("Just a quite long label value to test performance."), 379 }, 380 }, 381 Histogram: &dto.Histogram{ 382 SampleCount: proto.Uint64(2693), 383 SampleSum: proto.Float64(1756047.3), 384 Bucket: []*dto.Bucket{ 385 &dto.Bucket{ 386 UpperBound: proto.Float64(100), 387 CumulativeCount: proto.Uint64(123), 388 }, 389 &dto.Bucket{ 390 UpperBound: proto.Float64(120), 391 CumulativeCount: proto.Uint64(412), 392 }, 393 &dto.Bucket{ 394 UpperBound: proto.Float64(144), 395 CumulativeCount: proto.Uint64(592), 396 }, 397 &dto.Bucket{ 398 UpperBound: proto.Float64(172.8), 399 CumulativeCount: proto.Uint64(1524), 400 }, 401 &dto.Bucket{ 402 UpperBound: proto.Float64(math.Inf(+1)), 403 CumulativeCount: proto.Uint64(2693), 404 }, 405 }, 406 }, 407 }, 408 &dto.Metric{ 409 Label: []*dto.LabelPair{ 410 &dto.LabelPair{ 411 Name: proto.String("name_1"), 412 Value: proto.String("Björn"), 413 }, 414 &dto.LabelPair{ 415 Name: proto.String("name_2"), 416 Value: proto.String("佖佥"), 417 }, 418 &dto.LabelPair{ 419 Name: proto.String("name_3"), 420 Value: proto.String("Just a quite long label value to test performance."), 421 }, 422 }, 423 Histogram: &dto.Histogram{ 424 SampleCount: proto.Uint64(5699), 425 SampleSum: proto.Float64(49484343543.4343), 426 Bucket: []*dto.Bucket{ 427 &dto.Bucket{ 428 UpperBound: proto.Float64(100), 429 CumulativeCount: proto.Uint64(120), 430 }, 431 &dto.Bucket{ 432 UpperBound: proto.Float64(120), 433 CumulativeCount: proto.Uint64(412), 434 }, 435 &dto.Bucket{ 436 UpperBound: proto.Float64(144), 437 CumulativeCount: proto.Uint64(596), 438 }, 439 &dto.Bucket{ 440 UpperBound: proto.Float64(172.8), 441 CumulativeCount: proto.Uint64(1535), 442 }, 443 }, 444 }, 445 TimestampMs: proto.Int64(1234567890), 446 }, 447 }, 448 } 449 out := bytes.NewBuffer(make([]byte, 0, 1024)) 450 451 for i := 0; i < b.N; i++ { 452 _, err := MetricFamilyToText(out, mf) 453 if err != nil { 454 b.Fatal(err) 455 } 456 out.Reset() 457 } 458} 459 460func BenchmarkCreateBuildInfo(b *testing.B) { 461 mf := &dto.MetricFamily{ 462 Name: proto.String("benchmark_build_info"), 463 Help: proto.String("Test the creation of constant 1-value build_info metric."), 464 Type: dto.MetricType_GAUGE.Enum(), 465 Metric: []*dto.Metric{ 466 &dto.Metric{ 467 Label: []*dto.LabelPair{ 468 &dto.LabelPair{ 469 Name: proto.String("version"), 470 Value: proto.String("1.2.3"), 471 }, 472 &dto.LabelPair{ 473 Name: proto.String("revision"), 474 Value: proto.String("2e84f5e4eacdffb574035810305191ff390360fe"), 475 }, 476 &dto.LabelPair{ 477 Name: proto.String("go_version"), 478 Value: proto.String("1.11.1"), 479 }, 480 }, 481 Gauge: &dto.Gauge{ 482 Value: proto.Float64(1), 483 }, 484 }, 485 }, 486 } 487 out := bytes.NewBuffer(make([]byte, 0, 1024)) 488 489 for i := 0; i < b.N; i++ { 490 _, err := MetricFamilyToText(out, mf) 491 if err != nil { 492 b.Fatal(err) 493 } 494 out.Reset() 495 } 496} 497 498func TestCreateError(t *testing.T) { 499 var scenarios = []struct { 500 in *dto.MetricFamily 501 err string 502 }{ 503 // 0: No metric. 504 { 505 in: &dto.MetricFamily{ 506 Name: proto.String("name"), 507 Help: proto.String("doc string"), 508 Type: dto.MetricType_COUNTER.Enum(), 509 Metric: []*dto.Metric{}, 510 }, 511 err: "MetricFamily has no metrics", 512 }, 513 // 1: No metric name. 514 { 515 in: &dto.MetricFamily{ 516 Help: proto.String("doc string"), 517 Type: dto.MetricType_UNTYPED.Enum(), 518 Metric: []*dto.Metric{ 519 &dto.Metric{ 520 Untyped: &dto.Untyped{ 521 Value: proto.Float64(math.Inf(-1)), 522 }, 523 }, 524 }, 525 }, 526 err: "MetricFamily has no name", 527 }, 528 // 2: Wrong type. 529 { 530 in: &dto.MetricFamily{ 531 Name: proto.String("name"), 532 Help: proto.String("doc string"), 533 Type: dto.MetricType_COUNTER.Enum(), 534 Metric: []*dto.Metric{ 535 &dto.Metric{ 536 Untyped: &dto.Untyped{ 537 Value: proto.Float64(math.Inf(-1)), 538 }, 539 }, 540 }, 541 }, 542 err: "expected counter in metric", 543 }, 544 } 545 546 for i, scenario := range scenarios { 547 var out bytes.Buffer 548 _, err := MetricFamilyToText(&out, scenario.in) 549 if err == nil { 550 t.Errorf("%d. expected error, got nil", i) 551 continue 552 } 553 if expected, got := scenario.err, err.Error(); strings.Index(got, expected) != 0 { 554 t.Errorf( 555 "%d. expected error starting with %q, got %q", 556 i, expected, got, 557 ) 558 } 559 } 560 561} 562