Replace GetAll by GetAllAsJSON and change storage package implementation

This commit is contained in:
TwinProduction 2021-01-08 22:41:57 -05:00
parent 19bb831fbf
commit 329bd86e09
3 changed files with 108 additions and 409 deletions

View File

@ -1,6 +1,7 @@
package storage package storage
import ( import (
"encoding/json"
"fmt" "fmt"
"sync" "sync"
@ -22,19 +23,12 @@ func NewInMemoryStore() *InMemoryStore {
} }
} }
// GetAll returns all the observed results for all services from the in memory store // GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
func (ims *InMemoryStore) GetAll() map[string]*core.ServiceStatus { func (ims *InMemoryStore) GetAllAsJSON() ([]byte, error) {
results := make(map[string]*core.ServiceStatus, len(ims.serviceStatuses))
ims.serviceResultsMutex.RLock() ims.serviceResultsMutex.RLock()
for key, serviceStatus := range ims.serviceStatuses { serviceStatuses, err := json.Marshal(ims.serviceStatuses)
results[key] = &core.ServiceStatus{
Name: serviceStatus.Name,
Group: serviceStatus.Group,
Results: copyResults(serviceStatus.Results),
}
}
ims.serviceResultsMutex.RUnlock() ims.serviceResultsMutex.RUnlock()
return results return serviceStatuses, err
} }
// GetServiceStatus returns the service status for a given service name in the given group // 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() 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 // Clear will empty all the results from the in memory store
func (ims *InMemoryStore) Clear() { func (ims *InMemoryStore) Clear() {
ims.serviceResultsMutex.Lock() ims.serviceResultsMutex.Lock()

View File

@ -8,130 +8,98 @@ import (
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
) )
var testService = core.Service{ var (
Name: "Name", firstCondition = core.Condition("[STATUS] == 200")
Group: "Group", secondCondition = core.Condition("[RESPONSE_TIME] < 500")
URL: "URL", thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
DNS: &core.DNS{QueryType: "QueryType", QueryName: "QueryName"},
Method: "Method", timestamp, _ = time.Parse(time.RFC3339, "2021-01-08T21:41:18-05:00")
Body: "Body",
GraphQL: false, testService = core.Service{
Headers: nil, Name: "name",
Interval: time.Second * 2, Group: "group",
Conditions: nil, URL: "https://example.org/what/ever",
Method: "GET",
Body: "body",
Interval: 30 * time.Second,
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
Alerts: nil, Alerts: nil,
Insecure: false, Insecure: false,
NumberOfFailuresInARow: 0, NumberOfFailuresInARow: 0,
NumberOfSuccessesInARow: 0, NumberOfSuccessesInARow: 0,
}
var memoryStore = NewInMemoryStore()
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))
} }
} testSuccessfulResult = core.Result{
Hostname: "example.org",
func TestStorage_InsertIntoEmptyMemoryStoreThenGetAllReturnsOneResult(t *testing.T) { IP: "127.0.0.1",
memoryStore.Clear()
result := core.Result{
HTTPStatus: 200, HTTPStatus: 200,
DNSRCode: "DNSRCode", Body: []byte("body"),
Body: nil,
Hostname: "Hostname",
IP: "IP",
Connected: false,
Duration: time.Second * 2,
Errors: nil, 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, Connected: true,
Duration: time.Second * 2, Success: true,
Errors: nil, Timestamp: timestamp,
ConditionResults: nil, 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, Success: false,
Timestamp: time.Now(), Timestamp: timestamp,
CertificateExpiration: time.Second * 2, 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) if len(store.serviceStatuses) != 1 {
memoryStore.Insert(&testService, &result2) t.Fatalf("expected 1 ServiceStatus, got %d", len(store.serviceStatuses))
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) key := fmt.Sprintf("%s_%s", testService.Group, testService.Name)
serviceResults, exists := results[key] serviceStatus, exists := store.serviceStatuses[key]
if !exists { 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(serviceStatus.Results) != 2 {
if len(serviceResults.Results) != 2 { t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results))
t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceResults.Name, len(serviceResults.Results))
} }
for i, r := range serviceStatus.Results {
for i, r := range serviceResults.Results { expectedResult := store.GetServiceStatus(testService.Group, testService.Name).Results[i]
expectedResult := resultsToInsert[i]
if r.HTTPStatus != expectedResult.HTTPStatus { 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) 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) { func TestInMemoryStore_GetServiceStatusForExistingStatusReturnsThatServiceStatus(t *testing.T) {
memoryStore.Clear() store := NewInMemoryStore()
result1 := core.Result{ store.Insert(&testService, &testSuccessfulResult)
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)
serviceStatus := store.GetServiceStatus(testService.Group, testService.Name)
if serviceStatus == nil { 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) 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) { func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
memoryStore.Clear() store := NewInMemoryStore()
store.Insert(&testService, &testSuccessfulResult)
memoryStore.Insert(&testService, &core.Result{}) serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname")
serviceStatus := memoryStore.GetServiceStatus("nonexistantgroup", "nonexistantname")
if serviceStatus != nil { 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) 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 = store.GetServiceStatus(testService.Group, "nonexistantname")
serviceStatus = memoryStore.GetServiceStatus(testService.Group, "nonexistantname")
if serviceStatus != nil { 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") t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname")
} }
serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name)
serviceStatus = memoryStore.GetServiceStatus("nonexistantgroup", testService.Name)
if serviceStatus != nil { 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) 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()
}
}

View File

@ -1,7 +1,6 @@
package watchdog package watchdog
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"sync" "sync"
@ -21,10 +20,9 @@ var (
monitoringMutex sync.Mutex 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) { func GetServiceStatusesAsJSON() ([]byte, error) {
serviceStatuses := store.GetAll() return store.GetAllAsJSON()
return json.Marshal(serviceStatuses)
} }
// GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name // GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name