github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/telemetry/region_pusher_test.go (about)

     1  package telemetry
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"reflect"
     7  	"sync"
     8  	"testing"
     9  	"testing/synctest"
    10  	"time"
    11  
    12  	"github.com/grafana/synthetic-monitoring-agent/internal/testhelper"
    13  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    14  
    15  	prom "github.com/prometheus/client_golang/prometheus"
    16  	prommodel "github.com/prometheus/client_model/go"
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/grpc"
    19  )
    20  
    21  const (
    22  	instance = "instance"
    23  	regionID = 1
    24  )
    25  
    26  type testData struct {
    27  	executions []Execution
    28  	message    sm.RegionTelemetry
    29  }
    30  
    31  func getTestDataset(idx int) testData {
    32  	data := []testData{
    33  		{
    34  			executions: []Execution{
    35  				{
    36  					LocalTenantID:         1,
    37  					CheckClass:            sm.CheckClass_PROTOCOL,
    38  					Duration:              59 * time.Second,
    39  					CostAttributionLabels: []sm.CostAttributionLabel{},
    40  				},
    41  				{
    42  					LocalTenantID: 1,
    43  					CheckClass:    sm.CheckClass_PROTOCOL,
    44  					Duration:      60 * time.Second,
    45  
    46  					CostAttributionLabels: []sm.CostAttributionLabel{},
    47  				},
    48  				{
    49  					LocalTenantID: 2,
    50  					CheckClass:    sm.CheckClass_SCRIPTED,
    51  					Duration:      61 * time.Second,
    52  
    53  					CostAttributionLabels: []sm.CostAttributionLabel{},
    54  				},
    55  				{
    56  					LocalTenantID: 2,
    57  					CheckClass:    sm.CheckClass_SCRIPTED,
    58  					Duration:      30 * time.Second,
    59  
    60  					CostAttributionLabels: []sm.CostAttributionLabel{},
    61  				},
    62  				{
    63  					LocalTenantID: 3,
    64  					CheckClass:    sm.CheckClass_BROWSER,
    65  					Duration:      61 * time.Second,
    66  
    67  					CostAttributionLabels: []sm.CostAttributionLabel{},
    68  				},
    69  				{
    70  					LocalTenantID: 3,
    71  					CheckClass:    sm.CheckClass_BROWSER,
    72  					Duration:      30 * time.Second,
    73  
    74  					CostAttributionLabels: []sm.CostAttributionLabel{},
    75  				},
    76  				{
    77  					LocalTenantID: 3,
    78  					CheckClass:    sm.CheckClass_BROWSER,
    79  					Duration:      45 * time.Second,
    80  					CostAttributionLabels: []sm.CostAttributionLabel{
    81  						{
    82  							Name:  "env",
    83  							Value: CalNilStringTerminator,
    84  						},
    85  						{
    86  							Name:  "team",
    87  							Value: CalNilStringTerminator,
    88  						},
    89  					},
    90  				},
    91  
    92  				{
    93  					LocalTenantID: 3,
    94  					CheckClass:    sm.CheckClass_BROWSER,
    95  					Duration:      15 * time.Second,
    96  					CostAttributionLabels: []sm.CostAttributionLabel{
    97  						{
    98  							Name:  "env",
    99  							Value: "prod",
   100  						},
   101  						{
   102  							Name:  "team",
   103  							Value: CalNilStringTerminator,
   104  						},
   105  					},
   106  				},
   107  				{
   108  					LocalTenantID: 3,
   109  					CheckClass:    sm.CheckClass_BROWSER,
   110  					Duration:      30 * time.Second,
   111  					CostAttributionLabels: []sm.CostAttributionLabel{
   112  						{
   113  							Name:  "team",
   114  							Value: "a",
   115  						},
   116  						{
   117  							Name:  "env",
   118  							Value: CalNilStringTerminator,
   119  						},
   120  					},
   121  				},
   122  				{
   123  					LocalTenantID: 3,
   124  					CheckClass:    sm.CheckClass_BROWSER,
   125  					Duration:      30 * time.Second,
   126  					CostAttributionLabels: []sm.CostAttributionLabel{
   127  						{
   128  							Name:  "team",
   129  							Value: "a",
   130  						},
   131  						{
   132  							Name:  "env",
   133  							Value: "b",
   134  						},
   135  					},
   136  				},
   137  			},
   138  			message: sm.RegionTelemetry{
   139  				Instance: instance,
   140  				RegionId: 1,
   141  				Telemetry: []*sm.TenantTelemetry{
   142  					{
   143  						TenantId: 1,
   144  						Telemetry: []*sm.CheckClassTelemetry{
   145  							{
   146  								CheckClass:            sm.CheckClass_PROTOCOL,
   147  								Executions:            2,
   148  								Duration:              119,
   149  								SampledExecutions:     2,
   150  								CostAttributionLabels: []sm.CostAttributionLabel{},
   151  							},
   152  						},
   153  					},
   154  					{
   155  						TenantId: 2,
   156  						Telemetry: []*sm.CheckClassTelemetry{
   157  							{
   158  								CheckClass:        sm.CheckClass_SCRIPTED,
   159  								Executions:        2,
   160  								Duration:          91,
   161  								SampledExecutions: 3,
   162  
   163  								CostAttributionLabels: []sm.CostAttributionLabel{},
   164  							},
   165  						},
   166  					},
   167  					{
   168  						TenantId: 3,
   169  						Telemetry: []*sm.CheckClassTelemetry{
   170  							{
   171  								CheckClass:            sm.CheckClass_BROWSER,
   172  								Executions:            2,
   173  								Duration:              91,
   174  								SampledExecutions:     3,
   175  								CostAttributionLabels: []sm.CostAttributionLabel{},
   176  							},
   177  							{
   178  								CheckClass:        sm.CheckClass_BROWSER,
   179  								Executions:        1,
   180  								Duration:          45,
   181  								SampledExecutions: 1,
   182  								CostAttributionLabels: []sm.CostAttributionLabel{
   183  									{
   184  										Name:  "env",
   185  										Value: CalNilStringTerminator,
   186  									},
   187  									{
   188  										Name:  "team",
   189  										Value: CalNilStringTerminator,
   190  									},
   191  								},
   192  							},
   193  
   194  							{
   195  								CheckClass:        sm.CheckClass_BROWSER,
   196  								Executions:        1,
   197  								Duration:          15,
   198  								SampledExecutions: 1,
   199  								CostAttributionLabels: []sm.CostAttributionLabel{
   200  									{
   201  										Name:  "env",
   202  										Value: "prod",
   203  									},
   204  									{
   205  										Name:  "team",
   206  										Value: CalNilStringTerminator,
   207  									},
   208  								},
   209  							},
   210  							{
   211  								CheckClass:        sm.CheckClass_BROWSER,
   212  								Executions:        1,
   213  								Duration:          30,
   214  								SampledExecutions: 1,
   215  								CostAttributionLabels: []sm.CostAttributionLabel{
   216  									{
   217  										Name:  "env",
   218  										Value: CalNilStringTerminator,
   219  									},
   220  									{
   221  										Name:  "team",
   222  										Value: "a",
   223  									},
   224  								},
   225  							},
   226  							{
   227  								CheckClass:        sm.CheckClass_BROWSER,
   228  								Executions:        1,
   229  								Duration:          30,
   230  								SampledExecutions: 1,
   231  								CostAttributionLabels: []sm.CostAttributionLabel{
   232  									{
   233  										Name:  "env",
   234  										Value: "b",
   235  									},
   236  									{
   237  										Name:  "team",
   238  										Value: "a",
   239  									},
   240  								},
   241  							},
   242  						},
   243  					},
   244  				},
   245  			},
   246  		},
   247  		{
   248  			executions: []Execution{
   249  				{
   250  					LocalTenantID: 1,
   251  					CheckClass:    sm.CheckClass_SCRIPTED,
   252  					Duration:      30 * time.Second,
   253  				},
   254  				{
   255  					LocalTenantID: 1,
   256  					CheckClass:    sm.CheckClass_SCRIPTED,
   257  					Duration:      59 * time.Second,
   258  				},
   259  				{
   260  					LocalTenantID: 1,
   261  					CheckClass:    sm.CheckClass_PROTOCOL,
   262  					Duration:      130 * time.Second,
   263  				},
   264  				{
   265  					LocalTenantID: 1,
   266  					CheckClass:    sm.CheckClass_SCRIPTED,
   267  					Duration:      60 * time.Second,
   268  				},
   269  				{
   270  					LocalTenantID: 2,
   271  					CheckClass:    sm.CheckClass_PROTOCOL,
   272  					Duration:      45 * time.Second,
   273  				},
   274  				{
   275  					LocalTenantID: 1,
   276  					CheckClass:    sm.CheckClass_SCRIPTED,
   277  					Duration:      65 * time.Second,
   278  				},
   279  				{
   280  					LocalTenantID: 3,
   281  					CheckClass:    sm.CheckClass_BROWSER,
   282  					Duration:      65 * time.Second,
   283  				},
   284  				{
   285  					LocalTenantID: 3,
   286  					CheckClass:    sm.CheckClass_BROWSER,
   287  					Duration:      30 * time.Second,
   288  					CostAttributionLabels: []sm.CostAttributionLabel{
   289  						{
   290  							Name:  "env",
   291  							Value: CalNilStringTerminator,
   292  						},
   293  						{
   294  							Name:  "team",
   295  							Value: CalNilStringTerminator,
   296  						},
   297  					},
   298  				},
   299  
   300  				{
   301  					LocalTenantID: 3,
   302  					CheckClass:    sm.CheckClass_BROWSER,
   303  					Duration:      30 * time.Second,
   304  					CostAttributionLabels: []sm.CostAttributionLabel{
   305  						{
   306  							Name:  "env",
   307  							Value: "prod",
   308  						},
   309  						{
   310  							Name:  "team",
   311  							Value: CalNilStringTerminator,
   312  						},
   313  					},
   314  				},
   315  				{
   316  					LocalTenantID: 3,
   317  					CheckClass:    sm.CheckClass_BROWSER,
   318  					Duration:      30 * time.Second,
   319  					CostAttributionLabels: []sm.CostAttributionLabel{
   320  						{
   321  							Name:  "team",
   322  							Value: "a",
   323  						},
   324  						{
   325  							Name:  "env",
   326  							Value: CalNilStringTerminator,
   327  						},
   328  					},
   329  				},
   330  				{
   331  					LocalTenantID: 3,
   332  					CheckClass:    sm.CheckClass_BROWSER,
   333  					Duration:      30 * time.Second,
   334  					CostAttributionLabels: []sm.CostAttributionLabel{
   335  						{
   336  							Name:  "team",
   337  							Value: "a",
   338  						},
   339  						{
   340  							Name:  "env",
   341  							Value: "b",
   342  						},
   343  					},
   344  				},
   345  			},
   346  			message: sm.RegionTelemetry{
   347  				Instance: instance,
   348  				RegionId: 1,
   349  				Telemetry: []*sm.TenantTelemetry{
   350  					{
   351  						TenantId: 1,
   352  						Telemetry: []*sm.CheckClassTelemetry{
   353  							{
   354  								CheckClass:            sm.CheckClass_PROTOCOL,
   355  								Executions:            3,
   356  								Duration:              249,
   357  								SampledExecutions:     5,
   358  								CostAttributionLabels: []sm.CostAttributionLabel{},
   359  							},
   360  							{
   361  								CheckClass:            sm.CheckClass_SCRIPTED,
   362  								Executions:            4,
   363  								Duration:              214,
   364  								SampledExecutions:     5,
   365  								CostAttributionLabels: []sm.CostAttributionLabel{},
   366  							},
   367  						},
   368  					},
   369  					{
   370  						TenantId: 2,
   371  						Telemetry: []*sm.CheckClassTelemetry{
   372  							{
   373  								CheckClass:        sm.CheckClass_PROTOCOL,
   374  								Executions:        1,
   375  								Duration:          45,
   376  								SampledExecutions: 1,
   377  
   378  								CostAttributionLabels: []sm.CostAttributionLabel{},
   379  							},
   380  							{
   381  								CheckClass:            sm.CheckClass_SCRIPTED,
   382  								Executions:            2,
   383  								Duration:              91,
   384  								SampledExecutions:     3,
   385  								CostAttributionLabels: []sm.CostAttributionLabel{},
   386  							},
   387  						},
   388  					},
   389  					{
   390  						TenantId: 3,
   391  						Telemetry: []*sm.CheckClassTelemetry{
   392  							{
   393  								CheckClass:            sm.CheckClass_BROWSER,
   394  								Executions:            3,
   395  								Duration:              156,
   396  								SampledExecutions:     5,
   397  								CostAttributionLabels: []sm.CostAttributionLabel{},
   398  							},
   399  							{
   400  								CheckClass:        sm.CheckClass_BROWSER,
   401  								Executions:        2,
   402  								Duration:          75,
   403  								SampledExecutions: 2,
   404  								CostAttributionLabels: []sm.CostAttributionLabel{
   405  									{
   406  										Name:  "env",
   407  										Value: CalNilStringTerminator,
   408  									},
   409  									{
   410  										Name:  "team",
   411  										Value: CalNilStringTerminator,
   412  									},
   413  								},
   414  							},
   415  
   416  							{
   417  								CheckClass:        sm.CheckClass_BROWSER,
   418  								Executions:        2,
   419  								Duration:          45,
   420  								SampledExecutions: 2,
   421  								CostAttributionLabels: []sm.CostAttributionLabel{
   422  									{
   423  										Name:  "env",
   424  										Value: "prod",
   425  									},
   426  									{
   427  										Name:  "team",
   428  										Value: CalNilStringTerminator,
   429  									},
   430  								},
   431  							},
   432  							{
   433  								CheckClass:        sm.CheckClass_BROWSER,
   434  								Executions:        2,
   435  								Duration:          60,
   436  								SampledExecutions: 2,
   437  								CostAttributionLabels: []sm.CostAttributionLabel{
   438  									{
   439  										Name:  "env",
   440  										Value: CalNilStringTerminator,
   441  									},
   442  									{
   443  										Name:  "team",
   444  										Value: "a",
   445  									},
   446  								},
   447  							},
   448  							{
   449  								CheckClass:        sm.CheckClass_BROWSER,
   450  								Executions:        2,
   451  								Duration:          60,
   452  								SampledExecutions: 2,
   453  								CostAttributionLabels: []sm.CostAttributionLabel{
   454  									{
   455  										Name:  "env",
   456  										Value: "b",
   457  									},
   458  									{
   459  										Name:  "team",
   460  										Value: "a",
   461  									},
   462  								},
   463  							},
   464  						},
   465  					},
   466  				},
   467  			},
   468  		},
   469  	}
   470  
   471  	// Why bother with a copy? For the same reason the above data is not a
   472  	// global variable: because we don't trust the caller to behave and not
   473  	// modify the data.
   474  	return testData{
   475  		executions: append([]Execution{}, data[idx].executions...),
   476  		message:    data[idx].message,
   477  	}
   478  }
   479  
   480  func TestTenantPusher(t *testing.T) {
   481  	t.Parallel()
   482  
   483  	var (
   484  		testPushRespOK = testPushResp{
   485  			tr: &sm.PushTelemetryResponse{
   486  				Status: &sm.Status{Code: sm.StatusCode_OK},
   487  			},
   488  		}
   489  		testPushRespKO = testPushResp{
   490  			tr: &sm.PushTelemetryResponse{
   491  				Status: &sm.Status{Code: sm.StatusCode_INTERNAL_ERROR},
   492  			},
   493  		}
   494  	)
   495  
   496  	addExecutions := func(p *RegionPusher, executions []Execution) {
   497  		for _, execution := range executions {
   498  			p.AddExecution(execution)
   499  		}
   500  	}
   501  
   502  	t.Run("should send telemetry data once", func(t *testing.T) {
   503  		t.Parallel()
   504  
   505  		synctest.Test(t, func(t *testing.T) {
   506  			td, tc, pusher, _ := setupTest(t)
   507  
   508  			t.Cleanup(td.shutdownAndWait)
   509  
   510  			// Add some executions.
   511  			addExecutions(pusher, getTestDataset(0).executions)
   512  
   513  			// Set mock response for client.
   514  			tc.rr = testPushRespOK
   515  
   516  			// Tick.
   517  			td.tickAndWait()
   518  
   519  			// Verify sent data.
   520  			tc.assert(t, getTestDataset(0).message)
   521  		})
   522  	})
   523  
   524  	t.Run("should retry sending data once", func(t *testing.T) {
   525  		t.Parallel()
   526  
   527  		synctest.Test(t, func(t *testing.T) {
   528  			td, tc, pusher, _ := setupTest(t)
   529  
   530  			t.Cleanup(td.shutdownAndWait)
   531  
   532  			// Add some executions.
   533  			addExecutions(pusher, getTestDataset(0).executions)
   534  
   535  			// Set mock response for client.
   536  			tc.rr = testPushRespKO
   537  
   538  			// Tick twice, one for initial push and one for retry.
   539  			td.tickAndWait()
   540  			td.tickAndWait()
   541  
   542  			// Verify sent data.
   543  			tc.assert(t, getTestDataset(0).message, getTestDataset(0).message)
   544  		})
   545  	})
   546  
   547  	t.Run("should retry and send more data", func(t *testing.T) {
   548  		t.Parallel()
   549  
   550  		synctest.Test(t, func(t *testing.T) {
   551  			td, tc, pusher, _ := setupTest(t)
   552  
   553  			t.Cleanup(td.shutdownAndWait)
   554  
   555  			// Add some executions.
   556  			addExecutions(pusher, getTestDataset(0).executions)
   557  
   558  			// Set KO mock response for client and tick once.
   559  			tc.rr = testPushRespKO
   560  
   561  			td.tickAndWait()
   562  
   563  			// Send more executions.
   564  			addExecutions(pusher, getTestDataset(1).executions)
   565  
   566  			// Set OK mock response for client and tick again.
   567  			tc.rr = testPushRespOK
   568  
   569  			td.tickAndWait()
   570  
   571  			// Verify sent data.
   572  			tc.assert(t,
   573  				getTestDataset(0).message, // First tick message
   574  				getTestDataset(1).message, // First message retry with newly accumulated data
   575  			)
   576  		})
   577  	})
   578  
   579  	t.Run("should push on context done", func(t *testing.T) {
   580  		t.Parallel()
   581  
   582  		synctest.Test(t, func(t *testing.T) {
   583  			td, tc, pusher, _ := setupTest(t)
   584  
   585  			// Add some executions.
   586  			addExecutions(pusher, getTestDataset(0).executions)
   587  
   588  			// Set mock response for client.
   589  			tc.rr = testPushRespKO
   590  
   591  			// Tick once, which should make the push fail.
   592  			td.tickAndWait()
   593  
   594  			// Verify sent data.
   595  			tc.assert(t, getTestDataset(0).message)
   596  
   597  			// Send more executions.
   598  			addExecutions(pusher, getTestDataset(1).executions)
   599  
   600  			// Cancel the context, which should make the pusher send the currently accumulated data.
   601  			td.shutdownAndWait()
   602  
   603  			// Verify sent data on exit.
   604  			tc.assert(t, getTestDataset(1).message)
   605  		})
   606  	})
   607  
   608  	t.Run("should report push error", func(t *testing.T) {
   609  		t.Parallel()
   610  
   611  		synctest.Test(t, func(t *testing.T) {
   612  			td, tc, pusher, metrics := setupTest(t)
   613  
   614  			t.Cleanup(td.shutdownAndWait)
   615  
   616  			// Setup test client to return err on push.
   617  			tc.rr.err = errors.New("test error")
   618  
   619  			// Add some executions.
   620  			addExecutions(pusher, getTestDataset(0).executions)
   621  
   622  			// Tick once, which should make the push fail
   623  			td.tickAndWait()
   624  
   625  			// Verify sent data.
   626  			tc.assert(t, getTestDataset(0).message)
   627  
   628  			// Verify error metric.
   629  			//
   630  			// td.tickAndWait() above ensures all goroutines have completed or are durably blocked.
   631  			errsMetric := getMetricFromCollector(t, metrics.pushRequestsError)
   632  			require.Equal(t, 1, int(*errsMetric.Counter.Value))
   633  		})
   634  	})
   635  
   636  	t.Run("should report push error on unexpected status", func(t *testing.T) {
   637  		t.Parallel()
   638  
   639  		synctest.Test(t, func(t *testing.T) {
   640  			td, tc, pusher, metrics := setupTest(t)
   641  
   642  			t.Cleanup(td.shutdownAndWait)
   643  
   644  			// Add some executions.
   645  			addExecutions(pusher, getTestDataset(0).executions)
   646  
   647  			// Set mock response for client with unexpected status code.
   648  			tc.rr = testPushRespKO
   649  
   650  			// Tick once, which should make the push fail.
   651  			td.tickAndWait()
   652  
   653  			// Verify sent data.
   654  			tc.assert(t, getTestDataset(0).message)
   655  
   656  			// Verify error metric.
   657  			//
   658  			// td.tickAndWait() above ensures all goroutines have completed or are durably blocked.
   659  			errsMetric := getMetricFromCollector(t, metrics.pushRequestsError)
   660  			require.Equal(t, 1, int(*errsMetric.Counter.Value))
   661  		})
   662  	})
   663  }
   664  
   665  type testTicker struct {
   666  	c chan time.Time
   667  }
   668  
   669  func (t *testTicker) C() <-chan time.Time {
   670  	return t.c
   671  }
   672  
   673  func (t *testTicker) Stop() {
   674  	close(t.c)
   675  }
   676  
   677  type testPushResp struct {
   678  	tr  *sm.PushTelemetryResponse
   679  	err error
   680  }
   681  
   682  func setupTest(t *testing.T) (*testDriver, *testTelemetryClient, *RegionPusher, RegionMetrics) {
   683  	// All setup happens within synctest bubble synctest will track all goroutines automatically.
   684  	//
   685  	// Do not use t.Context() here, as that context is derived from the main context, which is
   686  	// created outside the bubble.
   687  	testCtx, cancel := context.WithCancel(context.Background())
   688  	t.Cleanup(cancel)
   689  
   690  	ticker := &testTicker{c: make(chan time.Time)}
   691  	td := testDriver{cancel: cancel, ticker: ticker}
   692  	tc := &testTelemetryClient{}
   693  	logger := testhelper.Logger(t)
   694  	metrics := RegionMetrics{
   695  		pushRequestsActive:   prom.NewGauge(prom.GaugeOpts{}),
   696  		pushRequestsDuration: prom.NewHistogram(prom.HistogramOpts{}),
   697  		pushRequestsTotal:    prom.NewCounter(prom.CounterOpts{}),
   698  		pushRequestsError:    prom.NewCounter(prom.CounterOpts{}),
   699  		addExecutionDuration: prom.NewHistogram(prom.HistogramOpts{}),
   700  	}
   701  
   702  	// Pass a WaitGroup to maintain compatibility with the production code,
   703  	// but synctest.Wait() will handle the synchronization.
   704  	wg := new(sync.WaitGroup)
   705  
   706  	pusher := NewRegionPusher(
   707  		testCtx,
   708  		1*time.Second,
   709  		tc,
   710  		logger,
   711  		instance,
   712  		regionID,
   713  		metrics,
   714  		WithTicker(ticker),
   715  		WithWaitGroup(wg),
   716  	)
   717  
   718  	return &td, tc, pusher, metrics
   719  }
   720  
   721  type testDriver struct {
   722  	cancel context.CancelFunc
   723  	ticker *testTicker
   724  }
   725  
   726  // tickAndWait will tick the ticker once, so the push process starts, and wait for all goroutines to be durably blocked.
   727  func (td *testDriver) tickAndWait() {
   728  	td.ticker.c <- time.Now()
   729  
   730  	synctest.Wait() // Wait for all goroutines to be durably blocked or complete.
   731  }
   732  
   733  // shutdownAndWait will cancel the context and wait for the pusher to finish.
   734  func (td *testDriver) shutdownAndWait() {
   735  	td.cancel()
   736  	synctest.Wait() // Wait for pusher to finish its final push.
   737  }
   738  
   739  type testTelemetryClient struct {
   740  	mu sync.Mutex
   741  
   742  	rr testPushResp
   743  	mm []sm.RegionTelemetry
   744  }
   745  
   746  func (tc *testTelemetryClient) PushTelemetry(
   747  	ctx context.Context, in *sm.RegionTelemetry, opts ...grpc.CallOption,
   748  ) (*sm.PushTelemetryResponse, error) {
   749  	tc.mu.Lock()
   750  	defer tc.mu.Unlock()
   751  
   752  	tc.mm = append(tc.mm, *in)
   753  
   754  	return tc.rr.tr, tc.rr.err
   755  }
   756  
   757  func (tc *testTelemetryClient) assert(t *testing.T, exp ...sm.RegionTelemetry) {
   758  	t.Helper()
   759  
   760  	defer func() {
   761  		// Copy the remaining messages in the mm buffer to the front, and clip it to that size.
   762  		rest := copy(tc.mm, tc.mm[len(exp):])
   763  		tc.mm = tc.mm[:rest]
   764  	}()
   765  
   766  	for i, msg := range exp {
   767  		assertInfoData(t, &msg, &tc.mm[i])
   768  		assertRegionTelemetryData(t, &msg, &tc.mm[i])
   769  	}
   770  }
   771  
   772  func assertInfoData(t *testing.T, exp, got *sm.RegionTelemetry) {
   773  	t.Helper()
   774  	require.Equal(t, exp.Instance, got.Instance, "instances should match")
   775  	require.Equal(t, exp.RegionId, got.RegionId, "regions should match")
   776  }
   777  
   778  func assertRegionTelemetryData(t *testing.T, exp, got *sm.RegionTelemetry) {
   779  	t.Helper()
   780  	require.Equal(t, len(exp.Telemetry), len(got.Telemetry), "region telemetry length should match")
   781  
   782  	// Because the message is built in the pusher by iterating a map, the
   783  	// order is not deterministic, therefore we have to find each element
   784  LOOP:
   785  	for _, expTenantTele := range exp.Telemetry {
   786  		for j, gotTenantTele := range got.Telemetry {
   787  			if expTenantTele.TenantId == gotTenantTele.TenantId {
   788  				assertTenantTelemetryData(t, expTenantTele, gotTenantTele)
   789  
   790  				got.Telemetry = append(got.Telemetry[:j], got.Telemetry[j+1:]...)
   791  
   792  				continue LOOP
   793  			}
   794  		}
   795  
   796  		t.Fatalf("region telemetry not found: %v", expTenantTele)
   797  	}
   798  }
   799  
   800  func assertTenantTelemetryData(t *testing.T, exp, got *sm.TenantTelemetry) {
   801  	t.Helper()
   802  	require.Equal(t, len(exp.Telemetry), len(got.Telemetry), "tenant telemetry length should match")
   803  
   804  LOOP:
   805  	for _, expTele := range exp.Telemetry {
   806  		for j, gotTele := range got.Telemetry {
   807  			if reflect.DeepEqual(expTele, gotTele) {
   808  				got.Telemetry = append(got.Telemetry[:j], got.Telemetry[j+1:]...)
   809  				continue LOOP
   810  			}
   811  		}
   812  
   813  		t.Fatalf("tenant telemetry not found: %v", expTele)
   814  	}
   815  }
   816  
   817  func getMetricFromCollector(t *testing.T, c prom.Collector) *prommodel.Metric {
   818  	t.Helper()
   819  
   820  	metricCh := make(chan prom.Metric)
   821  	defer close(metricCh)
   822  
   823  	go c.Collect(metricCh)
   824  
   825  	metric := <-metricCh
   826  
   827  	m := &prommodel.Metric{}
   828  	require.NoError(t, metric.Write(m))
   829  
   830  	return m
   831  }