github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/adhoc/adhoc_test.go (about) 1 package adhoc 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "os" 8 "testing" 9 "time" 10 11 "github.com/prometheus/client_golang/prometheus" 12 "github.com/rs/zerolog" 13 "github.com/stretchr/testify/require" 14 "google.golang.org/grpc" 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/connectivity" 17 "google.golang.org/grpc/metadata" 18 "google.golang.org/grpc/status" 19 20 "github.com/google/uuid" 21 "github.com/grafana/synthetic-monitoring-agent/internal/feature" 22 "github.com/grafana/synthetic-monitoring-agent/internal/k6runner" 23 "github.com/grafana/synthetic-monitoring-agent/internal/prober" 24 "github.com/grafana/synthetic-monitoring-agent/internal/prober/logger" 25 "github.com/grafana/synthetic-monitoring-agent/internal/pusher" 26 "github.com/grafana/synthetic-monitoring-agent/internal/testhelper" 27 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 28 ) 29 30 func TestNewHandler(t *testing.T) { 31 features := feature.NewCollection() 32 require.NoError(t, features.Set("adhoc")) 33 34 opts := HandlerOpts{ 35 Conn: nil, 36 Logger: zerolog.New(io.Discard), 37 Publisher: channelPublisher(make(chan pusher.Payload)), 38 TenantCh: make(chan sm.Tenant), 39 PromRegisterer: prometheus.NewPedanticRegistry(), 40 Features: features, 41 } 42 43 h, err := NewHandler(opts) 44 require.NoError(t, err) 45 require.NotNil(t, h) 46 47 require.Equal(t, opts.Conn, h.api.conn) 48 require.Equal(t, opts.Logger, h.logger) 49 require.Equal(t, opts.Features, h.features) 50 require.Equal(t, opts.Publisher, h.publisher) 51 require.NotNil(t, h.runnerFactory) 52 require.NotNil(t, h.grpcAdhocChecksClientFactory) 53 require.Nil(t, h.probe, "probe should not be set at this point") 54 require.NotNil(t, h.metrics.opsCounter) 55 require.False(t, h.supportsProtocolSecrets, "default value should be false") 56 } 57 58 func TestHandlerSupportsProtocolSecrets(t *testing.T) { 59 features := feature.NewCollection() 60 require.NoError(t, features.Set("adhoc")) 61 62 var capturedProbeInfo *sm.ProbeInfo 63 64 testClient := &testClient{ 65 logger: zerolog.New(io.Discard), 66 registerProbeHook: func(info *sm.ProbeInfo) { 67 capturedProbeInfo = info 68 }, 69 } 70 71 opts := HandlerOpts{ 72 Conn: &grpcTestConn{}, 73 Logger: zerolog.New(io.Discard), 74 Publisher: channelPublisher(make(chan pusher.Payload)), 75 TenantCh: make(chan sm.Tenant), 76 PromRegisterer: prometheus.NewPedanticRegistry(), 77 Features: features, 78 SupportsProtocolSecrets: true, 79 grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) { 80 return testClient, nil 81 }, 82 } 83 84 h, err := NewHandler(opts) 85 require.NoError(t, err) 86 require.NotNil(t, h) 87 require.True(t, h.supportsProtocolSecrets, "should be set to true") 88 89 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 90 defer cancel() 91 92 // Run will call RegisterProbe, which should capture the ProbeInfo 93 _ = h.Run(ctx) 94 95 require.NotNil(t, capturedProbeInfo, "RegisterProbe should have been called") 96 require.True(t, capturedProbeInfo.SupportsProtocolSecrets, "SupportsProtocolSecrets should be true") 97 } 98 99 func TestHandlerRun(t *testing.T) { 100 features := feature.NewCollection() 101 require.NoError(t, features.Set("adhoc")) 102 103 logger := zerolog.New(io.Discard) 104 if testing.Verbose() { 105 logger = zerolog.New(os.Stdout) 106 } 107 108 publishCh := make(chan pusher.Payload) 109 110 zerolog.SetGlobalLevel(zerolog.WarnLevel) // default log level. 111 112 opts := HandlerOpts{ 113 Logger: logger, 114 Publisher: channelPublisher(publishCh), 115 TenantCh: make(chan sm.Tenant), 116 PromRegisterer: prometheus.NewPedanticRegistry(), 117 Features: features, 118 runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) { 119 return &runner{ 120 logger: logger, 121 prober: &testProber{logger}, 122 id: req.AdHocCheck.Id, 123 target: req.AdHocCheck.Target, 124 probe: "testProbe", 125 }, nil 126 }, 127 grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) { 128 return &testClient{logger: logger}, nil 129 }, 130 } 131 132 h, err := NewHandler(opts) 133 require.NoError(t, err) 134 require.NotNil(t, h) 135 136 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 137 defer cancel() 138 139 runErr := h.Run(ctx) 140 require.NoError(t, runErr) 141 142 payload := <-publishCh 143 require.Len(t, payload.Metrics(), 0) 144 require.Len(t, payload.Streams(), 1) 145 146 entry := payload.Streams()[0].Entries[0] 147 require.Contains(t, entry.Line, "ad-hoc check done") // log must publish even when verbose is disabled for check tests 148 require.Len(t, entry.StructuredMetadata, 1) 149 require.Equal(t, "execution_id", entry.StructuredMetadata[0].Name) 150 _, err = uuid.Parse(entry.StructuredMetadata[0].Value) 151 require.NoError(t, err, "execution_id should be a valid UUID") 152 } 153 154 type channelPublisher chan pusher.Payload 155 156 func (c channelPublisher) Publish(payload pusher.Payload) { 157 c <- payload 158 } 159 160 type grpcTestError struct { 161 code codes.Code 162 msg string 163 } 164 165 func newGrpcTestError(code codes.Code, msg string) *grpcTestError { 166 return &grpcTestError{ 167 code: code, 168 msg: msg, 169 } 170 } 171 172 func (e grpcTestError) Error() string { 173 return e.msg 174 } 175 176 func (e grpcTestError) GRPCStatus() *status.Status { 177 return status.New(e.code, e.msg) 178 } 179 180 type testBackoff struct{} 181 182 func (testBackoff) Duration() time.Duration { 183 return 0 184 } 185 186 func (testBackoff) Reset() {} 187 188 type timeoutBackoff struct{} 189 190 func (timeoutBackoff) Duration() time.Duration { 191 return 100 * time.Millisecond 192 } 193 194 func (timeoutBackoff) Reset() {} 195 196 type grpcTestConn struct{} 197 198 func (grpcTestConn) GetState() connectivity.State { 199 return connectivity.Ready 200 } 201 202 func (grpcTestConn) Invoke(context.Context, string, any, any, ...grpc.CallOption) error { 203 return nil 204 } 205 206 func (grpcTestConn) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { 207 return nil, nil 208 } 209 210 func TestHandlerRunErrors(t *testing.T) { 211 features := feature.NewCollection() 212 require.NoError(t, features.Set("adhoc")) 213 214 logger := zerolog.New(io.Discard) 215 if testing.Verbose() { 216 logger = zerolog.New(os.Stdout) 217 } 218 219 publishCh := make(chan pusher.Payload) 220 221 testcases := map[string]struct { 222 expectError bool 223 registerProbeError error 224 getAdhocChecksError error 225 }{ 226 "canceled": { 227 expectError: false, 228 registerProbeError: newGrpcTestError(codes.Canceled, "context canceled"), 229 }, 230 "unimplemented": { 231 expectError: true, 232 registerProbeError: newGrpcTestError(codes.Unimplemented, "not implemented"), 233 }, 234 "permission denied": { 235 expectError: true, 236 registerProbeError: newGrpcTestError(codes.PermissionDenied, "not authorized"), 237 }, 238 "idle timeout": { 239 expectError: true, 240 registerProbeError: newGrpcTestError(codes.Unavailable, "unexpected HTTP status code received from server: 504 (Gateway Timeout); transport: received unexpected content-type \"text/html\""), 241 }, 242 // "transport closing": { 243 // registerProbeError: newGrpcTestError(codes.Aborted, "transport is closing"), 244 // }, 245 // "other": { 246 // registerProbeError: errors.New("arbitrary error"), 247 // }, 248 } 249 250 for name, tc := range testcases { 251 t.Run(name, func(t *testing.T) { 252 tc := tc 253 254 // Use timeoutBackoff for idle timeout test to prevent infinite loop 255 var backoff Backoffer = testBackoff{} 256 if name == "idle timeout" { 257 backoff = timeoutBackoff{} 258 } 259 260 opts := HandlerOpts{ 261 Conn: &grpcTestConn{}, 262 Logger: logger, 263 Publisher: channelPublisher(publishCh), 264 Backoff: backoff, 265 TenantCh: make(chan sm.Tenant), 266 PromRegisterer: prometheus.NewPedanticRegistry(), 267 Features: features, 268 runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) { 269 return &runner{ 270 logger: logger, 271 prober: &testProber{logger}, 272 id: req.AdHocCheck.Id, 273 target: req.AdHocCheck.Target, 274 probe: "testProbe", 275 }, nil 276 }, 277 grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) { 278 return &testClient{logger: logger, registerProbeError: tc.registerProbeError}, nil 279 }, 280 } 281 282 h, err := NewHandler(opts) 283 require.NoError(t, err) 284 require.NotNil(t, h) 285 286 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 287 defer cancel() 288 289 runErr := h.Run(ctx) 290 if tc.expectError { 291 require.Error(t, runErr) 292 } else { 293 require.NoError(t, runErr) 294 } 295 296 select { 297 case <-publishCh: 298 t.Fail() 299 default: 300 } 301 }) 302 } 303 } 304 305 type testClient struct { 306 logger zerolog.Logger 307 registerProbeError error 308 getAdhocChecksError error 309 registerProbeHook func(*sm.ProbeInfo) 310 } 311 312 func (c *testClient) RegisterProbe(ctx context.Context, in *sm.ProbeInfo, opts ...grpc.CallOption) (*sm.RegisterProbeResult, error) { 313 c.logger.Info().Str("func", "RegisterProbe").Interface("in", in).Interface("opts", opts).Send() 314 315 if c.registerProbeHook != nil { 316 c.registerProbeHook(in) 317 } 318 319 if c.registerProbeError != nil { 320 return nil, c.registerProbeError 321 } 322 323 return &sm.RegisterProbeResult{ 324 Probe: sm.Probe{ 325 Id: 1, 326 TenantId: 1000, 327 Name: "testProbe", 328 Version: in.Version, 329 Commit: in.Commit, 330 Buildstamp: in.Buildstamp, 331 }, 332 Status: sm.Status{ 333 Code: sm.StatusCode_OK, 334 Message: "OK", 335 }, 336 }, nil 337 } 338 339 type testGetAdhocChecksClient struct { 340 count int 341 logger zerolog.Logger 342 ctx context.Context 343 } 344 345 func (c *testClient) GetAdHocChecks(ctx context.Context, in *sm.Void, opts ...grpc.CallOption) (sm.AdHocChecks_GetAdHocChecksClient, error) { 346 c.logger.Info().Str("func", "GetAdHocChecks").Interface("in", in).Interface("opts", opts).Send() 347 348 if c.getAdhocChecksError != nil { 349 return nil, c.getAdhocChecksError 350 } 351 352 return &testGetAdhocChecksClient{logger: c.logger, ctx: ctx}, nil 353 } 354 355 func (c *testGetAdhocChecksClient) Recv() (*sm.AdHocRequest, error) { 356 c.logger.Info().Str("func", "Recv").Caller(0).Send() 357 c.count++ 358 359 switch c.count { 360 case 1: 361 return &sm.AdHocRequest{ 362 AdHocCheck: sm.AdHocCheck{ 363 Id: "test", 364 Target: "testTarget", 365 Timeout: 1, 366 Settings: sm.CheckSettings{ 367 Ping: &sm.PingSettings{}, 368 }, 369 }, 370 }, nil 371 372 default: 373 return nil, io.EOF 374 } 375 } 376 377 func (c *testGetAdhocChecksClient) CloseSend() error { 378 c.logger.Info().Str("func", "CloseSend").Caller(0).Send() 379 return nil 380 } 381 382 func (c *testGetAdhocChecksClient) Context() context.Context { 383 c.logger.Info().Str("func", "Context").Caller(0).Send() 384 return c.ctx 385 } 386 387 func (c *testGetAdhocChecksClient) Header() (metadata.MD, error) { 388 c.logger.Info().Str("func", "Header").Caller(0).Send() 389 return nil, nil 390 } 391 392 func (c *testGetAdhocChecksClient) Trailer() metadata.MD { 393 c.logger.Info().Str("func", "Trailer").Caller(0).Send() 394 return nil 395 } 396 397 func (c *testGetAdhocChecksClient) RecvMsg(any) error { 398 c.logger.Info().Str("func", "RecvMsg").Caller(0).Send() 399 return nil 400 } 401 402 func (c *testGetAdhocChecksClient) SendMsg(any) error { 403 c.logger.Info().Str("func", "SendMsg").Caller(0).Send() 404 return nil 405 } 406 407 type testProber struct { 408 logger zerolog.Logger 409 } 410 411 func (p *testProber) Name() string { 412 p.logger.Info().Str("func", "Name").Caller(0).Send() 413 return "test" 414 } 415 416 func (p *testProber) Probe(ctx context.Context, target string, registry *prometheus.Registry, logger logger.Logger, _ string) (bool, float64) { 417 p.logger.Info().Str("func", "Probe").Caller(0).Send() 418 419 g := prometheus.NewGauge(prometheus.GaugeOpts{ 420 Name: "test", 421 }) 422 g.Set(1) 423 registry.MustRegister(g) 424 425 _ = logger.Log("msg", "test") 426 427 return true, 1 428 } 429 430 func TestIdleTimeoutHandling(t *testing.T) { 431 features := feature.NewCollection() 432 require.NoError(t, features.Set("adhoc")) 433 434 logger := zerolog.New(io.Discard) 435 if testing.Verbose() { 436 logger = zerolog.New(os.Stdout) 437 } 438 439 publishCh := make(chan pusher.Payload) 440 441 opts := HandlerOpts{ 442 Conn: &grpcTestConn{}, 443 Logger: logger, 444 Publisher: channelPublisher(publishCh), 445 Backoff: testBackoff{}, 446 TenantCh: make(chan sm.Tenant), 447 PromRegisterer: prometheus.NewPedanticRegistry(), 448 Features: features, 449 runnerFactory: func(ctx context.Context, req *sm.AdHocRequest) (*runner, error) { 450 return &runner{ 451 logger: logger, 452 prober: &testProber{logger}, 453 id: req.AdHocCheck.Id, 454 target: req.AdHocCheck.Target, 455 probe: "testProbe", 456 }, nil 457 }, 458 grpcAdhocChecksClientFactory: func(conn ClientConn) (sm.AdHocChecksClient, error) { 459 return &testClient{ 460 logger: logger, 461 registerProbeError: newGrpcTestError(codes.Unavailable, "unexpected HTTP status code received from server: 504 (Gateway Timeout); transport: received unexpected content-type \"text/html\""), 462 }, nil 463 }, 464 } 465 466 h, err := NewHandler(opts) 467 require.NoError(t, err) 468 require.NotNil(t, h) 469 470 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 471 defer cancel() 472 473 runErr := h.Run(ctx) 474 require.Error(t, runErr) 475 require.ErrorIs(t, runErr, context.DeadlineExceeded) 476 477 // Should not publish anything for idle timeout 478 select { 479 case <-publishCh: 480 t.Fail() 481 default: 482 } 483 } 484 485 func TestDefaultRunnerFactory(t *testing.T) { 486 t.Parallel() 487 488 features := feature.NewCollection() 489 require.NoError(t, features.Set(feature.K6)) 490 491 logger := zerolog.New(io.Discard) 492 if testing.Verbose() { 493 logger = zerolog.New(os.Stdout) 494 } 495 496 // Initialize the mockRunner and secretStore 497 mockRunner := &testK6Runner{} 498 secretStore := &testhelper.TestSecretStore{} 499 500 testcases := map[string]struct { 501 request *sm.AdHocRequest 502 expectError bool 503 errCheck func(error) bool 504 shouldPanic bool 505 }{ 506 "ping check": { 507 request: &sm.AdHocRequest{ 508 AdHocCheck: sm.AdHocCheck{ 509 Id: "test-ping", 510 TenantId: 1000, 511 Target: "example.com", 512 Timeout: 1000, 513 Settings: sm.CheckSettings{ 514 Ping: &sm.PingSettings{}, 515 }, 516 }, 517 }, 518 }, 519 "http check": { 520 request: &sm.AdHocRequest{ 521 AdHocCheck: sm.AdHocCheck{ 522 Id: "test-http", 523 TenantId: 1000, 524 Target: "http://example.com", 525 Timeout: 1000, 526 Settings: sm.CheckSettings{ 527 Http: &sm.HttpSettings{}, 528 }, 529 }, 530 }, 531 }, 532 "dns check": { 533 request: &sm.AdHocRequest{ 534 AdHocCheck: sm.AdHocCheck{ 535 Id: "test-dns", 536 TenantId: 1000, 537 Target: "example.com", 538 Timeout: 1000, 539 Settings: sm.CheckSettings{ 540 Dns: &sm.DnsSettings{}, 541 }, 542 }, 543 }, 544 }, 545 "tcp check": { 546 request: &sm.AdHocRequest{ 547 AdHocCheck: sm.AdHocCheck{ 548 Id: "test-tcp", 549 TenantId: 1000, 550 Target: "example.com:80", 551 Timeout: 1000, 552 Settings: sm.CheckSettings{ 553 Tcp: &sm.TcpSettings{}, 554 }, 555 }, 556 }, 557 }, 558 "k6 scripted check": { 559 request: &sm.AdHocRequest{ 560 AdHocCheck: sm.AdHocCheck{ 561 Id: "test-scripted", 562 TenantId: 1000, 563 Target: "test-target", 564 Timeout: 1000, 565 Settings: sm.CheckSettings{ 566 Scripted: &sm.ScriptedSettings{}, 567 }, 568 }, 569 }, 570 }, 571 "k6 multihttp check": { 572 request: &sm.AdHocRequest{ 573 AdHocCheck: sm.AdHocCheck{ 574 Id: "test-multihttp", 575 TenantId: 1000, 576 Target: "test-target", 577 Timeout: 1000, 578 Settings: sm.CheckSettings{ 579 Multihttp: &sm.MultiHttpSettings{ 580 Entries: []*sm.MultiHttpEntry{ 581 { 582 Request: &sm.MultiHttpEntryRequest{ 583 Url: "http://example.com", 584 }, 585 }, 586 }, 587 }, 588 }, 589 }, 590 }, 591 }, 592 "k6 browser check": { 593 request: &sm.AdHocRequest{ 594 AdHocCheck: sm.AdHocCheck{ 595 Id: "test-browser", 596 TenantId: 1000, 597 Target: "test-target", 598 Timeout: 1000, 599 Settings: sm.CheckSettings{ 600 Browser: &sm.BrowserSettings{}, 601 }, 602 }, 603 }, 604 }, 605 "zero timeout": { 606 request: &sm.AdHocRequest{ 607 AdHocCheck: sm.AdHocCheck{ 608 Id: "test-zero-timeout", 609 TenantId: 1000, 610 Target: "example.com", 611 Timeout: 0, 612 Settings: sm.CheckSettings{ 613 Ping: &sm.PingSettings{}, 614 }, 615 }, 616 }, 617 }, 618 "empty settings": { 619 request: &sm.AdHocRequest{ 620 AdHocCheck: sm.AdHocCheck{ 621 Id: "test-empty-settings", 622 TenantId: 1000, 623 Target: "example.com", 624 Timeout: 1000, 625 Settings: sm.CheckSettings{}, 626 }, 627 }, 628 expectError: true, 629 shouldPanic: true, 630 }, 631 "nil request": { 632 request: nil, 633 expectError: true, 634 errCheck: func(err error) bool { 635 return errors.Is(err, errInvalidAdHocRequest) 636 }, 637 }, 638 "zero tenant": { 639 request: &sm.AdHocRequest{ 640 AdHocCheck: sm.AdHocCheck{ 641 Id: "test-zero-tenant", 642 TenantId: 0, 643 Target: "example.com", 644 Timeout: 1000, 645 Settings: sm.CheckSettings{}, 646 }, 647 }, 648 expectError: true, 649 errCheck: func(err error) bool { 650 return errors.Is(err, errInvalidAdHocRequest) 651 }, 652 }, 653 } 654 655 for name, tc := range testcases { 656 t.Run(name, func(t *testing.T) { 657 t.Parallel() 658 659 h := &Handler{ 660 logger: logger, 661 features: features, 662 probe: &sm.Probe{ 663 Name: "test-probe", 664 }, 665 proberFactory: prober.NewProberFactory(mockRunner, 0, features, secretStore), 666 } 667 668 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 669 defer cancel() 670 671 if tc.shouldPanic { 672 require.Panics(t, func() { 673 _, _ = h.defaultRunnerFactory(ctx, tc.request) 674 }) 675 676 return 677 } 678 679 runner, err := h.defaultRunnerFactory(ctx, tc.request) 680 if tc.expectError { 681 require.Error(t, err) 682 683 if tc.errCheck != nil { 684 require.True(t, tc.errCheck(err), "unexpected error: %v", err) 685 } 686 687 require.Nil(t, runner) 688 689 return 690 } 691 692 require.NoError(t, err) 693 require.NotNil(t, runner) 694 695 // Verify runner fields 696 require.Equal(t, logger, runner.logger) 697 require.NotNil(t, runner.prober) 698 require.Equal(t, tc.request.AdHocCheck.Id, runner.id) 699 require.Equal(t, "test-probe", runner.probe) 700 701 // For k6-based checks, verify the grace period is added 702 switch tc.request.AdHocCheck.Type() { 703 case sm.CheckTypeMultiHttp, sm.CheckTypeScripted, sm.CheckTypeBrowser: 704 expectedTimeout := time.Duration(tc.request.AdHocCheck.Timeout)*time.Millisecond + k6AdhocGraceTime 705 require.Equal(t, expectedTimeout, runner.timeout) 706 707 default: 708 expectedTimeout := time.Duration(tc.request.AdHocCheck.Timeout) * time.Millisecond 709 require.Equal(t, expectedTimeout, runner.timeout) 710 } 711 }) 712 } 713 } 714 715 // Add mock k6runner 716 type testK6Runner struct{} 717 718 func (r *testK6Runner) WithLogger(logger *zerolog.Logger) k6runner.Runner { 719 return r 720 } 721 722 func (r *testK6Runner) Run(ctx context.Context, script k6runner.Script, secretStore k6runner.SecretStore, _ string) (*k6runner.RunResponse, error) { 723 return &k6runner.RunResponse{}, nil 724 } 725 726 func (*testK6Runner) Versions(_ context.Context) <-chan []string { 727 return nil // Blocks forever if read. 728 }