From 329bd86e090929420644e9dd42b75309ee574eb6 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Fri, 8 Jan 2021 22:41:57 -0500 Subject: [PATCH] Replace GetAll by GetAllAsJSON and change storage package implementation --- storage/memory.go | 56 +---- storage/memory_test.go | 455 +++++++++-------------------------------- watchdog/watchdog.go | 6 +- 3 files changed, 108 insertions(+), 409 deletions(-) diff --git a/storage/memory.go b/storage/memory.go index 54d2bdd1..9f550a84 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -1,6 +1,7 @@ package storage import ( + "encoding/json" "fmt" "sync" @@ -22,19 +23,12 @@ func NewInMemoryStore() *InMemoryStore { } } -// GetAll returns all the observed results for all services from the in memory store -func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { - results := make(map[string]*core.ServiceStatus, len(ims.serviceStatuses)) +// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus +func (ims *InMemoryStore) GetAllAsJSON() ([]byte, error) { ims.serviceResultsMutex.RLock() - for key, serviceStatus := range ims.serviceStatuses { - results[key] = &core.ServiceStatus{ - Name: serviceStatus.Name, - Group: serviceStatus.Group, - Results: copyResults(serviceStatus.Results), - } - } + serviceStatuses, err := json.Marshal(ims.serviceStatuses) ims.serviceResultsMutex.RUnlock() - return results + return serviceStatuses, err } // GetServiceStatus returns the service status for a given service name in the given group @@ -59,46 +53,6 @@ func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) { ims.serviceResultsMutex.Unlock() } -func copyResults(results []*core.Result) []*core.Result { - var copiedResults []*core.Result - for _, result := range results { - copiedResults = append(copiedResults, &core.Result{ - HTTPStatus: result.HTTPStatus, - DNSRCode: result.DNSRCode, - Body: result.Body, - Hostname: result.Hostname, - IP: result.IP, - Connected: result.Connected, - Duration: result.Duration, - Errors: copyErrors(result.Errors), - ConditionResults: copyConditionResults(result.ConditionResults), - Success: result.Success, - Timestamp: result.Timestamp, - CertificateExpiration: result.CertificateExpiration, - }) - } - return copiedResults -} - -func copyConditionResults(conditionResults []*core.ConditionResult) []*core.ConditionResult { - var copiedConditionResults []*core.ConditionResult - for _, conditionResult := range conditionResults { - copiedConditionResults = append(copiedConditionResults, &core.ConditionResult{ - Condition: conditionResult.Condition, - Success: conditionResult.Success, - }) - } - return copiedConditionResults -} - -func copyErrors(errors []string) []string { - var copiedErrors []string - for _, err := range errors { - copiedErrors = append(copiedErrors, err) - } - return copiedErrors -} - // Clear will empty all the results from the in memory store func (ims *InMemoryStore) Clear() { ims.serviceResultsMutex.Lock() diff --git a/storage/memory_test.go b/storage/memory_test.go index 1159f914..4309df16 100644 --- a/storage/memory_test.go +++ b/storage/memory_test.go @@ -8,130 +8,98 @@ import ( "github.com/TwinProduction/gatus/core" ) -var testService = core.Service{ - Name: "Name", - Group: "Group", - URL: "URL", - DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"}, - Method: "Method", - Body: "Body", - GraphQL: false, - Headers: nil, - Interval: time.Second * 2, - Conditions: nil, - Alerts: nil, - Insecure: false, - NumberOfFailuresInARow: 0, - NumberOfSuccessesInARow: 0, -} +var ( + firstCondition = core.Condition("[STATUS] == 200") + secondCondition = core.Condition("[RESPONSE_TIME] < 500") + thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") -var memoryStore = NewInMemoryStore() + timestamp, _ = time.Parse(time.RFC3339, "2021-01-08T21:41:18-05:00") -func TestStorage_GetAllFromEmptyMemoryStoreReturnsNothing(t *testing.T) { - memoryStore.Clear() - results := memoryStore.GetAll() - if len(results) != 0 { - t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results)) + testService = core.Service{ + Name: "name", + Group: "group", + URL: "https://example.org/what/ever", + Method: "GET", + Body: "body", + Interval: 30 * time.Second, + Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition}, + Alerts: nil, + Insecure: false, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, } -} - -func TestStorage_InsertIntoEmptyMemoryStoreThenGetAllReturnsOneResult(t *testing.T) { - memoryStore.Clear() - result := core.Result{ + testSuccessfulResult = core.Result{ + Hostname: "example.org", + IP: "127.0.0.1", HTTPStatus: 200, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: false, - Duration: time.Second * 2, + Body: []byte("body"), Errors: nil, - ConditionResults: nil, - Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - - memoryStore.Insert(&testService, &result) - - results := memoryStore.GetAll() - if len(results) != 1 { - t.Errorf("MemoryStore should've returned 0 results, but actually returned %d", len(results)) - } - - key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - storedResult, exists := results[key] - if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) - } - - if storedResult.Name != testService.Name { - t.Errorf("Stored Results Name should've been %s, but was %s", testService.Name, storedResult.Name) - } - if storedResult.Group != testService.Group { - t.Errorf("Stored Results Group should've been %s, but was %s", testService.Group, storedResult.Group) - } - if len(storedResult.Results) != 1 { - t.Errorf("Stored Results for service %s should've had 1 result, but actually had %d", storedResult.Name, len(storedResult.Results)) - } - if storedResult.Results[0] == &result { - t.Errorf("Returned result is the same reference as result passed to insert. Returned result should be copies only") - } -} - -func TestStorage_InsertTwoResultsForSingleServiceIntoEmptyMemoryStore_ThenGetAllReturnsTwoResults(t *testing.T) { - memoryStore.Clear() - result1 := core.Result{ - HTTPStatus: 404, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: false, - Duration: time.Second * 2, - Errors: nil, - ConditionResults: nil, - Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - result2 := core.Result{ - HTTPStatus: 200, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", Connected: true, - Duration: time.Second * 2, - Errors: nil, - ConditionResults: nil, + Success: true, + Timestamp: timestamp, + Duration: 150 * time.Millisecond, + CertificateExpiration: 10 * time.Hour, + ConditionResults: []*core.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: true, + }, + { + Condition: "[CERTIFICATE_EXPIRATION] < 72h", + Success: true, + }, + }, + } + testUnsuccessfulResult = core.Result{ + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Body: []byte("body"), + Errors: []string{"error-1", "error-2"}, + Connected: true, Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, + Timestamp: timestamp, + Duration: 750 * time.Millisecond, + CertificateExpiration: 10 * time.Hour, + ConditionResults: []*core.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: false, + }, + { + Condition: "[CERTIFICATE_EXPIRATION] < 72h", + Success: false, + }, + }, } +) - resultsToInsert := []core.Result{result1, result2} +func TestInMemoryStore_Insert(t *testing.T) { + store := NewInMemoryStore() + store.Insert(&testService, &testSuccessfulResult) + store.Insert(&testService, &testUnsuccessfulResult) - memoryStore.Insert(&testService, &result1) - memoryStore.Insert(&testService, &result2) - - results := memoryStore.GetAll() - if len(results) != 1 { - t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) + if len(store.serviceStatuses) != 1 { + t.Fatalf("expected 1 ServiceStatus, got %d", len(store.serviceStatuses)) } - key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - serviceResults, exists := results[key] + serviceStatus, exists := store.serviceStatuses[key] if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) + t.Fatalf("Store should've had key '%s', but didn't", key) } - - if len(serviceResults.Results) != 2 { - t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) + if len(serviceStatus.Results) != 2 { + t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results)) } - - for i, r := range serviceResults.Results { - expectedResult := resultsToInsert[i] + for i, r := range serviceStatus.Results { + expectedResult := store.GetServiceStatus(testService.Group, testService.Name).Results[i] if r.HTTPStatus != expectedResult.HTTPStatus { t.Errorf("Result at index %d should've had a HTTPStatus of %d, but was actually %d", i, expectedResult.HTTPStatus, r.HTTPStatus) } @@ -171,265 +139,44 @@ func TestStorage_InsertTwoResultsForSingleServiceIntoEmptyMemoryStore_ThenGetAll } } -func TestStorage_InsertTwoResultsTwoServicesIntoEmptyMemoryStore_ThenGetAllReturnsTwoServicesWithOneResultEach(t *testing.T) { - memoryStore.Clear() - result1 := core.Result{ - HTTPStatus: 404, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: false, - Duration: time.Second * 2, - Errors: nil, - ConditionResults: nil, - Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - result2 := core.Result{ - HTTPStatus: 200, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: true, - Duration: time.Second * 2, - Errors: nil, - ConditionResults: nil, - Success: true, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - - testService2 := core.Service{ - Name: "Name2", - Group: "Group", - URL: "URL", - DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"}, - Method: "Method", - Body: "Body", - GraphQL: false, - Headers: nil, - Interval: time.Second * 2, - Conditions: nil, - Alerts: nil, - Insecure: false, - NumberOfFailuresInARow: 0, - NumberOfSuccessesInARow: 0, - } - - memoryStore.Insert(&testService, &result1) - memoryStore.Insert(&testService2, &result2) - - results := memoryStore.GetAll() - if len(results) != 2 { - t.Fatalf("MemoryStore should've returned 2 results, but actually returned %d", len(results)) - } - - key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - serviceResults1, exists := results[key] - if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) - } - - if len(serviceResults1.Results) != 1 { - t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results)) - } - - key = fmt.Sprintf("%s_%s", testService2.Group, testService2.Name) - serviceResults2, exists := results[key] - if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) - } - - if len(serviceResults2.Results) != 1 { - t.Fatalf("Service '%s' should've had 1 results, but actually returned %d", serviceResults1.Name, len(serviceResults1.Results)) - } -} - -func TestStorage_InsertResultForServiceWithErrorsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithErrors(t *testing.T) { - memoryStore.Clear() - errors := []string{ - "error1", - "error2", - } - result1 := core.Result{ - HTTPStatus: 404, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: false, - Duration: time.Second * 2, - Errors: errors, - ConditionResults: nil, - Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - - memoryStore.Insert(&testService, &result1) - - results := memoryStore.GetAll() - if len(results) != 1 { - t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) - } - - key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - serviceResults, exists := results[key] - if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) - } - - if len(serviceResults.Results) != 1 { - t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) - } - - actualResult := serviceResults.Results[0] - - if len(actualResult.Errors) != len(errors) { - t.Errorf("Service result should've had 2 errors, but actually had %d errors", len(actualResult.Errors)) - } - - for i, err := range actualResult.Errors { - if err != errors[i] { - t.Errorf("Error at index %d should've been %s, but was actually %s", i, errors[i], err) - } - } -} - -func TestStorage_InsertResultForServiceWithConditionResultsIntoEmptyMemoryStore_ThenGetAllReturnsOneResultWithConditionResults(t *testing.T) { - memoryStore.Clear() - crs := []*core.ConditionResult{ - { - Condition: "condition1", - Success: true, - }, - { - Condition: "condition2", - Success: false, - }, - } - result := core.Result{ - HTTPStatus: 404, - DNSRCode: "DNSRCode", - Body: nil, - Hostname: "Hostname", - IP: "IP", - Connected: false, - Duration: time.Second * 2, - Errors: nil, - ConditionResults: crs, - Success: false, - Timestamp: time.Now(), - CertificateExpiration: time.Second * 2, - } - - memoryStore.Insert(&testService, &result) - - results := memoryStore.GetAll() - if len(results) != 1 { - t.Fatalf("MemoryStore should've returned 1 results, but actually returned %d", len(results)) - } - - key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - serviceResults, exists := results[key] - if !exists { - t.Fatalf("In Memory Store should've contained key '%s', but didn't", key) - } - - if len(serviceResults.Results) != 1 { - t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results)) - } - - actualResult := serviceResults.Results[0] - - if len(actualResult.ConditionResults) != len(crs) { - t.Errorf("Service result should've had 2 ConditionResults, but actually had %d ConditionResults", len(actualResult.Errors)) - } - - for i, cr := range actualResult.ConditionResults { - if cr.Condition != crs[i].Condition { - t.Errorf("ConditionResult at index %d should've had condition %s, but was actually %s", i, crs[i].Condition, cr.Condition) - } - if cr.Success != crs[i].Success { - t.Errorf("ConditionResult at index %d should've had success value of %t, but was actually %t", i, crs[i].Success, cr.Success) - } - } -} - -func TestStorage_MultipleMemoryStoreInstancesReferToDifferentInternalMaps(t *testing.T) { - memoryStore.Clear() - currentMap := memoryStore.GetAll() - - otherMemoryStore := NewInMemoryStore() - otherMemoryStoresMap := otherMemoryStore.GetAll() - - if len(currentMap) != len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to the different internal maps, but 'memoryStore' returned %d results, and 'otherMemoryStore' returned %d results", len(currentMap), len(otherMemoryStoresMap)) - } - - memoryStore.Insert(&testService, &core.Result{}) - currentMap = memoryStore.GetAll() - otherMemoryStoresMap = otherMemoryStore.GetAll() - - if len(currentMap) == len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after inserting, and 'otherMemoryStore' returned %d results after inserting", len(currentMap), len(otherMemoryStoresMap)) - } - - otherMemoryStore.Clear() - currentMap = memoryStore.GetAll() - otherMemoryStoresMap = otherMemoryStore.GetAll() - - if len(currentMap) == len(otherMemoryStoresMap) { - t.Errorf("Multiple memory stores should refer to different internal maps, but 'memoryStore' returned %d results after clearing, and 'otherMemoryStore' returned %d results after clearing", len(currentMap), len(otherMemoryStoresMap)) - } -} - -func TestStorage_ModificationsToReturnedMapDoNotAffectInternalMap(t *testing.T) { - memoryStore.Clear() - - memoryStore.Insert(&testService, &core.Result{}) - modifiedResults := memoryStore.GetAll() - for k := range modifiedResults { - delete(modifiedResults, k) - } - results := memoryStore.GetAll() - - if len(modifiedResults) == len(results) { - t.Errorf("Returned map from GetAll should be free to modify by the caller without affecting internal in-memory map, but length of results from in-memory map (%d) was equal to the length of results in modified map (%d)", len(results), len(modifiedResults)) - } -} - -func TestStorage_GetServiceStatusForExistingStatusReturnsThatServiceStatus(t *testing.T) { - memoryStore.Clear() - - memoryStore.Insert(&testService, &core.Result{}) - serviceStatus := memoryStore.GetServiceStatus(testService.Group, testService.Name) +func TestInMemoryStore_GetServiceStatusForExistingStatusReturnsThatServiceStatus(t *testing.T) { + store := NewInMemoryStore() + store.Insert(&testService, &testSuccessfulResult) + serviceStatus := store.GetServiceStatus(testService.Group, testService.Name) if serviceStatus == nil { t.Errorf("Returned service status for group '%s' and name '%s' was nil after inserting the service into the store", testService.Group, testService.Name) } } -func TestStorage_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { - memoryStore.Clear() +func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { + store := NewInMemoryStore() + store.Insert(&testService, &testSuccessfulResult) - memoryStore.Insert(&testService, &core.Result{}) - - serviceStatus := memoryStore.GetServiceStatus("nonexistantgroup", "nonexistantname") + serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname") if serviceStatus != nil { t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) } - - serviceStatus = memoryStore.GetServiceStatus(testService.Group, "nonexistantname") + serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname") if serviceStatus != nil { t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") } - - serviceStatus = memoryStore.GetServiceStatus("nonexistantgroup", testService.Name) + serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name) if serviceStatus != nil { t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) } } + +func TestInMemoryStore_GetAllAsJSON(t *testing.T) { + store := NewInMemoryStore() + store.Insert(&testService, &testSuccessfulResult) + store.Insert(&testService, &testUnsuccessfulResult) + output, err := store.GetAllAsJSON() + if err != nil { + t.Fatal("shouldn't have returned an error, got", err.Error()) + } + expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"condition-results":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"2021-01-08T21:41:18-05:00"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"condition-results":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"2021-01-08T21:41:18-05:00"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}` + if string(output) != expectedOutput { + t.Error() + } +} diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index b307b140..1c34e4f8 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -1,7 +1,6 @@ package watchdog import ( - "encoding/json" "fmt" "log" "sync" @@ -21,10 +20,9 @@ var ( monitoringMutex sync.Mutex ) -// GetServiceStatusesAsJSON returns a list of core.ServiceStatus for each services encoded using json.Marshal. +// GetServiceStatusesAsJSON the JSON encoding of all core.ServiceStatus recorded func GetServiceStatusesAsJSON() ([]byte, error) { - serviceStatuses := store.GetAll() - return json.Marshal(serviceStatuses) + return store.GetAllAsJSON() } // GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name