github.com/Axway/agent-sdk@v1.1.101/pkg/transaction/metric/metricscollector_test.go (about)

     1  package metric
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/Axway/agent-sdk/pkg/agent"
    16  	apiv1 "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/api/v1"
    17  	management "github.com/Axway/agent-sdk/pkg/apic/apiserver/models/management/v1alpha1"
    18  	defs "github.com/Axway/agent-sdk/pkg/apic/definitions"
    19  	"github.com/Axway/agent-sdk/pkg/cmd"
    20  	"github.com/Axway/agent-sdk/pkg/config"
    21  	"github.com/Axway/agent-sdk/pkg/traceability"
    22  	"github.com/Axway/agent-sdk/pkg/transaction/models"
    23  	"github.com/Axway/agent-sdk/pkg/util/healthcheck"
    24  	"github.com/stretchr/testify/assert"
    25  )
    26  
    27  var (
    28  	apiDetails1 = models.APIDetails{
    29  		ID:                 "111",
    30  		Name:               "111",
    31  		Revision:           1,
    32  		TeamID:             teamID,
    33  		APIServiceInstance: "",
    34  		Stage:              "",
    35  		Version:            "",
    36  	}
    37  	apiDetails2 = models.APIDetails{
    38  		ID:                 "111",
    39  		Name:               "111",
    40  		Revision:           1,
    41  		TeamID:             teamID,
    42  		APIServiceInstance: "",
    43  		Stage:              "",
    44  		Version:            "",
    45  	}
    46  	traceStatus = healthcheck.OK
    47  )
    48  
    49  func getFutureTime() time.Time {
    50  	return time.Now().Add(10 * time.Minute)
    51  }
    52  
    53  func createCentralCfg(url, env string) *config.CentralConfiguration {
    54  
    55  	cfg := config.NewCentralConfig(config.TraceabilityAgent).(*config.CentralConfiguration)
    56  	cfg.URL = url
    57  	cfg.SingleURL = url
    58  	cfg.TenantID = "123456"
    59  	cfg.Environment = env
    60  	cfg.APICDeployment = "test"
    61  	authCfg := cfg.Auth.(*config.AuthConfiguration)
    62  	authCfg.URL = url + "/auth"
    63  	authCfg.Realm = "Broker"
    64  	authCfg.ClientID = "serviceaccount_1234"
    65  	authCfg.PrivateKey = "../../transaction/testdata/private_key.pem"
    66  	authCfg.PublicKey = "../../transaction/testdata/public_key"
    67  	usgCfg := cfg.UsageReporting.(*config.UsageReportingConfiguration)
    68  	usgCfg.Publish = true
    69  	metricCfg := cfg.MetricReporting.(*config.MetricReportingConfiguration)
    70  	metricCfg.Publish = true
    71  	// metricCfg.Schedule = "1 * * * * * *"
    72  	return cfg
    73  }
    74  
    75  var accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNjE0NjA0NzE0LCJleHAiOjE2NDYxNDA3MTQsImF1ZCI6InRlc3RhdWQiLCJzdWIiOiIxMjM0NTYiLCJvcmdfZ3VpZCI6IjEyMzQtMTIzNC0xMjM0LTEyMzQifQ.5Uqt0oFhMgmI-sLQKPGkHwknqzlTxv-qs9I_LmZ18LQ"
    76  
    77  var teamID = "team123"
    78  
    79  type testHTTPServer struct {
    80  	lighthouseEventCount int
    81  	transactionCount     int
    82  	transactionVolume    int
    83  	failUsageEvent       bool
    84  	failUsageResponse    *UsageResponse
    85  	server               *httptest.Server
    86  	reportCount          int
    87  	givenGranularity     int
    88  	eventTimestamp       ISO8601Time
    89  }
    90  
    91  func (s *testHTTPServer) startServer() {
    92  	s.server = httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
    93  		if strings.Contains(req.RequestURI, "/auth") {
    94  			token := "{\"access_token\":\"" + accessToken + "\",\"expires_in\": 12235677}"
    95  			resp.Write([]byte(token))
    96  		}
    97  		if strings.Contains(req.RequestURI, "/lighthouse") {
    98  			if s.failUsageEvent {
    99  				if s.failUsageResponse != nil {
   100  					b, _ := json.Marshal(*s.failUsageResponse)
   101  					resp.WriteHeader(s.failUsageResponse.StatusCode)
   102  					resp.Write(b)
   103  					return
   104  				}
   105  				resp.WriteHeader(http.StatusBadRequest)
   106  				return
   107  			}
   108  			s.lighthouseEventCount++
   109  			req.ParseMultipartForm(1 << 20)
   110  			for _, fileHeaders := range req.MultipartForm.File {
   111  				for _, fileHeader := range fileHeaders {
   112  					file, err := fileHeader.Open()
   113  					if err != nil {
   114  						return
   115  					}
   116  					body, _ := io.ReadAll(file)
   117  					var usageEvent UsageEvent
   118  					json.Unmarshal(body, &usageEvent)
   119  					fmt.Printf("\n\n %+v \n\n", usageEvent)
   120  					for _, report := range usageEvent.Report {
   121  						for usageType, usage := range report.Usage {
   122  							if strings.Index(usageType, "Transactions") > 0 {
   123  								s.transactionCount += int(usage)
   124  							} else if strings.Index(usageType, "Volume") > 0 {
   125  								s.transactionVolume += int(usage)
   126  							}
   127  						}
   128  						s.reportCount++
   129  					}
   130  					s.givenGranularity = usageEvent.Granularity
   131  					s.eventTimestamp = usageEvent.Timestamp
   132  				}
   133  			}
   134  		}
   135  		resp.WriteHeader(202)
   136  	}))
   137  }
   138  
   139  func (s *testHTTPServer) closeServer() {
   140  	if s.server != nil {
   141  		s.server.Close()
   142  	}
   143  }
   144  
   145  func (s *testHTTPServer) resetConfig() {
   146  	s.lighthouseEventCount = 0
   147  	s.transactionCount = 0
   148  	s.transactionVolume = 0
   149  	s.failUsageEvent = false
   150  	s.givenGranularity = 0
   151  	s.reportCount = 0
   152  }
   153  
   154  func (s *testHTTPServer) resetOffline(myCollector Collector) {
   155  	events := myCollector.(*collector).reports.loadEvents()
   156  	events.Report = make(map[string]UsageReport)
   157  	myCollector.(*collector).reports.updateEvents(events)
   158  	s.resetConfig()
   159  }
   160  
   161  func cleanUpCachedMetricFile() {
   162  	os.RemoveAll("./cache")
   163  }
   164  
   165  func generateMockReports(transactionPerReport []int) UsageEvent {
   166  	jsonStructure := `{"envId":"267bd671-e5e2-4679-bcc3-bbe7b70f30fd","timestamp":"2024-02-14T10:30:00+02:00","granularity":3600000,"schemaId":"http://127.0.0.1:53493/lighthouse/api/v1/report.schema.json","report":{},"meta":{"AgentName":"","AgentVersion":""}}`
   167  	var mockEvent UsageEvent
   168  	json.Unmarshal([]byte(jsonStructure), &mockEvent)
   169  	startDate := time.Time(mockEvent.Timestamp)
   170  	nextTime := func(i int) string {
   171  		next := startDate.Add(time.Hour * time.Duration(-i-1))
   172  		return next.Format(ISO8601)
   173  	}
   174  	for i, transaction := range transactionPerReport {
   175  		mockEvent.Report[nextTime(i)] = UsageReport{
   176  			Product: "Azure",
   177  			Usage:   map[string]int64{"Azure.Transactions": int64(transaction)},
   178  		}
   179  	}
   180  	return mockEvent
   181  }
   182  
   183  func cleanUpReportfiles() {
   184  	os.RemoveAll("./reports")
   185  }
   186  
   187  func createRI(group, kind, id, name string, subRes map[string]interface{}) *apiv1.ResourceInstance {
   188  	return &apiv1.ResourceInstance{
   189  		ResourceMeta: apiv1.ResourceMeta{
   190  			Metadata: apiv1.Metadata{
   191  				ID: id,
   192  			},
   193  			GroupVersionKind: apiv1.GroupVersionKind{
   194  				GroupKind: apiv1.GroupKind{
   195  					Group: group,
   196  					Kind:  kind,
   197  				},
   198  			},
   199  			SubResources: subRes,
   200  			Name:         name,
   201  		},
   202  	}
   203  }
   204  
   205  func createAPIServiceInstance(id, name string, apiID string) *apiv1.ResourceInstance {
   206  	sub := map[string]interface{}{
   207  		defs.XAgentDetails: map[string]interface{}{
   208  			defs.AttrExternalAPIID: apiID,
   209  		},
   210  	}
   211  	return createRI(management.APIServiceInstanceGVK().Group, management.APIServiceInstanceGVK().Kind, id, name, sub)
   212  }
   213  
   214  func createManagedApplication(id, name, consumerOrgID string) *apiv1.ResourceInstance {
   215  	var marketplaceSubRes map[string]interface{}
   216  	if consumerOrgID != "" {
   217  		marketplaceSubRes = map[string]interface{}{
   218  			"marketplace": management.ManagedApplicationMarketplace{
   219  				Name: name,
   220  				Resource: management.ManagedApplicationMarketplaceResource{
   221  					Owner: &apiv1.Owner{
   222  						Organization: apiv1.Organization{
   223  							ID: consumerOrgID,
   224  						},
   225  					},
   226  				},
   227  			},
   228  		}
   229  	}
   230  	return createRI(management.ManagedApplicationGVK().Group, management.ManagedApplicationGVK().Kind, id, name, marketplaceSubRes)
   231  }
   232  
   233  func createAccessRequest(id, name, appName, instanceID, instanceName, subscriptionName string) *apiv1.ResourceInstance {
   234  	ar := &management.AccessRequest{
   235  		ResourceMeta: apiv1.ResourceMeta{
   236  			Metadata: apiv1.Metadata{
   237  				ID: id,
   238  				References: []apiv1.Reference{
   239  					{
   240  						Group: management.APIServiceInstanceGVK().Group,
   241  						Kind:  management.APIServiceInstanceGVK().Kind,
   242  						ID:    instanceID,
   243  						Name:  instanceName,
   244  					},
   245  				},
   246  			},
   247  			Name: name,
   248  		},
   249  		Spec: management.AccessRequestSpec{
   250  			ManagedApplication: appName,
   251  			ApiServiceInstance: instanceName,
   252  		},
   253  		References: []interface{}{
   254  			management.AccessRequestReferencesSubscription{
   255  				Kind: defs.Subscription,
   256  				Name: "catalog/" + subscriptionName,
   257  			},
   258  		},
   259  	}
   260  	ri, _ := ar.AsInstance()
   261  	return ri
   262  }
   263  
   264  func runTestHealthcheck() {
   265  	// register a healthcheck
   266  	healthcheck.RegisterHealthcheck("Traceability", traceability.HealthCheckEndpoint,
   267  		func(name string) *healthcheck.Status {
   268  			return &healthcheck.Status{Result: traceStatus}
   269  		},
   270  	)
   271  	healthcheck.RunChecks()
   272  }
   273  
   274  func TestMetricCollector(t *testing.T) {
   275  	defer cleanUpCachedMetricFile()
   276  	s := &testHTTPServer{}
   277  	defer s.closeServer()
   278  	s.startServer()
   279  	traceability.SetDataDirPath(".")
   280  
   281  	cfg := createCentralCfg(s.server.URL, "demo")
   282  	cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   283  	cfg.MetricReporting.(*config.MetricReportingConfiguration).Publish = true
   284  	cfg.SetEnvironmentID("267bd671-e5e2-4679-bcc3-bbe7b70f30fd")
   285  	cmd.BuildDataPlaneType = "Azure"
   286  	agent.Initialize(cfg)
   287  
   288  	cm := agent.GetCacheManager()
   289  	cm.AddAPIServiceInstance(createAPIServiceInstance("inst-1", "instance-1", "111"))
   290  
   291  	cm.AddManagedApplication(createManagedApplication("app-1", "managed-app-1", ""))
   292  	cm.AddManagedApplication(createManagedApplication("app-2", "managed-app-2", "test-consumer-org"))
   293  
   294  	cm.AddAccessRequest(createAccessRequest("ac-1", "access-req-1", "managed-app-1", "inst-1", "instance-1", "subscription-1"))
   295  	cm.AddAccessRequest(createAccessRequest("ac-2", "access-req-2", "managed-app-2", "inst-1", "instance-1", "subscription-2"))
   296  
   297  	myCollector := createMetricCollector()
   298  	metricCollector := myCollector.(*collector)
   299  
   300  	testCases := []struct {
   301  		name                      string
   302  		loopCount                 int
   303  		retryBatchCount           int
   304  		apiTransactionCount       []int
   305  		failUsageEventOnServer    []bool
   306  		failUsageResponseOnServer []*UsageResponse
   307  		expectedLHEvents          []int
   308  		expectedTransactionCount  []int
   309  		trackVolume               bool
   310  		expectedTransactionVolume []int
   311  		expectedMetricEventsAcked int
   312  		appName                   string
   313  		publishPrior              bool
   314  		hcStatus                  healthcheck.StatusLevel
   315  	}{
   316  		// Success case with no app detail
   317  		{
   318  			name:                      "WithUsage",
   319  			loopCount:                 1,
   320  			retryBatchCount:           0,
   321  			apiTransactionCount:       []int{5},
   322  			failUsageEventOnServer:    []bool{false},
   323  			failUsageResponseOnServer: []*UsageResponse{nil},
   324  			expectedLHEvents:          []int{1},
   325  			expectedTransactionCount:  []int{5},
   326  			trackVolume:               false,
   327  			expectedTransactionVolume: []int{0},
   328  			expectedMetricEventsAcked: 1, // API metric + no Provider subscription metric
   329  		},
   330  		{
   331  			name:                      "WithUsageWithPriorPublish",
   332  			loopCount:                 1,
   333  			retryBatchCount:           0,
   334  			apiTransactionCount:       []int{5},
   335  			failUsageEventOnServer:    []bool{false},
   336  			failUsageResponseOnServer: []*UsageResponse{nil},
   337  			expectedLHEvents:          []int{1},
   338  			expectedTransactionCount:  []int{5},
   339  			trackVolume:               false,
   340  			expectedTransactionVolume: []int{0},
   341  			expectedMetricEventsAcked: 1, // API metric + no Provider subscription metric
   342  			publishPrior:              true,
   343  		},
   344  		// Success case
   345  		{
   346  			name:                      "WithUsage",
   347  			loopCount:                 1,
   348  			retryBatchCount:           0,
   349  			apiTransactionCount:       []int{5},
   350  			failUsageEventOnServer:    []bool{false},
   351  			failUsageResponseOnServer: []*UsageResponse{nil},
   352  			expectedLHEvents:          []int{1},
   353  			expectedTransactionCount:  []int{5},
   354  			trackVolume:               false,
   355  			expectedTransactionVolume: []int{0},
   356  			expectedMetricEventsAcked: 1, // API metric + Provider subscription metric
   357  			appName:                   "managed-app-1",
   358  		},
   359  		// Success case with consumer metric event
   360  		{
   361  			name:                      "WithUsage",
   362  			loopCount:                 1,
   363  			retryBatchCount:           0,
   364  			apiTransactionCount:       []int{5},
   365  			failUsageEventOnServer:    []bool{false},
   366  			failUsageResponseOnServer: []*UsageResponse{nil},
   367  			expectedLHEvents:          []int{1},
   368  			expectedTransactionCount:  []int{5},
   369  			trackVolume:               false,
   370  			expectedTransactionVolume: []int{0},
   371  			expectedMetricEventsAcked: 1, // API metric + Provider + Consumer subscription metric
   372  			appName:                   "managed-app-2",
   373  		},
   374  		// Success case with no usage report
   375  		{
   376  			name:                      "WithUsageNoUsageReport",
   377  			loopCount:                 1,
   378  			retryBatchCount:           0,
   379  			apiTransactionCount:       []int{0},
   380  			failUsageEventOnServer:    []bool{false},
   381  			failUsageResponseOnServer: []*UsageResponse{nil},
   382  			expectedLHEvents:          []int{0},
   383  			expectedTransactionCount:  []int{0},
   384  			trackVolume:               false,
   385  			expectedTransactionVolume: []int{0},
   386  			expectedMetricEventsAcked: 0,
   387  		},
   388  		// Test case with failing request to LH, the subsequent successful request should contain the total count since initial failure
   389  		{
   390  			name:                   "WithUsageWithFailure",
   391  			loopCount:              3,
   392  			retryBatchCount:        0,
   393  			apiTransactionCount:    []int{5, 10, 12},
   394  			failUsageEventOnServer: []bool{false, true, false, false},
   395  			failUsageResponseOnServer: []*UsageResponse{
   396  				nil, {
   397  					Description: "Regular failure",
   398  					StatusCode:  400,
   399  					Success:     false,
   400  				},
   401  				nil,
   402  				nil,
   403  			},
   404  			expectedLHEvents:          []int{1, 1, 2},
   405  			expectedTransactionCount:  []int{5, 5, 17},
   406  			trackVolume:               true,
   407  			expectedTransactionVolume: []int{50, 50, 170},
   408  			expectedMetricEventsAcked: 1,
   409  			appName:                   "unknown",
   410  		},
   411  		// Test case with failing request to LH, no subsequent request triggered.
   412  		{
   413  			name:                   "WithUsageWithFailureWithSpecificDescription",
   414  			loopCount:              3,
   415  			retryBatchCount:        0,
   416  			apiTransactionCount:    []int{1, 1, 1},
   417  			failUsageEventOnServer: []bool{true, true, false},
   418  			failUsageResponseOnServer: []*UsageResponse{
   419  				{
   420  					Description: "The file exceeds the maximum upload size of 454545",
   421  					StatusCode:  400,
   422  					Success:     false,
   423  				},
   424  				{
   425  					Description: "Environment ID not found",
   426  					StatusCode:  404,
   427  					Success:     false,
   428  				},
   429  				nil,
   430  			},
   431  			expectedLHEvents:          []int{0, 0, 1},
   432  			expectedTransactionCount:  []int{0, 0, 1},
   433  			trackVolume:               true,
   434  			expectedTransactionVolume: []int{0, 0, 10},
   435  			expectedMetricEventsAcked: 1,
   436  			appName:                   "unknown",
   437  		},
   438  		// Retry limit hit
   439  		{
   440  			name:                      "WithUsageAndFailedMetric",
   441  			loopCount:                 1,
   442  			retryBatchCount:           4,
   443  			apiTransactionCount:       []int{5},
   444  			failUsageEventOnServer:    []bool{false},
   445  			failUsageResponseOnServer: []*UsageResponse{nil},
   446  			expectedLHEvents:          []int{1},
   447  			expectedTransactionCount:  []int{5},
   448  			trackVolume:               false,
   449  			expectedTransactionVolume: []int{0},
   450  			expectedMetricEventsAcked: 0,
   451  		},
   452  		// Traceability healthcheck failing, nothing reported
   453  		{
   454  			name:                      "WithUsageTraceabilityNotConnected",
   455  			loopCount:                 1,
   456  			retryBatchCount:           0,
   457  			apiTransactionCount:       []int{0},
   458  			failUsageEventOnServer:    []bool{false},
   459  			failUsageResponseOnServer: []*UsageResponse{nil},
   460  			expectedLHEvents:          []int{0},
   461  			expectedTransactionCount:  []int{0},
   462  			trackVolume:               false,
   463  			expectedTransactionVolume: []int{0},
   464  			expectedMetricEventsAcked: 0, // API metric + Provider subscription metric
   465  			appName:                   "managed-app-1",
   466  			hcStatus:                  healthcheck.FAIL,
   467  		},
   468  	}
   469  
   470  	for _, test := range testCases {
   471  		t.Run(test.name, func(t *testing.T) {
   472  			if test.hcStatus != "" {
   473  				traceStatus = test.hcStatus
   474  			}
   475  			runTestHealthcheck()
   476  
   477  			cfg.SetAxwayManaged(test.trackVolume)
   478  			setupMockClient(test.retryBatchCount)
   479  			for l := 0; l < test.loopCount; l++ {
   480  				fmt.Printf("\n\nTransaction Info: %+v\n\n", test.apiTransactionCount[l])
   481  				for i := 0; i < test.apiTransactionCount[l]; i++ {
   482  					metricDetail := Detail{
   483  						APIDetails: apiDetails1,
   484  						StatusCode: "200",
   485  						Duration:   10,
   486  						Bytes:      10,
   487  						AppDetails: models.AppDetails{
   488  							ID:   "111",
   489  							Name: test.appName,
   490  						},
   491  					}
   492  					metricCollector.AddMetricDetail(metricDetail)
   493  				}
   494  				s.failUsageEvent = test.failUsageEventOnServer[l]
   495  				s.failUsageResponse = test.failUsageResponseOnServer[l]
   496  				if test.publishPrior {
   497  					metricCollector.usagePublisher.Execute()
   498  					metricCollector.Execute()
   499  				} else {
   500  					metricCollector.Execute()
   501  					metricCollector.usagePublisher.Execute()
   502  				}
   503  				assert.Equal(t, test.expectedMetricEventsAcked, myMockClient.(*MockClient).eventsAcked)
   504  			}
   505  			s.resetConfig()
   506  		})
   507  	}
   508  }
   509  
   510  func TestConcurrentMetricCollectorEvents(t *testing.T) {
   511  	// this test has no assertions it is to ensure concurrent map writs do not occur while collecting metrics
   512  	defer cleanUpCachedMetricFile()
   513  	s := &testHTTPServer{}
   514  	defer s.closeServer()
   515  	s.startServer()
   516  	traceability.SetDataDirPath(".")
   517  
   518  	cfg := createCentralCfg(s.server.URL, "demo")
   519  	cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   520  	cfg.MetricReporting.(*config.MetricReportingConfiguration).Publish = true
   521  	cfg.SetEnvironmentID("267bd671-e5e2-4679-bcc3-bbe7b70f30fd")
   522  	cmd.BuildDataPlaneType = "Azure"
   523  	agent.Initialize(cfg)
   524  	myCollector := createMetricCollector()
   525  	metricCollector := myCollector.(*collector)
   526  	traceStatus = healthcheck.OK
   527  	runTestHealthcheck()
   528  
   529  	apiDetails := []models.APIDetails{
   530  		{ID: "000", Name: "000", Revision: 1, TeamID: teamID},
   531  		{ID: "111", Name: "111", Revision: 1, TeamID: teamID},
   532  		{ID: "222", Name: "222", Revision: 1, TeamID: teamID},
   533  		{ID: "333", Name: "333", Revision: 1, TeamID: teamID},
   534  		{ID: "444", Name: "444", Revision: 1, TeamID: teamID},
   535  		{ID: "555", Name: "555", Revision: 1, TeamID: teamID},
   536  		{ID: "666", Name: "666", Revision: 1, TeamID: teamID},
   537  		{ID: "777", Name: "777", Revision: 1, TeamID: teamID},
   538  		{ID: "888", Name: "888", Revision: 1, TeamID: teamID},
   539  		{ID: "999", Name: "999", Revision: 1, TeamID: teamID},
   540  	}
   541  	appDetails := []models.AppDetails{
   542  		{ID: "000", Name: "app0"},
   543  		{ID: "111", Name: "app1"},
   544  		{ID: "222", Name: "app2"},
   545  		{ID: "333", Name: "app3"},
   546  		{ID: "444", Name: "app4"},
   547  		{ID: "555", Name: "app5"},
   548  		{ID: "666", Name: "app6"},
   549  		{ID: "777", Name: "app7"},
   550  		{ID: "888", Name: "app8"},
   551  		{ID: "999", Name: "app9"},
   552  	}
   553  
   554  	codes := []string{"200", "201", "202", "204", "205", "206", "300", "301", "400", "401", "403", "404", "500"}
   555  
   556  	details := []Detail{}
   557  
   558  	// load a bunch of different api details
   559  	for _, api := range apiDetails {
   560  		for _, app := range appDetails {
   561  			for _, code := range codes {
   562  				details = append(details, Detail{APIDetails: api, AppDetails: app, StatusCode: code})
   563  			}
   564  		}
   565  	}
   566  
   567  	// add all metrics via go routines
   568  	wg := sync.WaitGroup{}
   569  	transactionCount := 100
   570  	wg.Add(len(details) * transactionCount)
   571  
   572  	for j := range details {
   573  		for i := 0; i < transactionCount; i++ {
   574  			go func(dets Detail) {
   575  				defer wg.Done()
   576  				metricCollector.AddMetricDetail(dets)
   577  			}(details[j])
   578  		}
   579  	}
   580  
   581  	wg.Wait()
   582  }
   583  
   584  func TestMetricCollectorUsageAggregation(t *testing.T) {
   585  	defer cleanUpCachedMetricFile()
   586  	s := &testHTTPServer{}
   587  	defer s.closeServer()
   588  	s.startServer()
   589  	traceability.SetDataDirPath(".")
   590  
   591  	cfg := createCentralCfg(s.server.URL, "demo")
   592  	cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   593  	cfg.MetricReporting.(*config.MetricReportingConfiguration).Publish = true
   594  	cfg.SetEnvironmentID("267bd671-e5e2-4679-bcc3-bbe7b70f30fd")
   595  	cmd.BuildDataPlaneType = "Azure"
   596  	agent.Initialize(cfg)
   597  
   598  	traceStatus = healthcheck.OK
   599  	runTestHealthcheck()
   600  
   601  	testCases := []struct {
   602  		name                      string
   603  		transactionsPerReport     []int
   604  		expectedTransactionCount  int
   605  		expectedTransactionVolume int
   606  		expectedGranularity       int
   607  		expectedReportCount       int
   608  	}{
   609  		{
   610  			name:                     "FourReports",
   611  			transactionsPerReport:    []int{3, 4, 5, 6},
   612  			expectedTransactionCount: 18,
   613  			expectedGranularity:      4 * int(time.Hour/time.Millisecond),
   614  		},
   615  		{
   616  			name:                     "SevenReports",
   617  			transactionsPerReport:    []int{1, 2, 3, 4, 5, 6, 7},
   618  			expectedTransactionCount: 28,
   619  			expectedGranularity:      7 * int(time.Hour/time.Millisecond),
   620  		},
   621  		{
   622  			name:                     "OneReport",
   623  			transactionsPerReport:    []int{1},
   624  			expectedTransactionCount: 1,
   625  			expectedGranularity:      1 * int(time.Hour/time.Millisecond),
   626  		},
   627  	}
   628  
   629  	for _, test := range testCases {
   630  		t.Run(test.name, func(t *testing.T) {
   631  			cfg.SetAxwayManaged(false)
   632  			setupMockClient(0)
   633  			myCollector := createMetricCollector()
   634  			metricCollector := myCollector.(*collector)
   635  			metricCollector.usagePublisher.schedule = "* * * * *"
   636  			metricCollector.usagePublisher.report.currTimeFunc = getFutureTime
   637  
   638  			mockReports := generateMockReports(test.transactionsPerReport)
   639  			b, _ := json.Marshal(mockReports)
   640  			metricCollector.reports.reportCache.Set("lighthouse_events", string(b))
   641  			now = func() time.Time {
   642  				return time.Time(mockReports.Timestamp)
   643  			}
   644  			metricCollector.usagePublisher.Execute()
   645  			assert.Equal(t, test.expectedTransactionCount, s.transactionCount)
   646  			assert.Equal(t, 1, s.reportCount)
   647  			assert.Equal(t, test.expectedGranularity, s.givenGranularity)
   648  			assert.Equal(t, ISO8601Time(now()), s.eventTimestamp)
   649  			s.resetConfig()
   650  		})
   651  	}
   652  	cleanUpReportfiles()
   653  }
   654  
   655  func TestMetricCollectorCache(t *testing.T) {
   656  	defer cleanUpCachedMetricFile()
   657  	s := &testHTTPServer{}
   658  	defer s.closeServer()
   659  	s.startServer()
   660  
   661  	traceStatus = healthcheck.OK
   662  	runTestHealthcheck()
   663  
   664  	testCases := []struct {
   665  		name        string
   666  		trackVolume bool
   667  	}{
   668  		{
   669  			name:        "UsageOnly",
   670  			trackVolume: false,
   671  		},
   672  		{
   673  			name:        "UsageAndVolume",
   674  			trackVolume: true,
   675  		},
   676  	}
   677  
   678  	for _, test := range testCases {
   679  		t.Run(test.name, func(t *testing.T) {
   680  			cfg := createCentralCfg(s.server.URL, "demo")
   681  			cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   682  			cfg.SetEnvironmentID("267bd671-e5e2-4679-bcc3-bbe7b70f30fd")
   683  			cfg.SetAxwayManaged(test.trackVolume)
   684  			cmd.BuildDataPlaneType = "Azure"
   685  			agent.Initialize(cfg)
   686  
   687  			traceability.SetDataDirPath(".")
   688  			myCollector := createMetricCollector()
   689  			metricCollector := myCollector.(*collector)
   690  			metricCollector.usagePublisher.schedule = "* * * * *"
   691  			metricCollector.usagePublisher.report.currTimeFunc = getFutureTime
   692  
   693  			metricCollector.AddMetric(apiDetails1, "200", 5, 10, "")
   694  			metricCollector.AddMetric(apiDetails1, "200", 10, 10, "")
   695  			metricCollector.Execute()
   696  			metricCollector.usagePublisher.Execute()
   697  			metricCollector.AddMetric(apiDetails1, "401", 15, 10, "")
   698  			metricCollector.AddMetric(apiDetails2, "200", 20, 10, "")
   699  			metricCollector.AddMetric(apiDetails2, "200", 10, 10, "")
   700  
   701  			// No event generation/publish, store the cache
   702  			metricCollector.storage.save()
   703  			// Validate only one usage report sent with first 2 transactions
   704  			assert.Equal(t, 1, s.lighthouseEventCount)
   705  			assert.Equal(t, 2, s.transactionCount)
   706  			if test.trackVolume {
   707  				assert.Equal(t, 20, s.transactionVolume)
   708  			}
   709  			s.resetConfig()
   710  
   711  			// Recreate the collector that loads the stored metrics, so 3 transactions
   712  			myCollector = createMetricCollector()
   713  			metricCollector = myCollector.(*collector)
   714  			metricCollector.usagePublisher.schedule = "* * * * *"
   715  			metricCollector.usagePublisher.report.currTimeFunc = getFutureTime
   716  
   717  			metricCollector.AddMetric(apiDetails1, "200", 5, 10, "")
   718  			metricCollector.AddMetric(apiDetails1, "200", 10, 10, "")
   719  			metricCollector.AddMetric(apiDetails1, "401", 15, 10, "")
   720  			metricCollector.AddMetric(apiDetails2, "200", 20, 10, "")
   721  			metricCollector.AddMetric(apiDetails2, "200", 10, 10, "")
   722  
   723  			metricCollector.Execute()
   724  			metricCollector.usagePublisher.Execute()
   725  			// Validate only one usage report sent with 3 previous transactions and 5 new transactions
   726  			assert.Equal(t, 1, s.lighthouseEventCount)
   727  			assert.Equal(t, 8, s.transactionCount)
   728  			if test.trackVolume {
   729  				assert.Equal(t, 80, s.transactionVolume)
   730  			}
   731  
   732  			s.resetConfig()
   733  			// Recreate the collector that loads the stored metrics, 0 transactions
   734  			myCollector = createMetricCollector()
   735  			metricCollector = myCollector.(*collector)
   736  			metricCollector.usagePublisher.schedule = "* * * * *"
   737  			metricCollector.usagePublisher.report.currTimeFunc = getFutureTime
   738  
   739  			metricCollector.Execute()
   740  			// Validate only no usage report sent as no previous or new transactions
   741  			assert.Equal(t, 0, s.lighthouseEventCount)
   742  			assert.Equal(t, 0, s.transactionCount)
   743  			if test.trackVolume {
   744  				assert.Equal(t, 0, s.transactionVolume)
   745  			}
   746  		})
   747  	}
   748  }
   749  
   750  func TestOfflineMetricCollector(t *testing.T) {
   751  	defer cleanUpCachedMetricFile()
   752  	s := &testHTTPServer{}
   753  	defer s.closeServer()
   754  	s.startServer()
   755  	traceability.SetDataDirPath(".")
   756  
   757  	traceStatus = healthcheck.OK
   758  	runTestHealthcheck()
   759  
   760  	cfg := createCentralCfg(s.server.URL, "demo")
   761  	cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   762  	cfg.EnvironmentID = "267bd671-e5e2-4679-bcc3-bbe7b70f30fd"
   763  	cmd.BuildDataPlaneType = "Azure"
   764  	usgCfg := cfg.UsageReporting.(*config.UsageReportingConfiguration)
   765  	usgCfg.Offline = true
   766  	agent.Initialize(cfg)
   767  
   768  	testCases := []struct {
   769  		name                string
   770  		loopCount           int
   771  		apiTransactionCount []int
   772  		generateReport      bool
   773  	}{
   774  		{
   775  			name:                "NoReports",
   776  			loopCount:           0,
   777  			apiTransactionCount: []int{},
   778  			generateReport:      true,
   779  		},
   780  		{
   781  			name:                "OneReport",
   782  			loopCount:           1,
   783  			apiTransactionCount: []int{10},
   784  			generateReport:      true,
   785  		},
   786  		{
   787  			name:                "ThreeReports",
   788  			loopCount:           3,
   789  			apiTransactionCount: []int{5, 10, 2},
   790  			generateReport:      true,
   791  		},
   792  		{
   793  			name:                "ThreeReportsNoUsage",
   794  			loopCount:           3,
   795  			apiTransactionCount: []int{0, 0, 0},
   796  			generateReport:      true,
   797  		},
   798  		{
   799  			name:                "SixReports",
   800  			loopCount:           6,
   801  			apiTransactionCount: []int{5, 10, 2, 0, 3, 9},
   802  			generateReport:      true,
   803  		},
   804  	}
   805  
   806  	for testNum, test := range testCases {
   807  		t.Run(test.name, func(t *testing.T) {
   808  			startDate := time.Date(2021, 1, 31, 12, 30, 0, 0, time.Local)
   809  			setupMockClient(0)
   810  			testLoops := 0
   811  			now = func() time.Time {
   812  				next := startDate.Add(time.Hour * time.Duration(testLoops))
   813  				fmt.Println(next.Format(ISO8601))
   814  				return next
   815  			}
   816  
   817  			validateEvents := func(report UsageEvent) {
   818  				for j := 0; j < test.loopCount; j++ {
   819  					reportKey := startDate.Add(time.Duration(j-1) * time.Hour).Format(ISO8601)
   820  					assert.Equal(t, cmd.BuildDataPlaneType, report.Report[reportKey].Product)
   821  					assert.Equal(t, test.apiTransactionCount[j], int(report.Report[reportKey].Usage[cmd.BuildDataPlaneType+".Transactions"]))
   822  				}
   823  				// validate granularity when reports not empty
   824  				if test.loopCount != 0 {
   825  					assert.Equal(t, int(time.Hour.Milliseconds()), report.Granularity)
   826  					cfg.UsageReporting.(*config.UsageReportingConfiguration).URL = s.server.URL + "/lighthouse"
   827  					assert.Equal(t, cfg.UsageReporting.GetURL()+schemaPath, report.SchemaID)
   828  					assert.Equal(t, cfg.GetEnvironmentID(), report.EnvID)
   829  				}
   830  			}
   831  
   832  			myCollector := createMetricCollector()
   833  			metricCollector := myCollector.(*collector)
   834  
   835  			reportGenerator := metricCollector.reports
   836  			publisher := metricCollector.usagePublisher
   837  			for testLoops < test.loopCount {
   838  				for i := 0; i < test.apiTransactionCount[testLoops]; i++ {
   839  					metricCollector.AddMetric(apiDetails1, "200", 10, 10, "")
   840  				}
   841  				metricCollector.Execute()
   842  				testLoops++
   843  			}
   844  
   845  			// Get the usage reports from the cache and validate
   846  			events := myCollector.(*collector).reports.loadEvents()
   847  			validateEvents(events)
   848  
   849  			// generate the report file
   850  			publisher.Execute()
   851  
   852  			expectedFile := reportGenerator.generateReportPath(ISO8601Time(startDate), testNum-1)
   853  			if test.loopCount == 0 {
   854  				// no report expected, end the test here
   855  				expectedFile = reportGenerator.generateReportPath(ISO8601Time(startDate), 0)
   856  				assert.NoFileExists(t, expectedFile)
   857  				return
   858  			}
   859  
   860  			// validate the file exists and open it
   861  			assert.FileExists(t, expectedFile)
   862  			data, err := os.ReadFile(expectedFile)
   863  			assert.Nil(t, err)
   864  
   865  			// unmarshall it
   866  			var reportEvents UsageEvent
   867  			err = json.Unmarshal(data, &reportEvents)
   868  			assert.Nil(t, err)
   869  			assert.NotNil(t, reportEvents)
   870  
   871  			// validate event in generated reports
   872  			validateEvents(reportEvents)
   873  
   874  			s.resetOffline(myCollector)
   875  		})
   876  	}
   877  	cleanUpReportfiles()
   878  }