github.com/uber-go/tally/v4@v4.1.17/prometheus/reporter_test.go (about) 1 // Copyright (c) 2021 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package prometheus 22 23 import ( 24 "fmt" 25 "testing" 26 "time" 27 28 prom "github.com/prometheus/client_golang/prometheus" 29 dto "github.com/prometheus/client_model/go" 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 tally "github.com/uber-go/tally/v4" 33 ) 34 35 // NB(r): If a test is failing, you can debug what is being 36 // gathered from Prometheus by printing the following: 37 // proto.MarshalTextString(gather(t, registry)[0]) 38 39 func TestCounter(t *testing.T) { 40 registry := prom.NewRegistry() 41 r := NewReporter(Options{Registerer: registry}) 42 name := "test_counter" 43 tags := map[string]string{ 44 "foo": "bar", 45 "test": "everything", 46 } 47 tags2 := map[string]string{ 48 "foo": "baz", 49 "test": "something", 50 } 51 52 count := r.AllocateCounter(name, tags) 53 count.ReportCount(1) 54 count.ReportCount(2) 55 56 count = r.AllocateCounter(name, tags2) 57 count.ReportCount(2) 58 59 assertMetric(t, gather(t, registry), metric{ 60 name: name, 61 mtype: dto.MetricType_COUNTER, 62 instances: []instance{ 63 { 64 labels: tags, 65 counter: counterValue(3), 66 }, 67 { 68 labels: tags2, 69 counter: counterValue(2), 70 }, 71 }, 72 }) 73 } 74 75 func TestGauge(t *testing.T) { 76 registry := prom.NewRegistry() 77 r := NewReporter(Options{Registerer: registry}) 78 name := "test_gauge" 79 tags := map[string]string{ 80 "foo": "bar", 81 "test": "everything", 82 } 83 84 gauge := r.AllocateGauge(name, tags) 85 gauge.ReportGauge(15) 86 gauge.ReportGauge(30) 87 88 assertMetric(t, gather(t, registry), metric{ 89 name: name, 90 mtype: dto.MetricType_GAUGE, 91 instances: []instance{ 92 { 93 labels: tags, 94 gauge: gaugeValue(30), 95 }, 96 }, 97 }) 98 } 99 100 func TestTimerHistogram(t *testing.T) { 101 registry := prom.NewRegistry() 102 r := NewReporter(Options{ 103 Registerer: registry, 104 DefaultTimerType: HistogramTimerType, 105 DefaultHistogramBuckets: []float64{ 106 50 * ms, 107 250 * ms, 108 1000 * ms, 109 2500 * ms, 110 10000 * ms, 111 }, 112 }) 113 114 name := "test_timer" 115 tags := map[string]string{ 116 "foo": "bar", 117 "test": "everything", 118 } 119 tags2 := map[string]string{ 120 "foo": "baz", 121 "test": "something", 122 } 123 vals := []time.Duration{ 124 23 * time.Millisecond, 125 223 * time.Millisecond, 126 320 * time.Millisecond, 127 } 128 vals2 := []time.Duration{ 129 1742 * time.Millisecond, 130 3232 * time.Millisecond, 131 } 132 133 timer := r.AllocateTimer(name, tags) 134 for _, v := range vals { 135 timer.ReportTimer(v) 136 } 137 138 timer = r.AllocateTimer(name, tags2) 139 for _, v := range vals2 { 140 timer.ReportTimer(v) 141 } 142 143 assertMetric(t, gather(t, registry), metric{ 144 name: name, 145 mtype: dto.MetricType_HISTOGRAM, 146 instances: []instance{ 147 { 148 labels: tags, 149 histogram: histogramValue(histogramVal{ 150 sampleCount: uint64(len(vals)), 151 sampleSum: durationFloatSum(vals), 152 buckets: []histogramValBucket{ 153 {upperBound: 0.05, count: 1}, 154 {upperBound: 0.25, count: 2}, 155 {upperBound: 1.00, count: 3}, 156 {upperBound: 2.50, count: 3}, 157 {upperBound: 10.00, count: 3}, 158 }, 159 }), 160 }, 161 { 162 labels: tags2, 163 histogram: histogramValue(histogramVal{ 164 sampleCount: uint64(len(vals2)), 165 sampleSum: durationFloatSum(vals2), 166 buckets: []histogramValBucket{ 167 {upperBound: 0.05, count: 0}, 168 {upperBound: 0.25, count: 0}, 169 {upperBound: 1.00, count: 0}, 170 {upperBound: 2.50, count: 1}, 171 {upperBound: 10.00, count: 2}, 172 }, 173 }), 174 }, 175 }, 176 }) 177 } 178 179 func TestTimerSummary(t *testing.T) { 180 registry := prom.NewRegistry() 181 r := NewReporter(Options{ 182 Registerer: registry, 183 DefaultTimerType: SummaryTimerType, 184 DefaultSummaryObjectives: map[float64]float64{ 185 0.5: 0.01, 186 0.75: 0.001, 187 0.95: 0.001, 188 0.99: 0.001, 189 0.999: 0.0001, 190 }, 191 }) 192 193 name := "test_timer" 194 tags := map[string]string{ 195 "foo": "bar", 196 "test": "everything", 197 } 198 tags2 := map[string]string{ 199 "foo": "baz", 200 "test": "something", 201 } 202 vals := []time.Duration{ 203 23 * time.Millisecond, 204 223 * time.Millisecond, 205 320 * time.Millisecond, 206 } 207 vals2 := []time.Duration{ 208 1742 * time.Millisecond, 209 3232 * time.Millisecond, 210 } 211 212 timer := r.AllocateTimer(name, tags) 213 for _, v := range vals { 214 timer.ReportTimer(v) 215 } 216 217 timer = r.AllocateTimer(name, tags2) 218 for _, v := range vals2 { 219 timer.ReportTimer(v) 220 } 221 222 assertMetric(t, gather(t, registry), metric{ 223 name: name, 224 mtype: dto.MetricType_SUMMARY, 225 instances: []instance{ 226 { 227 labels: tags, 228 summary: summaryValue(summaryVal{ 229 sampleCount: uint64(len(vals)), 230 sampleSum: durationFloatSum(vals), 231 quantiles: []summaryValQuantile{ 232 {quantile: 0.50, value: 0.223}, 233 {quantile: 0.75, value: 0.32}, 234 {quantile: 0.95, value: 0.32}, 235 {quantile: 0.99, value: 0.32}, 236 {quantile: 0.999, value: 0.32}, 237 }, 238 }), 239 }, 240 { 241 labels: tags2, 242 summary: summaryValue(summaryVal{ 243 sampleCount: uint64(len(vals2)), 244 sampleSum: durationFloatSum(vals2), 245 quantiles: []summaryValQuantile{ 246 {quantile: 0.50, value: 1.742}, 247 {quantile: 0.75, value: 3.232}, 248 {quantile: 0.95, value: 3.232}, 249 {quantile: 0.99, value: 3.232}, 250 {quantile: 0.999, value: 3.232}, 251 }, 252 }), 253 }, 254 }, 255 }) 256 } 257 258 func TestHistogramBucketValues(t *testing.T) { 259 registry := prom.NewRegistry() 260 r := NewReporter(Options{ 261 Registerer: registry, 262 }) 263 264 buckets := tally.DurationBuckets{ 265 0 * time.Millisecond, 266 50 * time.Millisecond, 267 250 * time.Millisecond, 268 1000 * time.Millisecond, 269 2500 * time.Millisecond, 270 10000 * time.Millisecond, 271 } 272 273 name := "test_histogram" 274 tags := map[string]string{ 275 "foo": "bar", 276 "test": "everything", 277 } 278 tags2 := map[string]string{ 279 "foo": "baz", 280 "test": "something", 281 } 282 vals := []time.Duration{ 283 23 * time.Millisecond, 284 223 * time.Millisecond, 285 320 * time.Millisecond, 286 } 287 vals2 := []time.Duration{ 288 1742 * time.Millisecond, 289 3232 * time.Millisecond, 290 } 291 292 histogram := r.AllocateHistogram(name, tags, buckets) 293 histogram.DurationBucket(0, 50*time.Millisecond).ReportSamples(1) 294 histogram.DurationBucket(0, 250*time.Millisecond).ReportSamples(1) 295 histogram.DurationBucket(0, 1000*time.Millisecond).ReportSamples(1) 296 297 histogram = r.AllocateHistogram(name, tags2, buckets) 298 histogram.DurationBucket(0, 2500*time.Millisecond).ReportSamples(1) 299 histogram.DurationBucket(0, 10000*time.Millisecond).ReportSamples(1) 300 301 assertMetric(t, gather(t, registry), metric{ 302 name: name, 303 mtype: dto.MetricType_HISTOGRAM, 304 instances: []instance{ 305 { 306 labels: tags, 307 histogram: histogramValue(histogramVal{ 308 sampleCount: uint64(len(vals)), 309 sampleSum: 1.3, 310 buckets: []histogramValBucket{ 311 {upperBound: 0.00, count: 0}, 312 {upperBound: 0.05, count: 1}, 313 {upperBound: 0.25, count: 2}, 314 {upperBound: 1.00, count: 3}, 315 {upperBound: 2.50, count: 3}, 316 {upperBound: 10.00, count: 3}, 317 }, 318 }), 319 }, 320 { 321 labels: tags2, 322 histogram: histogramValue(histogramVal{ 323 sampleCount: uint64(len(vals2)), 324 sampleSum: 12.5, 325 buckets: []histogramValBucket{ 326 {upperBound: 0.00, count: 0}, 327 {upperBound: 0.05, count: 0}, 328 {upperBound: 0.25, count: 0}, 329 {upperBound: 1.00, count: 0}, 330 {upperBound: 2.50, count: 1}, 331 {upperBound: 10.00, count: 2}, 332 }, 333 }), 334 }, 335 }, 336 }) 337 } 338 339 func TestOnRegisterError(t *testing.T) { 340 var captured []error 341 342 registry := prom.NewRegistry() 343 r := NewReporter(Options{ 344 Registerer: registry, 345 OnRegisterError: func(err error) { 346 captured = append(captured, err) 347 }, 348 }) 349 350 c := r.AllocateCounter("bad-name", nil) 351 c.ReportCount(2) 352 c.ReportCount(4) 353 c = r.AllocateCounter("bad.name", nil) 354 c.ReportCount(42) 355 c.ReportCount(84) 356 357 assert.Equal(t, 2, len(captured)) 358 } 359 360 func TestAlreadyRegisteredCounter(t *testing.T) { 361 var captured []error 362 363 registry := prom.NewRegistry() 364 r := NewReporter(Options{ 365 Registerer: registry, 366 OnRegisterError: func(err error) { 367 captured = append(captured, err) 368 }, 369 }) 370 371 // n.b. Prometheus metrics are different from M3 metrics in that they are 372 // uniquely identified as "metric_name+label_name+label_name+..."; 373 // additionally, given that Prometheus ingestion is pull-based, there 374 // is only ever one reporter used regardless of tally.Scope hierarchy. 375 // 376 // Because of this, for a given metric "foo", only the first-registered 377 // permutation of metric and label names will succeed because the same 378 // registry is being used, and because Prometheus asserts that a 379 // registered metric name has the same corresponding label names. 380 // Subsequent registrations - such as adding or removing tags - will 381 // return an error. 382 // 383 // This is a problem because Tally's API does not apply semantics or 384 // restrictions to the combinatorics (or descendant mutations of) 385 // metric tags. As such, we must assert that Tally's Prometheus 386 // reporter does the right thing and indicates an error when this 387 // happens. 388 // 389 // The first allocation call will succeed. This establishes the required 390 // label names (["foo"]) for metric "foo". 391 r.AllocateCounter("foo", map[string]string{"foo": "bar"}) 392 393 // The second allocation call is okay, as it has the same label names (["foo"]). 394 r.AllocateCounter("foo", map[string]string{"foo": "baz"}) 395 396 // The third allocation call fails, because it has different label names 397 // (["bar"], vs previously ["foo"]) for the same metric name "foo". 398 r.AllocateCounter("foo", map[string]string{"bar": "qux"}) 399 400 // The fourth allocation call fails, because while it has one of the same 401 // label names ("foo") as was previously registered for metric "foo", it 402 // also has an additional label name (["foo", "zork"] != ["foo"]). 403 r.AllocateCounter("foo", map[string]string{ 404 "foo": "bar", 405 "zork": "derp", 406 }) 407 408 // The fifth allocation call fails, because it has no label names for the 409 // metric "foo", which expects the label names it was originally registered 410 // with (["foo"]). 411 r.AllocateCounter("foo", nil) 412 413 require.Equal(t, 3, len(captured)) 414 for _, err := range captured { 415 require.Contains(t, err.Error(), "same fully-qualified name") 416 } 417 } 418 419 func gather(t *testing.T, r prom.Gatherer) []*dto.MetricFamily { 420 metrics, err := r.Gather() 421 require.NoError(t, err) 422 return metrics 423 } 424 425 func counterValue(v float64) *dto.Counter { 426 return &dto.Counter{Value: &v} 427 } 428 429 func gaugeValue(v float64) *dto.Gauge { 430 return &dto.Gauge{Value: &v} 431 } 432 433 type histogramVal struct { 434 sampleCount uint64 435 sampleSum float64 436 buckets []histogramValBucket 437 } 438 439 type histogramValBucket struct { 440 count uint64 441 upperBound float64 442 } 443 444 func histogramValue(v histogramVal) *dto.Histogram { 445 r := &dto.Histogram{ 446 SampleCount: &v.sampleCount, 447 SampleSum: &v.sampleSum, 448 } 449 for _, b := range v.buckets { 450 b := b // or else the addresses we take will be static 451 r.Bucket = append(r.Bucket, &dto.Bucket{ 452 CumulativeCount: &b.count, 453 UpperBound: &b.upperBound, 454 }) 455 } 456 return r 457 } 458 459 type summaryVal struct { 460 sampleCount uint64 461 sampleSum float64 462 quantiles []summaryValQuantile 463 } 464 465 type summaryValQuantile struct { 466 quantile float64 467 value float64 468 } 469 470 func summaryValue(v summaryVal) *dto.Summary { 471 r := &dto.Summary{ 472 SampleCount: &v.sampleCount, 473 SampleSum: &v.sampleSum, 474 } 475 for _, q := range v.quantiles { 476 q := q // or else the addresses we take will be static 477 r.Quantile = append(r.Quantile, &dto.Quantile{ 478 Quantile: &q.quantile, 479 Value: &q.value, 480 }) 481 } 482 return r 483 } 484 485 func durationFloatSum(v []time.Duration) float64 { 486 var sum float64 487 for _, d := range v { 488 sum += durationFloat(d) 489 } 490 return sum 491 } 492 493 func durationFloat(d time.Duration) float64 { 494 return float64(d) / float64(time.Second) 495 } 496 497 type metric struct { 498 name string 499 mtype dto.MetricType 500 instances []instance 501 } 502 503 type instance struct { 504 labels map[string]string 505 counter *dto.Counter 506 gauge *dto.Gauge 507 histogram *dto.Histogram 508 summary *dto.Summary 509 } 510 511 func assertMetric( 512 t *testing.T, 513 metrics []*dto.MetricFamily, 514 query metric, 515 ) { 516 q := query 517 msgFmt := func(msg string, v ...interface{}) string { 518 prefix := fmt.Sprintf("assert fail for metric name=%s, type=%s: ", 519 q.name, q.mtype.String()) 520 return fmt.Sprintf(prefix+msg, v...) 521 } 522 for _, m := range metrics { 523 if m.GetName() != q.name || m.GetType() != q.mtype { 524 continue 525 } 526 if len(q.instances) == 0 { 527 require.Fail(t, msgFmt("no instances to assert")) 528 } 529 for _, i := range q.instances { 530 found := false 531 for _, j := range m.GetMetric() { 532 if len(i.labels) != len(j.GetLabel()) { 533 continue 534 } 535 536 notMatched := make(map[string]string, len(i.labels)) 537 for k, v := range i.labels { 538 notMatched[k] = v 539 } 540 541 for _, pair := range j.GetLabel() { 542 notMatchedValue, matches := notMatched[pair.GetName()] 543 if matches && pair.GetValue() == notMatchedValue { 544 delete(notMatched, pair.GetName()) 545 } 546 } 547 548 if len(notMatched) != 0 { 549 continue 550 } 551 552 found = true 553 554 switch { 555 case i.counter != nil: 556 require.NotNil(t, j.GetCounter()) 557 assert.Equal(t, i.counter.GetValue(), j.GetCounter().GetValue()) 558 case i.gauge != nil: 559 require.NotNil(t, j.GetGauge()) 560 assert.Equal(t, i.gauge.GetValue(), j.GetGauge().GetValue()) 561 case i.histogram != nil: 562 require.NotNil(t, j.GetHistogram()) 563 assert.Equal(t, i.histogram.GetSampleCount(), j.GetHistogram().GetSampleCount()) 564 assert.Equal(t, i.histogram.GetSampleSum(), j.GetHistogram().GetSampleSum()) 565 require.Equal(t, len(i.histogram.GetBucket()), len(j.GetHistogram().GetBucket())) 566 for idx, b := range i.histogram.GetBucket() { 567 actual := j.GetHistogram().GetBucket()[idx] 568 assert.Equal(t, b.GetCumulativeCount(), actual.GetCumulativeCount()) 569 assert.Equal(t, b.GetUpperBound(), actual.GetUpperBound()) 570 } 571 case i.summary != nil: 572 require.NotNil(t, j.GetSummary()) 573 assert.Equal(t, i.summary.GetSampleCount(), j.GetSummary().GetSampleCount()) 574 assert.Equal(t, i.summary.GetSampleSum(), j.GetSummary().GetSampleSum()) 575 require.Equal(t, len(i.summary.GetQuantile()), len(j.GetSummary().GetQuantile())) 576 for idx, q := range i.summary.GetQuantile() { 577 actual := j.GetSummary().GetQuantile()[idx] 578 assert.Equal(t, q.GetQuantile(), actual.GetQuantile()) 579 assert.Equal(t, q.GetValue(), actual.GetValue()) 580 } 581 } 582 } 583 if !found { 584 require.Fail(t, msgFmt("instance not found labels=%v", i.labels)) 585 } 586 } 587 return 588 } 589 require.Fail(t, msgFmt("metric not found")) 590 }