bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cloudwatch/cloudwatch_test.go (about)

     1  package cloudwatch
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/aws/aws-sdk-go/aws/awserr"
    11  	"github.com/aws/aws-sdk-go/service/cloudwatch"
    12  	"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
    13  )
    14  
    15  const namespace = "AWS/Kafka"
    16  const metric = "CpuSystem"
    17  const region = "eu-west-1"
    18  const profile = "default"
    19  const largeCluster = 95
    20  const smallCluster = 15
    21  const expansionLimit = 100
    22  const pagesLimit = 10
    23  
    24  // Singleton in real function prevents injection of appropriate mocks
    25  func MockGetContextWithProvider(p ProfileProvider) Context {
    26  	context = &cloudWatchContext{
    27  		profileProvider: p,
    28  		profiles:        make(map[string]cloudwatchiface.CloudWatchAPI),
    29  		ExpansionLimit:  expansionLimit,
    30  		PagesLimit:      pagesLimit,
    31  	}
    32  	return context
    33  }
    34  
    35  type slowProfileProvider struct {
    36  	callCount int
    37  }
    38  
    39  func (s *slowProfileProvider) NewProfile(name, region string) cloudwatchiface.CloudWatchAPI {
    40  	s.callCount += 1
    41  	time.Sleep(3 * time.Second)
    42  	return &cloudwatch.CloudWatch{}
    43  }
    44  
    45  func TestGetProfilOnlyCalledOnce(t *testing.T) {
    46  	wg := sync.WaitGroup{}
    47  	provider := &slowProfileProvider{}
    48  
    49  	for i := 0; i < 3; i++ {
    50  		wg.Add(1)
    51  		go func() {
    52  			defer wg.Done()
    53  			ctx, _ := GetContextWithProvider(provider).(*cloudWatchContext)
    54  			ctx.getProfile("fake-profile", "fake-region")
    55  		}()
    56  	}
    57  
    58  	wg.Wait()
    59  
    60  	if provider.callCount != 1 {
    61  		t.Errorf("Expected one call to NewProfile, got %d", provider.callCount)
    62  	}
    63  }
    64  
    65  type mockProfileProvider struct {
    66  }
    67  
    68  type mockCloudWatchClient struct {
    69  	cloudwatchiface.CloudWatchAPI
    70  }
    71  
    72  func (m *mockProfileProvider) NewProfile(name, region string) cloudwatchiface.CloudWatchAPI {
    73  	return &mockCloudWatchClient{}
    74  }
    75  
    76  func (c mockCloudWatchClient) ListMetricsPages(li *cloudwatch.ListMetricsInput, callback func(*cloudwatch.ListMetricsOutput, bool) bool) error {
    77  	var metrics []*cloudwatch.Metric
    78  	var n = metric
    79  	var ns = namespace
    80  
    81  	clusters := make(map[string]int)
    82  	if li.Dimensions == nil || (li.Dimensions != nil && *li.Dimensions[0].Value == "big") {
    83  		clusters["big"] = largeCluster
    84  	}
    85  
    86  	if li.Dimensions == nil || (li.Dimensions != nil && *li.Dimensions[0].Value == "small") {
    87  		clusters["small"] = smallCluster
    88  	}
    89  
    90  	for name, size := range clusters {
    91  		for i := 0; i < size; i++ {
    92  			dn := "Broker ID"
    93  			dv := strconv.Itoa(i)
    94  			dim := cloudwatch.Dimension{
    95  				Name:  &dn,
    96  				Value: &dv,
    97  			}
    98  			dimensions := []*cloudwatch.Dimension{&dim}
    99  
   100  			cn := "Cluster Name"
   101  			cv := name
   102  			cdim := cloudwatch.Dimension{
   103  				Name:  &cn,
   104  				Value: &cv,
   105  			}
   106  			dimensions = append(dimensions, &cdim)
   107  			metric := cloudwatch.Metric{
   108  				Dimensions: dimensions,
   109  				MetricName: &n,
   110  				Namespace:  &ns,
   111  			}
   112  			metrics = append(metrics, &metric)
   113  		}
   114  
   115  		// Some aws metrics are logged with varying number of dimensions, to differentiate between cluster
   116  		// level and node level values. The below adds a cluster only metric to test this case
   117  		cn := "Cluster Name"
   118  		cv := name
   119  		dimensions := []*cloudwatch.Dimension{{
   120  			Name:  &cn,
   121  			Value: &cv,
   122  		}}
   123  		metric := cloudwatch.Metric{
   124  			Dimensions: dimensions,
   125  			MetricName: &n,
   126  			Namespace:  &ns,
   127  		}
   128  		metrics = append(metrics, &metric)
   129  	}
   130  
   131  	lmo := &cloudwatch.ListMetricsOutput{
   132  		Metrics:   metrics,
   133  		NextToken: nil,
   134  	}
   135  	callback(lmo, true)
   136  	return nil
   137  }
   138  
   139  func (c mockCloudWatchClient) GetMetricData(input *cloudwatch.GetMetricDataInput) (*cloudwatch.GetMetricDataOutput, error) {
   140  	var mdr []*cloudwatch.MetricDataResult
   141  	cwo := &cloudwatch.GetMetricDataOutput{
   142  		Messages:          nil,
   143  		MetricDataResults: mdr,
   144  		NextToken:         nil,
   145  	}
   146  
   147  	if len(input.MetricDataQueries) == 0 {
   148  		return cwo, nil
   149  	}
   150  
   151  	for i := 0; i < 10; i++ {
   152  		id := fmt.Sprintf("q{i}")
   153  		m := cloudwatch.MetricDataResult{
   154  			Id:         &id,
   155  			Label:      nil,
   156  			Messages:   nil,
   157  			StatusCode: nil,
   158  			Timestamps: nil,
   159  			Values:     nil,
   160  		}
   161  
   162  		mdr = append(mdr, &m)
   163  	}
   164  
   165  	cwo.MetricDataResults = mdr
   166  	return cwo, nil
   167  }
   168  
   169  // Mocks to simulate being rate limited and test error handling
   170  
   171  type rateLimitedProfileProvider struct {
   172  }
   173  type mockCloudWatchRateLimitedClient struct {
   174  	cloudwatchiface.CloudWatchAPI
   175  }
   176  
   177  func (m *rateLimitedProfileProvider) NewProfile(name, region string) cloudwatchiface.CloudWatchAPI {
   178  	return &mockCloudWatchRateLimitedClient{}
   179  }
   180  
   181  func (m *mockCloudWatchRateLimitedClient) GetMetricData(input *cloudwatch.GetMetricDataInput) (*cloudwatch.GetMetricDataOutput, error) {
   182  	e := fmt.Errorf("Rate Limit exceeded")
   183  	ae := awserr.New("429", "Rate Limited Exceeded", e)
   184  	return nil, awserr.NewRequestFailure(ae, 429, "a5442de54s5454")
   185  }
   186  
   187  func (c mockCloudWatchRateLimitedClient) ListMetricsPages(li *cloudwatch.ListMetricsInput, callback func(*cloudwatch.ListMetricsOutput, bool) bool) error {
   188  	e := fmt.Errorf("Rate Limit exceeded")
   189  	ae := awserr.New("429", "Rate Limited Exceeded", e)
   190  	return awserr.NewRequestFailure(ae, 429, "a5442de54s5454")
   191  }
   192  
   193  // -----
   194  
   195  // Mocks for checking paging behaviour
   196  
   197  type pagingProfileProvider struct {
   198  }
   199  type mockCloudWatchPagingClient struct {
   200  	cloudwatchiface.CloudWatchAPI
   201  }
   202  
   203  func (m *pagingProfileProvider) NewProfile(name, region string) cloudwatchiface.CloudWatchAPI {
   204  	return &mockCloudWatchPagingClient{}
   205  }
   206  
   207  func (c mockCloudWatchPagingClient) ListMetricsPages(li *cloudwatch.ListMetricsInput, callback func(*cloudwatch.ListMetricsOutput, bool) bool) error {
   208  	var metrics []*cloudwatch.Metric
   209  	lmo := &cloudwatch.ListMetricsOutput{
   210  		Metrics:   metrics,
   211  		NextToken: nil,
   212  	}
   213  	p := 0
   214  	for callback(lmo, p == pagesLimit) {
   215  		p++
   216  	}
   217  	return nil
   218  }
   219  
   220  // ----------------------------
   221  
   222  func TestLookupDimensions(t *testing.T) {
   223  	c := MockGetContextWithProvider(&mockProfileProvider{})
   224  
   225  	lr := LookupRequest{
   226  		Region:     region,
   227  		Namespace:  namespace,
   228  		Metric:     metric,
   229  		Dimensions: nil,
   230  		Profile:    profile,
   231  	}
   232  
   233  	var tests = []struct {
   234  		dims  [][]Dimension
   235  		count int
   236  		e     error
   237  	}{
   238  		{[][]Dimension{{
   239  			Dimension{
   240  				Name:  "Broker ID",
   241  				Value: "*",
   242  			}, Dimension{
   243  				Name:  "Cluster Name",
   244  				Value: "*",
   245  			},
   246  		}}, 0, ErrExpansionLimit},
   247  		{[][]Dimension{{
   248  			Dimension{
   249  				Name:  "Broker ID",
   250  				Value: "*",
   251  			}, Dimension{
   252  				Name:  "Cluster Name",
   253  				Value: "big",
   254  			},
   255  		}}, largeCluster, nil},
   256  		{[][]Dimension{{
   257  			Dimension{
   258  				Name:  "Broker ID",
   259  				Value: "*",
   260  			}, Dimension{
   261  				Name:  "Cluster Name",
   262  				Value: "small",
   263  			},
   264  		}}, smallCluster, nil},
   265  		{[][]Dimension{{
   266  			Dimension{
   267  				Name:  "Irrelevant Dimension",
   268  				Value: "1234",
   269  			}, Dimension{
   270  				Name:  "Cluster Name",
   271  				Value: "small",
   272  			},
   273  		}}, 0, nil},
   274  	}
   275  	for _, test := range tests {
   276  		lr.Dimensions = test.dims
   277  		res, err := c.LookupDimensions(&lr)
   278  
   279  		if err != test.e {
   280  			t.Error(err)
   281  		}
   282  		if len(res) != test.count {
   283  			t.Errorf("Did not get expected count, wanted %d got %d", test.count, len(res))
   284  		}
   285  	}
   286  
   287  }
   288  
   289  func TestLookupPageLimit(t *testing.T) {
   290  	c := MockGetContextWithProvider(&pagingProfileProvider{})
   291  
   292  	lr := LookupRequest{
   293  		Region:     region,
   294  		Namespace:  namespace,
   295  		Metric:     metric,
   296  		Dimensions: nil,
   297  		Profile:    profile,
   298  	}
   299  
   300  	_, err := c.LookupDimensions(&lr)
   301  	if err != ErrPagingLimit {
   302  		t.Error("Should have failed from hitting expansion limit")
   303  	}
   304  }
   305  
   306  func TestLookupDimensionsError(t *testing.T) {
   307  	c := MockGetContextWithProvider(&rateLimitedProfileProvider{})
   308  	dims := [][]Dimension{{
   309  		Dimension{
   310  			Name:  "Broker ID",
   311  			Value: "*",
   312  		}, Dimension{
   313  			Name:  "Cluster Name",
   314  			Value: "*",
   315  		}}}
   316  
   317  	lr := LookupRequest{
   318  		Region:     region,
   319  		Namespace:  namespace,
   320  		Metric:     metric,
   321  		Dimensions: dims,
   322  		Profile:    profile,
   323  	}
   324  	_, err := c.LookupDimensions(&lr)
   325  	if err == nil {
   326  		t.Error("Error did not bubble up correctly")
   327  	}
   328  }
   329  
   330  func TestQuery(t *testing.T) {
   331  	c := MockGetContextWithProvider(&mockProfileProvider{})
   332  	start := time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
   333  	end := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC)
   334  
   335  	dims := [][]Dimension{{
   336  		Dimension{
   337  			Name:  "Broker ID",
   338  			Value: "*",
   339  		}, Dimension{
   340  			Name:  "Cluster Name",
   341  			Value: "grappler-msk-A",
   342  		}}}
   343  
   344  	tests := []struct {
   345  		r    Request
   346  		err  error
   347  		size int
   348  	}{
   349  		{
   350  			r: Request{
   351  				Start:      &start,
   352  				End:        &end,
   353  				Region:     region,
   354  				Namespace:  namespace,
   355  				Metric:     metric,
   356  				Period:     60,
   357  				Statistic:  "Sum",
   358  				Dimensions: dims,
   359  				Profile:    profile,
   360  			},
   361  			err:  nil,
   362  			size: 10,
   363  		},
   364  		{
   365  			r: Request{
   366  				Start:      &start,
   367  				End:        &end,
   368  				Region:     region,
   369  				Namespace:  namespace,
   370  				Metric:     metric,
   371  				Period:     60,
   372  				Statistic:  "Sum",
   373  				Dimensions: nil,
   374  				Profile:    profile,
   375  			},
   376  			err:  nil,
   377  			size: 10,
   378  		},
   379  		{
   380  			r: Request{
   381  				Start:      &start,
   382  				End:        &end,
   383  				Region:     region,
   384  				Namespace:  namespace,
   385  				Metric:     metric,
   386  				Period:     0,
   387  				Statistic:  "Sum",
   388  				Dimensions: nil,
   389  				Profile:    profile,
   390  			},
   391  			err:  ErrInvalidPeriod,
   392  			size: 0,
   393  		},
   394  	}
   395  	for _, test := range tests {
   396  		res, err := c.Query(&test.r)
   397  		if err != test.err {
   398  			t.Errorf("Query failed, expect error to be %v, got %v", test.err, err)
   399  		}
   400  		if len(res.Raw.MetricDataResults) != test.size {
   401  			t.Errorf("Query returned wrong number of results, expected %d, got %d", test.size, len(res.Raw.MetricDataResults))
   402  		}
   403  	}
   404  
   405  }
   406  
   407  func TestQueryError(t *testing.T) {
   408  	c := MockGetContextWithProvider(&rateLimitedProfileProvider{})
   409  	start := time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)
   410  	end := time.Date(2018, time.January, 1, 1, 0, 0, 0, time.UTC)
   411  
   412  	dims := [][]Dimension{{
   413  		Dimension{
   414  			Name:  "Broker ID",
   415  			Value: "*",
   416  		}, Dimension{
   417  			Name:  "Cluster Name",
   418  			Value: "grappler-msk-A",
   419  		}}}
   420  
   421  	r := Request{
   422  		Start:      &start,
   423  		End:        &end,
   424  		Region:     region,
   425  		Namespace:  namespace,
   426  		Metric:     metric,
   427  		Period:     60,
   428  		Statistic:  "Sum",
   429  		Dimensions: dims,
   430  		Profile:    profile,
   431  	}
   432  	_, err := c.Query(&r)
   433  	if err == nil {
   434  		t.Error("Error did not bubble properly", err)
   435  	}
   436  
   437  }
   438  
   439  func TestFilterDimensions(t *testing.T) {
   440  
   441  	metric := "FreeableMemory"
   442  	namespace := "AWS/ElastiCache"
   443  
   444  	d1 := "CacheClusterId"
   445  	v1 := "grappler-cluster-1"
   446  
   447  	d2 := "CacheNodeId"
   448  	v2 := "0001"
   449  
   450  	v3 := "not-cluster-1"
   451  
   452  	wildcards := make(Wildcards)
   453  	wildcards[d1] = "grappler-cluster-1"
   454  	wildcards[d2] = "0*"
   455  
   456  	// set dimensions that are present in the query and we expect to be in results set
   457  	ds := make(DimensionSet)
   458  	ds[d1] = true
   459  	ds[d2] = true
   460  
   461  	// example of elasticache node level metric
   462  	metric1 := cloudwatch.Metric{
   463  		Dimensions: []*cloudwatch.Dimension{
   464  			{
   465  				Name:  &d1,
   466  				Value: &v1,
   467  			},
   468  			{
   469  				Name:  &d2,
   470  				Value: &v2,
   471  			},
   472  		},
   473  		MetricName: &metric,
   474  		Namespace:  &namespace,
   475  	}
   476  
   477  	// cluster level metric
   478  	metric2 := cloudwatch.Metric{
   479  
   480  		Dimensions: []*cloudwatch.Dimension{
   481  			{
   482  				Name:  &d1,
   483  				Value: &v1,
   484  			}},
   485  		MetricName: &metric,
   486  		Namespace:  &namespace,
   487  	}
   488  
   489  	// account level metric
   490  	metric3 := cloudwatch.Metric{
   491  
   492  		Dimensions: nil,
   493  		MetricName: &metric,
   494  		Namespace:  &namespace,
   495  	}
   496  
   497  	// different cluster than the one we're searching for
   498  	metric4 := cloudwatch.Metric{
   499  		Dimensions: []*cloudwatch.Dimension{
   500  			{
   501  				Name:  &d1,
   502  				Value: &v3,
   503  			},
   504  			{
   505  				Name:  &d2,
   506  				Value: &v2,
   507  			},
   508  		},
   509  		MetricName: &metric,
   510  		Namespace:  &namespace,
   511  	}
   512  
   513  	metrics := []*cloudwatch.Metric{&metric1, &metric2, &metric3, &metric4}
   514  
   515  	m, err := filterDimensions(metrics, wildcards, ds, expansionLimit)
   516  	if err != nil {
   517  		t.Error(err)
   518  	}
   519  	// only  the node level metric should match the filter criteria
   520  	if len(m) != 1 || m[0][0].Value != v1 || m[0][1].Value != v2 {
   521  		t.Error("Filter didn't select correct metric")
   522  	}
   523  
   524  }
   525  
   526  func TestCacheKeyMatch(t *testing.T) {
   527  	start := time.Date(2018, 7, 4, 17, 0, 0, 0, time.UTC)
   528  	end := time.Date(2018, 7, 4, 18, 0, 0, 0, time.UTC)
   529  	var tests = []struct {
   530  		req Request
   531  		key string
   532  	}{
   533  		{req: Request{
   534  			Start:     &start,
   535  			End:       &end,
   536  			Region:    "eu-west-1",
   537  			Namespace: "AWS/EC2",
   538  			Metric:    "CPUUtilization",
   539  			Period:    60, Statistic: "Sum",
   540  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   541  			Profile:         "prod",
   542  		},
   543  			key: "cloudwatch-1530723600-1530727200-eu-west-1-AWS/EC2-CPUUtilization-60-Sum-InstanceId:i-0106b4d25c54baac7-prod"},
   544  	}
   545  
   546  	for _, u := range tests {
   547  		calculatedKey := u.req.CacheKey()
   548  		if u.key != calculatedKey {
   549  			t.Errorf("Cache key doesn't match, expected '%s' got '%s' ", u.key, calculatedKey)
   550  		}
   551  	}
   552  
   553  }
   554  
   555  func TestCacheKeyMisMatch(t *testing.T) {
   556  
   557  	start := time.Date(2018, 7, 4, 17, 0, 0, 0, time.UTC)
   558  	end := time.Date(2018, 7, 4, 18, 0, 0, 0, time.UTC)
   559  	exampleRequest := Request{
   560  		Start:           &start,
   561  		End:             &end,
   562  		Region:          "eu-west-1",
   563  		Namespace:       "AWS/EC2",
   564  		Metric:          "CPUUtilization",
   565  		Period:          60,
   566  		Statistic:       "Sum",
   567  		DimensionString: "InstanceId:i-0106b4d25c54baac7",
   568  		Profile:         "prod",
   569  	}
   570  
   571  	exampleKey := exampleRequest.CacheKey()
   572  
   573  	variantStart := time.Date(2018, 7, 4, 17, 30, 0, 0, time.UTC)
   574  	variantEnd := time.Date(2018, 7, 4, 18, 30, 0, 0, time.UTC)
   575  	var tests = []Request{
   576  		{
   577  			Start:           &start,
   578  			End:             &end,
   579  			Region:          "eu-west-1",
   580  			Namespace:       "AWS/EC2",
   581  			Metric:          "CPUUtilization",
   582  			Period:          60,
   583  			Statistic:       "Sum",
   584  			DimensionString: "InstanceId:i-0106b4d25*",
   585  			Profile:         "prod",
   586  		},
   587  		{
   588  			Start:           &variantStart,
   589  			End:             &end,
   590  			Region:          "eu-west-1",
   591  			Namespace:       "AWS/EC2",
   592  			Metric:          "CPUUtilization",
   593  			Period:          60,
   594  			Statistic:       "Sum",
   595  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   596  			Profile:         "prod",
   597  		},
   598  		{
   599  			Start:           &start,
   600  			End:             &variantEnd,
   601  			Region:          "eu-west-1",
   602  			Namespace:       "AWS/EC2",
   603  			Metric:          "CPUUtilization",
   604  			Period:          60,
   605  			Statistic:       "Sum",
   606  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   607  			Profile:         "prod",
   608  		},
   609  		{
   610  			Start:           &start,
   611  			End:             &end,
   612  			Region:          "eu-central-1",
   613  			Namespace:       "AWS/EC2",
   614  			Metric:          "CPUUtilization",
   615  			Period:          60,
   616  			Statistic:       "Sum",
   617  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   618  			Profile:         "prod",
   619  		},
   620  		{
   621  			Start:           &start,
   622  			End:             &end,
   623  			Region:          "eu-west-1",
   624  			Namespace:       "AWS/ECS",
   625  			Metric:          "CPUUtilization",
   626  			Period:          60,
   627  			Statistic:       "Sum",
   628  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   629  			Profile:         "prod",
   630  		},
   631  		{
   632  			Start:           &start,
   633  			End:             &end,
   634  			Region:          "eu-west-1",
   635  			Namespace:       "AWS/EC2",
   636  			Metric:          "MemoryUsage",
   637  			Period:          60,
   638  			Statistic:       "Sum",
   639  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   640  			Profile:         "prod",
   641  		},
   642  		{
   643  			Start:           &start,
   644  			End:             &end,
   645  			Region:          "eu-west-1",
   646  			Namespace:       "AWS/EC2",
   647  			Metric:          "CPUUtilization",
   648  			Period:          300,
   649  			Statistic:       "Sum",
   650  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   651  			Profile:         "prod",
   652  		},
   653  		{
   654  			Start:           &start,
   655  			End:             &end,
   656  			Region:          "eu-west-1",
   657  			Namespace:       "AWS/EC2",
   658  			Metric:          "CPUUtilization",
   659  			Period:          60,
   660  			Statistic:       "Avg",
   661  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   662  			Profile:         "prod",
   663  		},
   664  		{
   665  			Start:           &start,
   666  			End:             &end,
   667  			Region:          "eu-west-1",
   668  			Namespace:       "AWS/EC2",
   669  			Metric:          "CPUUtilization",
   670  			Period:          300,
   671  			Statistic:       "Sum",
   672  			DimensionString: "InstanceId:i-01064646d6d6baac7",
   673  			Profile:         "prod",
   674  		},
   675  		{
   676  			Start:           &start,
   677  			End:             &end,
   678  			Region:          "eu-west-1",
   679  			Namespace:       "AWS/EC2",
   680  			Metric:          "CPUUtilization",
   681  			Period:          60,
   682  			Statistic:       "Sum",
   683  			DimensionString: "InstanceId:i-0106b4d25c54baac7",
   684  			Profile:         "sandbox",
   685  		},
   686  	}
   687  	for _, u := range tests {
   688  		calculatedKey := u.CacheKey()
   689  		if exampleKey == calculatedKey {
   690  			t.Errorf("Calculated key shouldn't match example but does. '%s' == '%s' ", calculatedKey, exampleKey)
   691  		}
   692  	}
   693  }