github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/metamonitoring/metrics_test.go (about) 1 package metamonitoring 2 3 import ( 4 "context" 5 "sync" 6 "testing" 7 "testing/synctest" 8 "time" 9 10 "github.com/grafana/synthetic-monitoring-agent/internal/model" 11 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 12 "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 13 "github.com/prometheus/client_golang/prometheus" 14 "github.com/prometheus/prometheus/prompb" 15 "github.com/rs/zerolog" 16 "github.com/stretchr/testify/require" 17 ) 18 19 type capturedPayload struct { 20 tenantID model.GlobalID 21 metrics []prompb.TimeSeries 22 } 23 24 type mockPublisher struct { 25 mu sync.Mutex 26 payloads []capturedPayload 27 } 28 29 func (m *mockPublisher) Publish(p pusher.Payload) { 30 m.mu.Lock() 31 defer m.mu.Unlock() 32 33 m.payloads = append(m.payloads, capturedPayload{ 34 tenantID: p.Tenant(), 35 metrics: p.Metrics(), 36 }) 37 } 38 39 func (m *mockPublisher) getPayloads() []capturedPayload { 40 m.mu.Lock() 41 defer m.mu.Unlock() 42 43 cp := make([]capturedPayload, len(m.payloads)) 44 copy(cp, m.payloads) 45 46 return cp 47 } 48 49 func newTestHandler(t *testing.T, registry *prometheus.Registry, tenantID model.GlobalID) (Handler, *mockPublisher) { 50 t.Helper() 51 52 pub := &mockPublisher{} 53 handler := NewHandler(HandlerOpts{ 54 Logger: zerolog.New(zerolog.NewTestWriter(t)), 55 Registry: registry, 56 Publisher: pub, 57 TenantID: tenantID, 58 }) 59 require.NotNil(t, handler) 60 61 return handler, pub 62 } 63 64 func TestReportUsage(t *testing.T) { 65 t.Run("publishes metrics with correct tenant ID", func(t *testing.T) { 66 synctest.Test(t, func(t *testing.T) { 67 registry := prometheus.NewRegistry() 68 counter := prometheus.NewCounter(prometheus.CounterOpts{ 69 Name: "test_requests_total", 70 Help: "Test counter", 71 }) 72 registry.MustRegister(counter) 73 counter.Add(5) 74 75 handler, pub := newTestHandler(t, registry, 42) 76 ctx, cancel := context.WithCancel(context.Background()) 77 78 defer func() { 79 cancel() 80 synctest.Wait() 81 }() 82 83 go func() { 84 if err := handler.Run(ctx); err != nil { 85 t.Errorf("hander.Run: %v", err) 86 } 87 }() 88 89 time.Sleep(defaultInterval) 90 synctest.Wait() 91 92 payloads := pub.getPayloads() 93 require.Len(t, payloads, 1) 94 require.Equal(t, model.GlobalID(42), payloads[0].tenantID) 95 }) 96 }) 97 98 t.Run("publishes counter value", func(t *testing.T) { 99 synctest.Test(t, func(t *testing.T) { 100 registry := prometheus.NewRegistry() 101 counter := prometheus.NewCounter(prometheus.CounterOpts{ 102 Name: "test_requests_total", 103 Help: "Test counter", 104 }) 105 registry.MustRegister(counter) 106 counter.Add(7) 107 108 handler, pub := newTestHandler(t, registry, 1) 109 ctx, cancel := context.WithCancel(context.Background()) 110 111 defer func() { 112 cancel() 113 synctest.Wait() 114 }() 115 116 go func() { 117 if err := handler.Run(ctx); err != nil { 118 t.Errorf("hander.Run: %v", err) 119 } 120 }() 121 122 time.Sleep(defaultInterval) 123 synctest.Wait() 124 125 payloads := pub.getPayloads() 126 require.Len(t, payloads, 1) 127 require.Len(t, payloads[0].metrics, 1) 128 129 ts := payloads[0].metrics[0] 130 require.Contains(t, ts.Labels, prompb.Label{Name: "__name__", Value: "test_requests_total"}) 131 require.Len(t, ts.Samples, 1) 132 require.Equal(t, float64(7), ts.Samples[0].Value) 133 }) 134 }) 135 136 t.Run("publishes gauge value", func(t *testing.T) { 137 synctest.Test(t, func(t *testing.T) { 138 registry := prometheus.NewRegistry() 139 gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 140 Name: "test_temperature", 141 Help: "Test gauge", 142 }) 143 registry.MustRegister(gauge) 144 gauge.Set(36.6) 145 146 handler, pub := newTestHandler(t, registry, 1) 147 ctx, cancel := context.WithCancel(context.Background()) 148 149 defer func() { 150 cancel() 151 synctest.Wait() 152 }() 153 154 go func() { 155 if err := handler.Run(ctx); err != nil { 156 t.Errorf("hander.Run: %v", err) 157 } 158 }() 159 160 time.Sleep(defaultInterval) 161 synctest.Wait() 162 163 payloads := pub.getPayloads() 164 require.Len(t, payloads, 1) 165 require.Len(t, payloads[0].metrics, 1) 166 167 ts := payloads[0].metrics[0] 168 require.Contains(t, ts.Labels, prompb.Label{Name: "__name__", Value: "test_temperature"}) 169 require.InDelta(t, 36.6, ts.Samples[0].Value, 0.001) 170 }) 171 }) 172 173 t.Run("preserves metric labels", func(t *testing.T) { 174 synctest.Test(t, func(t *testing.T) { 175 registry := prometheus.NewRegistry() 176 counter := prometheus.NewCounterVec(prometheus.CounterOpts{ 177 Name: "test_requests_total", 178 Help: "Test counter with labels", 179 }, []string{"method", "status"}) 180 registry.MustRegister(counter) 181 counter.WithLabelValues("GET", "200").Add(3) 182 183 handler, pub := newTestHandler(t, registry, 1) 184 ctx, cancel := context.WithCancel(context.Background()) 185 186 defer func() { 187 cancel() 188 synctest.Wait() 189 }() 190 191 go func() { 192 if err := handler.Run(ctx); err != nil { 193 t.Errorf("hander.Run: %v", err) 194 } 195 }() 196 197 time.Sleep(defaultInterval) 198 synctest.Wait() 199 200 payloads := pub.getPayloads() 201 require.Len(t, payloads, 1) 202 require.Len(t, payloads[0].metrics, 1) 203 204 ts := payloads[0].metrics[0] 205 require.Contains(t, ts.Labels, prompb.Label{Name: "__name__", Value: "test_requests_total"}) 206 require.Contains(t, ts.Labels, prompb.Label{Name: "method", Value: "GET"}) 207 require.Contains(t, ts.Labels, prompb.Label{Name: "status", Value: "200"}) 208 }) 209 }) 210 211 t.Run("skips publish when registry is empty", func(t *testing.T) { 212 synctest.Test(t, func(t *testing.T) { 213 registry := prometheus.NewRegistry() 214 handler, pub := newTestHandler(t, registry, 1) 215 ctx, cancel := context.WithCancel(context.Background()) 216 217 defer func() { 218 cancel() 219 synctest.Wait() 220 }() 221 222 go func() { 223 if err := handler.Run(ctx); err != nil { 224 t.Errorf("hander.Run: %v", err) 225 } 226 }() 227 228 time.Sleep(defaultInterval) 229 synctest.Wait() 230 231 payloads := pub.getPayloads() 232 require.Empty(t, payloads) 233 }) 234 }) 235 236 t.Run("skips histogram metrics", func(t *testing.T) { 237 synctest.Test(t, func(t *testing.T) { 238 registry := prometheus.NewRegistry() 239 histogram := prometheus.NewHistogram(prometheus.HistogramOpts{ 240 Name: "test_duration_seconds", 241 Help: "Test histogram", 242 Buckets: prometheus.DefBuckets, 243 }) 244 registry.MustRegister(histogram) 245 histogram.Observe(0.5) 246 247 handler, pub := newTestHandler(t, registry, 1) 248 ctx, cancel := context.WithCancel(context.Background()) 249 250 defer func() { 251 cancel() 252 synctest.Wait() 253 }() 254 255 go func() { 256 if err := handler.Run(ctx); err != nil { 257 t.Errorf("hander.Run: %v", err) 258 } 259 }() 260 261 time.Sleep(defaultInterval) 262 synctest.Wait() 263 264 payloads := pub.getPayloads() 265 require.Empty(t, payloads) 266 }) 267 }) 268 269 t.Run("skips summary metrics", func(t *testing.T) { 270 synctest.Test(t, func(t *testing.T) { 271 registry := prometheus.NewRegistry() 272 summary := prometheus.NewSummary(prometheus.SummaryOpts{ 273 Name: "test_latency_seconds", 274 Help: "Test summary", 275 }) 276 registry.MustRegister(summary) 277 summary.Observe(0.3) 278 279 handler, pub := newTestHandler(t, registry, 1) 280 ctx, cancel := context.WithCancel(context.Background()) 281 282 defer func() { 283 cancel() 284 synctest.Wait() 285 }() 286 287 go func() { 288 if err := handler.Run(ctx); err != nil { 289 t.Errorf("hander.Run: %v", err) 290 } 291 }() 292 293 time.Sleep(defaultInterval) 294 synctest.Wait() 295 296 payloads := pub.getPayloads() 297 require.Empty(t, payloads) 298 }) 299 }) 300 301 t.Run("publishes multiple metrics in one payload", func(t *testing.T) { 302 synctest.Test(t, func(t *testing.T) { 303 registry := prometheus.NewRegistry() 304 counter := prometheus.NewCounter(prometheus.CounterOpts{ 305 Name: "test_counter", 306 Help: "Test counter", 307 }) 308 gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 309 Name: "test_gauge", 310 Help: "Test gauge", 311 }) 312 registry.MustRegister(counter, gauge) 313 counter.Add(1) 314 gauge.Set(2) 315 316 handler, pub := newTestHandler(t, registry, 1) 317 ctx, cancel := context.WithCancel(context.Background()) 318 319 defer func() { 320 cancel() 321 synctest.Wait() 322 }() 323 324 go func() { 325 if err := handler.Run(ctx); err != nil { 326 t.Errorf("hander.Run: %v", err) 327 } 328 }() 329 330 time.Sleep(defaultInterval) 331 synctest.Wait() 332 333 payloads := pub.getPayloads() 334 require.Len(t, payloads, 1) 335 require.Len(t, payloads[0].metrics, 2) 336 }) 337 }) 338 339 t.Run("payload streams are nil", func(t *testing.T) { 340 synctest.Test(t, func(t *testing.T) { 341 registry := prometheus.NewRegistry() 342 counter := prometheus.NewCounter(prometheus.CounterOpts{ 343 Name: "test_counter", 344 Help: "Test counter", 345 }) 346 registry.MustRegister(counter) 347 counter.Add(1) 348 349 pub := &mockPublisher{} 350 351 var rawPayload pusher.Payload 352 353 wrapper := &payloadCapture{inner: pub, captured: &rawPayload} 354 355 handler := NewHandler(HandlerOpts{ 356 Logger: zerolog.New(zerolog.NewTestWriter(t)), 357 Registry: registry, 358 Publisher: wrapper, 359 TenantID: 1, 360 }) 361 362 ctx, cancel := context.WithCancel(context.Background()) 363 364 defer func() { 365 cancel() 366 synctest.Wait() 367 }() 368 369 go func() { 370 if err := handler.Run(ctx); err != nil { 371 t.Errorf("hander.Run: %v", err) 372 } 373 }() 374 375 time.Sleep(defaultInterval) 376 synctest.Wait() 377 378 require.NotNil(t, rawPayload) 379 require.Nil(t, rawPayload.Streams()) 380 }) 381 }) 382 383 t.Run("timestamp is set on samples", func(t *testing.T) { 384 synctest.Test(t, func(t *testing.T) { 385 registry := prometheus.NewRegistry() 386 gauge := prometheus.NewGauge(prometheus.GaugeOpts{ 387 Name: "test_gauge", 388 Help: "Test gauge", 389 }) 390 registry.MustRegister(gauge) 391 gauge.Set(1) 392 393 handler, pub := newTestHandler(t, registry, 1) 394 395 before := time.Now().UnixNano() / 1e6 396 397 ctx, cancel := context.WithCancel(context.Background()) 398 399 defer func() { 400 cancel() 401 synctest.Wait() 402 }() 403 404 go func() { 405 if err := handler.Run(ctx); err != nil { 406 t.Errorf("hander.Run: %v", err) 407 } 408 }() 409 410 time.Sleep(defaultInterval) 411 synctest.Wait() 412 413 after := time.Now().UnixNano() / 1e6 414 415 payloads := pub.getPayloads() 416 require.Len(t, payloads, 1) 417 418 stamp := payloads[0].metrics[0].Samples[0].Timestamp 419 require.GreaterOrEqual(t, stamp, before) 420 require.LessOrEqual(t, stamp, after) 421 }) 422 }) 423 424 t.Run("includes probe name label on metrics", func(t *testing.T) { 425 synctest.Test(t, func(t *testing.T) { 426 registry := prometheus.NewRegistry() 427 counter := prometheus.NewCounter(prometheus.CounterOpts{ 428 Name: "test_requests_total", 429 Help: "Test counter", 430 }) 431 registry.MustRegister(counter) 432 counter.Add(1) 433 434 probeCh := make(chan *synthetic_monitoring.Probe, 1) 435 pub := &mockPublisher{} 436 handler := NewHandler(HandlerOpts{ 437 Logger: zerolog.New(zerolog.NewTestWriter(t)), 438 Registry: registry, 439 Publisher: pub, 440 ProbeCh: probeCh, 441 }) 442 443 ctx, cancel := context.WithCancel(context.Background()) 444 445 defer func() { 446 cancel() 447 synctest.Wait() 448 }() 449 450 go func() { 451 if err := handler.Run(ctx); err != nil { 452 t.Errorf("handler.Run: %v", err) 453 } 454 }() 455 456 synctest.Wait() 457 458 probeCh <- &synthetic_monitoring.Probe{ 459 TenantId: 42, 460 Name: "shark-taco", 461 } 462 463 time.Sleep(defaultInterval) 464 synctest.Wait() 465 466 payloads := pub.getPayloads() 467 require.Len(t, payloads, 1) 468 require.Equal(t, model.GlobalID(42), payloads[0].tenantID) 469 ts := payloads[0].metrics[0] 470 require.Contains(t, ts.Labels, prompb.Label{Name: "probe", Value: "shark-taco"}) 471 }) 472 }) 473 } 474 475 func TestRun(t *testing.T) { 476 t.Run("handler returns an error if no tenant is passed to the probeTenantCh", func(t *testing.T) { 477 synctest.Test(t, func(t *testing.T) { 478 probeTenantCh := make(chan *synthetic_monitoring.Probe, 1) 479 handler := NewHandler(HandlerOpts{ 480 Logger: zerolog.New(zerolog.NewTestWriter(t)), 481 Registry: prometheus.NewPedanticRegistry(), 482 Publisher: &mockPublisher{}, 483 ProbeCh: probeTenantCh, 484 }) 485 ctx, cancel := context.WithCancel(context.Background()) 486 cancel() 487 488 err := handler.Run(ctx) 489 require.ErrorIs(t, err, errTenantTimeout) 490 }) 491 }) 492 493 t.Run("gets the tenantID from the channel if the TenantID is not set", func(t *testing.T) { 494 synctest.Test(t, func(t *testing.T) { 495 probeTenantCh := make(chan *synthetic_monitoring.Probe, 1) 496 handler := NewHandler(HandlerOpts{ 497 Logger: zerolog.New(zerolog.NewTestWriter(t)), 498 Registry: prometheus.NewPedanticRegistry(), 499 Publisher: &mockPublisher{}, 500 ProbeCh: probeTenantCh, 501 }) 502 ctx, cancel := context.WithCancel(context.Background()) 503 504 defer func() { 505 cancel() 506 synctest.Wait() 507 }() 508 509 go func() { 510 if err := handler.Run(ctx); err != nil { 511 t.Errorf("handler.Run: %v", err) 512 } 513 }() 514 515 synctest.Wait() 516 517 probeTenantCh <- &synthetic_monitoring.Probe{ 518 TenantId: int64(1), 519 } 520 }) 521 }) 522 t.Run("publishes on each tick", func(t *testing.T) { 523 synctest.Test(t, func(t *testing.T) { 524 registry := prometheus.NewRegistry() 525 counter := prometheus.NewCounter(prometheus.CounterOpts{ 526 Name: "test_tick_counter", 527 Help: "Test counter", 528 }) 529 registry.MustRegister(counter) 530 counter.Add(1) 531 532 pub := &mockPublisher{} 533 handler := NewHandler(HandlerOpts{ 534 Logger: zerolog.New(zerolog.NewTestWriter(t)), 535 Registry: registry, 536 Publisher: pub, 537 TenantID: 1, 538 Interval: 50 * time.Millisecond, 539 }) 540 541 ctx, cancel := context.WithCancel(context.Background()) 542 543 defer func() { 544 cancel() 545 synctest.Wait() 546 }() 547 548 go func() { 549 if err := handler.Run(ctx); err != nil { 550 t.Errorf("hander.Run: %v", err) 551 } 552 }() 553 554 // Advance 3 ticks exactly. 555 time.Sleep(50 * time.Millisecond) 556 synctest.Wait() 557 time.Sleep(50 * time.Millisecond) 558 synctest.Wait() 559 time.Sleep(50 * time.Millisecond) 560 synctest.Wait() 561 562 payloads := pub.getPayloads() 563 require.Len(t, payloads, 3) 564 }) 565 }) 566 567 t.Run("stops on context cancellation", func(t *testing.T) { 568 synctest.Test(t, func(t *testing.T) { 569 registry := prometheus.NewRegistry() 570 pub := &mockPublisher{} 571 handler := NewHandler(HandlerOpts{ 572 Logger: zerolog.New(zerolog.NewTestWriter(t)), 573 Registry: registry, 574 Publisher: pub, 575 TenantID: 1, 576 Interval: time.Hour, 577 }) 578 579 ctx, cancel := context.WithCancel(context.Background()) 580 cancel() 581 582 go func() { 583 if err := handler.Run(ctx); err != nil { 584 t.Errorf("hander.Run: %v", err) 585 } 586 }() 587 588 synctest.Wait() 589 590 payloads := pub.getPayloads() 591 require.Empty(t, payloads) 592 }) 593 }) 594 595 t.Run("defaults interval to 1 minute", func(t *testing.T) { 596 handler, _ := newTestHandler(t, prometheus.NewRegistry(), 1) 597 require.Equal(t, time.Minute, handler.(*metricsHandler).interval) 598 }) 599 } 600 601 // payloadCapture wraps a publisher and captures the raw Payload for interface assertions. 602 type payloadCapture struct { 603 inner pusher.Publisher 604 captured *pusher.Payload 605 } 606 607 func (p *payloadCapture) Publish(payload pusher.Payload) { 608 *p.captured = payload 609 p.inner.Publish(payload) 610 }