From 0b6fc6b52088c9110c3c668e625d261a6782944b Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Thu, 12 Aug 2021 21:54:23 -0400 Subject: [PATCH] Add GetUptimeByKey to store interface --- controller/badge.go | 44 +++++++++-------- controller/controller.go | 6 +-- controller/util.go | 4 +- core/service_status.go | 12 ++--- storage/store/common/errors.go | 8 ++++ storage/store/common/limits.go | 9 ++++ storage/store/{ => common}/paging/paging.go | 0 .../store/{ => common}/paging/paging_test.go | 0 storage/store/memory/memory.go | 32 ++++++++++++- storage/store/memory/memory_test.go | 2 +- storage/store/memory/util.go | 11 +++-- storage/store/memory/util_bench_test.go | 5 +- storage/store/memory/util_test.go | 13 ++--- storage/store/sqlite/sqlite.go | 47 ++++++++++++++----- storage/store/sqlite/sqlite_test.go | 9 ++-- storage/store/store.go | 6 ++- storage/store/store_bench_test.go | 2 +- storage/store/store_test.go | 43 ++++++++++++++--- 18 files changed, 182 insertions(+), 71 deletions(-) create mode 100644 storage/store/common/errors.go create mode 100644 storage/store/common/limits.go rename storage/store/{ => common}/paging/paging.go (100%) rename storage/store/{ => common}/paging/paging_test.go (100%) diff --git a/controller/badge.go b/controller/badge.go index db9c91ae..b9b7baca 100644 --- a/controller/badge.go +++ b/controller/badge.go @@ -6,9 +6,8 @@ import ( "strings" "time" - "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/storage" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" "github.com/gorilla/mux" ) @@ -19,22 +18,31 @@ import ( func badgeHandler(writer http.ResponseWriter, request *http.Request) { variables := mux.Vars(request) duration := variables["duration"] - if duration != "7d" && duration != "24h" && duration != "1h" { + var from time.Time + switch duration { + case "7d": + from = time.Now().Add(-time.Hour * 24 * 7) + case "24h": + from = time.Now().Add(-time.Hour * 24) + case "1h": + from = time.Now().Add(-time.Hour) + default: writer.WriteHeader(http.StatusBadRequest) _, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h")) return } identifier := variables["identifier"] key := strings.TrimSuffix(identifier, ".svg") - serviceStatus := storage.Get().GetServiceStatusByKey(key, paging.NewServiceStatusParams().WithUptime()) - if serviceStatus == nil { - writer.WriteHeader(http.StatusNotFound) - _, _ = writer.Write([]byte("Requested service not found")) - return - } - if serviceStatus.Uptime == nil { - writer.WriteHeader(http.StatusInternalServerError) - _, _ = writer.Write([]byte("Failed to compute uptime")) + uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now()) + if err != nil { + if err == common.ErrServiceNotFound { + writer.WriteHeader(http.StatusNotFound) + } else if err == common.ErrInvalidTimeRange { + writer.WriteHeader(http.StatusBadRequest) + } else { + writer.WriteHeader(http.StatusInternalServerError) + } + _, _ = writer.Write([]byte(err.Error())) return } formattedDate := time.Now().Format(http.TimeFormat) @@ -42,26 +50,22 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Date", formattedDate) writer.Header().Set("Expires", formattedDate) writer.Header().Set("Content-Type", "image/svg+xml") - _, _ = writer.Write(generateSVG(duration, serviceStatus.Uptime)) + _, _ = writer.Write(generateSVG(duration, uptime)) } -func generateSVG(duration string, uptime *core.Uptime) []byte { +func generateSVG(duration string, uptime float64) []byte { var labelWidth, valueWidth, valueWidthAdjustment int - var value float64 switch duration { case "7d": labelWidth = 65 - value = uptime.LastSevenDays case "24h": labelWidth = 70 - value = uptime.LastTwentyFourHours case "1h": labelWidth = 65 - value = uptime.LastHour default: } - color := getBadgeColorFromUptime(value) - sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", value*100), "0"), ".") + "%" + color := getBadgeColorFromUptime(uptime) + sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", uptime*100), "0"), ".") + "%" if strings.Contains(sanitizedValue, ".") { valueWidthAdjustment = -10 } diff --git a/controller/controller.go b/controller/controller.go index caaa6b77..8d42f6aa 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -13,10 +13,10 @@ import ( "time" "github.com/TwinProduction/gatus/config" - "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/security" "github.com/TwinProduction/gatus/storage" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gocache" "github.com/TwinProduction/health" "github.com/gorilla/mux" @@ -138,7 +138,7 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) { func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { page, pageSize := extractPageAndPageSizeFromRequest(r) vars := mux.Vars(r) - serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"], paging.NewServiceStatusParams().WithResults(page, pageSize).WithEvents(1, core.MaximumNumberOfEvents).WithUptime()) + serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"], paging.NewServiceStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents).WithUptime()) if serviceStatus == nil { log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"]) writer.WriteHeader(http.StatusNotFound) diff --git a/controller/util.go b/controller/util.go index 585da6da..b15571fc 100644 --- a/controller/util.go +++ b/controller/util.go @@ -4,7 +4,7 @@ import ( "net/http" "strconv" - "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/common" ) const ( @@ -15,7 +15,7 @@ const ( DefaultPageSize = 20 // MaximumPageSize is the maximum page size allowed - MaximumPageSize = core.MaximumNumberOfResults + MaximumPageSize = common.MaximumNumberOfResults ) func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) { diff --git a/core/service_status.go b/core/service_status.go index 99a358db..30ff01a5 100644 --- a/core/service_status.go +++ b/core/service_status.go @@ -1,13 +1,5 @@ package core -const ( - // MaximumNumberOfResults is the maximum number of results that ServiceStatus.Results can have - MaximumNumberOfResults = 100 - - // MaximumNumberOfEvents is the maximum number of events that ServiceStatus.Events can have - MaximumNumberOfEvents = 50 -) - // ServiceStatus contains the evaluation Results of a Service type ServiceStatus struct { // Name of the service @@ -34,6 +26,10 @@ type ServiceStatus struct { // We don't expose this through JSON, because the main dashboard doesn't need to have this data. // However, the detailed service page does leverage this by including it to a map that will be // marshalled alongside the ServiceStatus. + // + // TODO: Get rid of this in favor of using the new store.GetUptimeByKey. + // TODO: For memory, store the uptime in a different map? (is that possible, given that we need to persist it through gocache?) + // Deprecated Uptime *Uptime `json:"-"` } diff --git a/storage/store/common/errors.go b/storage/store/common/errors.go new file mode 100644 index 00000000..b186825e --- /dev/null +++ b/storage/store/common/errors.go @@ -0,0 +1,8 @@ +package common + +import "errors" + +var ( + ErrServiceNotFound = errors.New("service not found") // When a service does not exist in the store + ErrInvalidTimeRange = errors.New("'from' cannot be older than 'to'") // When an invalid time range is provided +) diff --git a/storage/store/common/limits.go b/storage/store/common/limits.go new file mode 100644 index 00000000..b7fc449d --- /dev/null +++ b/storage/store/common/limits.go @@ -0,0 +1,9 @@ +package common + +const ( + // MaximumNumberOfResults is the maximum number of results that a service can have + MaximumNumberOfResults = 100 + + // MaximumNumberOfEvents is the maximum number of events that a service can have + MaximumNumberOfEvents = 50 +) diff --git a/storage/store/paging/paging.go b/storage/store/common/paging/paging.go similarity index 100% rename from storage/store/paging/paging.go rename to storage/store/common/paging/paging.go diff --git a/storage/store/paging/paging_test.go b/storage/store/common/paging/paging_test.go similarity index 100% rename from storage/store/paging/paging_test.go rename to storage/store/common/paging/paging_test.go diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go index fd6d1c1e..a073d78e 100644 --- a/storage/store/memory/memory.go +++ b/storage/store/memory/memory.go @@ -6,7 +6,8 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gatus/util" "github.com/TwinProduction/gocache" ) @@ -69,6 +70,35 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa return ShallowCopyServiceStatus(serviceStatus.(*core.ServiceStatus), params) } +// GetUptimeByKey returns the uptime percentage during a time range +func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error) { + if from.After(to) { + return 0, common.ErrInvalidTimeRange + } + serviceStatus := s.cache.GetValue(key) + if serviceStatus == nil || serviceStatus.(*core.ServiceStatus).Uptime == nil { + return 0, common.ErrServiceNotFound + } + successfulExecutions := uint64(0) + totalExecutions := uint64(0) + current := from + for to.Sub(current) >= 0 { + hourlyUnixTimestamp := current.Truncate(time.Hour).Unix() + hourlyStats := serviceStatus.(*core.ServiceStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp] + if hourlyStats == nil || hourlyStats.TotalExecutions == 0 { + current = current.Add(time.Hour) + continue + } + successfulExecutions += hourlyStats.SuccessfulExecutions + totalExecutions += hourlyStats.TotalExecutions + current = current.Add(time.Hour) + } + if totalExecutions == 0 { + return 0, nil + } + return float64(successfulExecutions) / float64(totalExecutions), nil +} + // Insert adds the observed result for the specified service into the store func (s *Store) Insert(service *core.Service, result *core.Result) { key := service.Key() diff --git a/storage/store/memory/memory_test.go b/storage/store/memory/memory_test.go index 26bea9eb..acde0a3c 100644 --- a/storage/store/memory/memory_test.go +++ b/storage/store/memory/memory_test.go @@ -5,7 +5,7 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common/paging" ) var ( diff --git a/storage/store/memory/util.go b/storage/store/memory/util.go index 0f707789..bb4021ff 100644 --- a/storage/store/memory/util.go +++ b/storage/store/memory/util.go @@ -2,7 +2,8 @@ package memory import ( "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" ) // ShallowCopyServiceStatus returns a shallow copy of a ServiceStatus with only the results @@ -63,11 +64,11 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) { // Check if there's any change since the last result if ss.Results[len(ss.Results)-1].Success != result.Success { ss.Events = append(ss.Events, core.NewEventFromResult(result)) - if len(ss.Events) > core.MaximumNumberOfEvents { + if len(ss.Events) > common.MaximumNumberOfEvents { // Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has // more than one extra element, we can get rid of all of them at once and thus returning the slice to a // length of MaximumNumberOfEvents by using ss.Events[len(ss.Events)-MaximumNumberOfEvents:] instead - ss.Events = ss.Events[len(ss.Events)-core.MaximumNumberOfEvents:] + ss.Events = ss.Events[len(ss.Events)-common.MaximumNumberOfEvents:] } } } else { @@ -75,11 +76,11 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) { ss.Events = append(ss.Events, core.NewEventFromResult(result)) } ss.Results = append(ss.Results, result) - if len(ss.Results) > core.MaximumNumberOfResults { + if len(ss.Results) > common.MaximumNumberOfResults { // Doing ss.Results[1:] would usually be sufficient, but in the case where for some reason, the slice has more // than one extra element, we can get rid of all of them at once and thus returning the slice to a length of // MaximumNumberOfResults by using ss.Results[len(ss.Results)-MaximumNumberOfResults:] instead - ss.Results = ss.Results[len(ss.Results)-core.MaximumNumberOfResults:] + ss.Results = ss.Results[len(ss.Results)-common.MaximumNumberOfResults:] } processUptimeAfterResult(ss.Uptime, result) } diff --git a/storage/store/memory/util_bench_test.go b/storage/store/memory/util_bench_test.go index b3a7413d..e5a85f99 100644 --- a/storage/store/memory/util_bench_test.go +++ b/storage/store/memory/util_bench_test.go @@ -4,13 +4,14 @@ import ( "testing" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" ) func BenchmarkShallowCopyServiceStatus(b *testing.B) { service := &testService serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name) - for i := 0; i < core.MaximumNumberOfResults; i++ { + for i := 0; i < common.MaximumNumberOfResults; i++ { AddResult(serviceStatus, &testSuccessfulResult) } for n := 0; n < b.N; n++ { diff --git a/storage/store/memory/util_test.go b/storage/store/memory/util_test.go index 480e22d3..048eb3a4 100644 --- a/storage/store/memory/util_test.go +++ b/storage/store/memory/util_test.go @@ -5,20 +5,21 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" ) func TestAddResult(t *testing.T) { service := &core.Service{Name: "name", Group: "group"} serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name) - for i := 0; i < (core.MaximumNumberOfResults+core.MaximumNumberOfEvents)*2; i++ { + for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ { AddResult(serviceStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()}) } - if len(serviceStatus.Results) != core.MaximumNumberOfResults { - t.Errorf("expected serviceStatus.Results to not exceed a length of %d", core.MaximumNumberOfResults) + if len(serviceStatus.Results) != common.MaximumNumberOfResults { + t.Errorf("expected serviceStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults) } - if len(serviceStatus.Events) != core.MaximumNumberOfEvents { - t.Errorf("expected serviceStatus.Events to not exceed a length of %d", core.MaximumNumberOfEvents) + if len(serviceStatus.Events) != common.MaximumNumberOfEvents { + t.Errorf("expected serviceStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents) } // Try to add nil serviceStatus AddResult(nil, &core.Result{Timestamp: time.Now()}) diff --git a/storage/store/sqlite/sqlite.go b/storage/store/sqlite/sqlite.go index a93302de..724636e8 100644 --- a/storage/store/sqlite/sqlite.go +++ b/storage/store/sqlite/sqlite.go @@ -9,7 +9,8 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gatus/util" _ "modernc.org/sqlite" ) @@ -24,9 +25,9 @@ const ( // for aesthetic purposes, I deemed it wasn't worth the performance impact of yet another one-to-many table. arraySeparator = "|~|" - uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a clean up - eventsCleanUpThreshold = core.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a clean up - resultsCleanUpThreshold = core.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up + uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a clean up + eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a clean up + resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up uptimeRetention = 7 * 24 * time.Hour ) @@ -38,8 +39,7 @@ var ( // ErrDatabaseDriverNotSpecified is the error returned when the driver parameter passed in NewStore is blank ErrDatabaseDriverNotSpecified = errors.New("database driver cannot be empty") - errServiceNotFoundInDatabase = errors.New("service does not exist in database") - errNoRowsReturned = errors.New("expected a row to be returned, but none was") + errNoRowsReturned = errors.New("expected a row to be returned, but none was") ) // Store that leverages a database @@ -194,6 +194,31 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa return serviceStatus } +// GetUptimeByKey returns the uptime percentage during a time range +func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error) { + if from.After(to) { + return 0, common.ErrInvalidTimeRange + } + tx, err := s.db.Begin() + if err != nil { + return 0, err + } + serviceID, _, _, err := s.getServiceIDGroupAndNameByKey(tx, key) + if err != nil { + _ = tx.Rollback() + return 0, err + } + uptime, _, err := s.getServiceUptime(tx, serviceID, from, to) + if err != nil { + _ = tx.Rollback() + return 0, err + } + if err = tx.Commit(); err != nil { + _ = tx.Rollback() + } + return uptime, nil +} + // Insert adds the observed result for the specified service into the store func (s *Store) Insert(service *core.Service, result *core.Result) { tx, err := s.db.Begin() @@ -203,7 +228,7 @@ func (s *Store) Insert(service *core.Service, result *core.Result) { //start := time.Now() serviceID, err := s.getServiceID(tx, service) if err != nil { - if err == errServiceNotFoundInDatabase { + if err == common.ErrServiceNotFound { // Service doesn't exist in the database, insert it if serviceID, err = s.insertService(tx, service); err != nil { _ = tx.Rollback() @@ -503,7 +528,7 @@ func (s *Store) getServiceIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64, } _ = rows.Close() if id == 0 { - return 0, "", "", errServiceNotFoundInDatabase + return 0, "", "", common.ErrServiceNotFound } return } @@ -642,7 +667,7 @@ func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) { } _ = rows.Close() if !found { - return 0, errServiceNotFoundInDatabase + return 0, common.ErrServiceNotFound } return id, nil } @@ -735,7 +760,7 @@ func (s *Store) deleteOldServiceEvents(tx *sql.Tx, serviceID int64) error { ) `, serviceID, - core.MaximumNumberOfEvents, + common.MaximumNumberOfEvents, ) if err != nil { return err @@ -760,7 +785,7 @@ func (s *Store) deleteOldServiceResults(tx *sql.Tx, serviceID int64) error { ) `, serviceID, - core.MaximumNumberOfResults, + common.MaximumNumberOfResults, ) if err != nil { return err diff --git a/storage/store/sqlite/sqlite_test.go b/storage/store/sqlite/sqlite_test.go index e139a067..2632528c 100644 --- a/storage/store/sqlite/sqlite_test.go +++ b/storage/store/sqlite/sqlite_test.go @@ -5,7 +5,8 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/storage/store/paging" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" ) var ( @@ -157,7 +158,7 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ { store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) - ss := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults*5).WithEvents(1, core.MaximumNumberOfEvents*5)) + ss := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults*5).WithEvents(1, common.MaximumNumberOfEvents*5)) if len(ss.Results) > resultsCleanUpThreshold+1 { t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results)) } @@ -173,7 +174,7 @@ func TestStore_Persistence(t *testing.T) { store, _ := NewStore("sqlite", file) store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) - ssFromOldStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults).WithEvents(1, core.MaximumNumberOfEvents).WithUptime()) + ssFromOldStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents).WithUptime()) if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 || ssFromOldStore.Uptime.LastHour != 0.5 || ssFromOldStore.Uptime.LastTwentyFourHours != 0.5 || ssFromOldStore.Uptime.LastSevenDays != 0.5 { store.Close() t.Fatal("sanity check failed") @@ -181,7 +182,7 @@ func TestStore_Persistence(t *testing.T) { store.Close() store, _ = NewStore("sqlite", file) defer store.Close() - ssFromNewStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, core.MaximumNumberOfResults).WithEvents(1, core.MaximumNumberOfEvents).WithUptime()) + ssFromNewStore := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents).WithUptime()) if ssFromNewStore == nil || ssFromNewStore.Group != "group" || ssFromNewStore.Name != "name" || len(ssFromNewStore.Events) != 3 || len(ssFromNewStore.Results) != 2 || ssFromNewStore.Uptime.LastHour != 0.5 || ssFromNewStore.Uptime.LastTwentyFourHours != 0.5 || ssFromNewStore.Uptime.LastSevenDays != 0.5 { t.Fatal("failed sanity check") } diff --git a/storage/store/store.go b/storage/store/store.go index 4dfcfc2b..9e371364 100644 --- a/storage/store/store.go +++ b/storage/store/store.go @@ -2,9 +2,10 @@ package store import ( "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gatus/storage/store/memory" - "github.com/TwinProduction/gatus/storage/store/paging" "github.com/TwinProduction/gatus/storage/store/sqlite" + "time" ) // Store is the interface that each stores should implement @@ -19,6 +20,9 @@ type Store interface { // GetServiceStatusByKey returns the service status for a given key GetServiceStatusByKey(key string, params *paging.ServiceStatusParams) *core.ServiceStatus + // GetUptimeByKey returns the uptime percentage during a time range + GetUptimeByKey(key string, from, to time.Time) (float64, error) + // Insert adds the observed result for the specified service into the store Insert(service *core.Service, result *core.Result) diff --git a/storage/store/store_bench_test.go b/storage/store/store_bench_test.go index 5a6222fb..df1267d3 100644 --- a/storage/store/store_bench_test.go +++ b/storage/store/store_bench_test.go @@ -5,8 +5,8 @@ import ( "time" "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gatus/storage/store/memory" - "github.com/TwinProduction/gatus/storage/store/paging" "github.com/TwinProduction/gatus/storage/store/sqlite" ) diff --git a/storage/store/store_test.go b/storage/store/store_test.go index adfa206c..0e68964b 100644 --- a/storage/store/store_test.go +++ b/storage/store/store_test.go @@ -5,8 +5,9 @@ import ( "time" "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/common" + "github.com/TwinProduction/gatus/storage/store/common/paging" "github.com/TwinProduction/gatus/storage/store/memory" - "github.com/TwinProduction/gatus/storage/store/paging" "github.com/TwinProduction/gatus/storage/store/sqlite" ) @@ -126,7 +127,7 @@ func TestStore_GetServiceStatusByKey(t *testing.T) { scenario.Store.Insert(&testService, &firstResult) scenario.Store.Insert(&testService, &secondResult) - serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults).WithUptime()) if serviceStatus == nil { t.Fatalf("serviceStatus shouldn't have been nil") } @@ -165,15 +166,15 @@ func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { scenario.Store.Insert(&testService, &testSuccessfulResult) - serviceStatus := scenario.Store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + serviceStatus := scenario.Store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults).WithUptime()) 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 = scenario.Store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + serviceStatus = scenario.Store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults).WithUptime()) 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 = scenario.Store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + serviceStatus = scenario.Store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults).WithUptime()) 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) } @@ -273,6 +274,36 @@ func TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2(t *testing.T } } +func TestStore_GetUptimeByKey(t *testing.T) { + scenarios := initStoresAndBaseScenarios(t, "TestStore_GetUptimeByKey") + defer cleanUp(scenarios) + firstResult := testSuccessfulResult + firstResult.Timestamp = now.Add(-time.Minute) + secondResult := testUnsuccessfulResult + secondResult.Timestamp = now + for _, scenario := range scenarios { + t.Run(scenario.Name, func(t *testing.T) { + if _, err := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour), time.Now()); err != common.ErrServiceNotFound { + t.Errorf("should've returned not found because there's nothing yet, got %v", err) + } + scenario.Store.Insert(&testService, &firstResult) + scenario.Store.Insert(&testService, &secondResult) + if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 { + t.Errorf("the uptime over the past 1h should've been 0.5, got %f", uptime) + } + if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24), time.Now()); uptime != 0.5 { + t.Errorf("the uptime over the past 24h should've been 0.5, got %f", uptime) + } + if uptime, _ := scenario.Store.GetUptimeByKey(testService.Key(), time.Now().Add(-time.Hour*24*7), time.Now()); uptime != 0.5 { + t.Errorf("the uptime over the past 7d should've been 0.5, got %f", uptime) + } + if _, err := scenario.Store.GetUptimeByKey(testService.Key(), time.Now(), time.Now().Add(-time.Hour)); err == nil { + t.Error("should've returned an error because the parameter 'from' cannot be older than 'to'") + } + }) + } +} + func TestStore_Insert(t *testing.T) { scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert") defer cleanUp(scenarios) @@ -285,7 +316,7 @@ func TestStore_Insert(t *testing.T) { scenario.Store.Insert(&testService, &testSuccessfulResult) scenario.Store.Insert(&testService, &testUnsuccessfulResult) - ss := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) + ss := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults).WithUptime()) if ss == nil { t.Fatalf("Store should've had key '%s', but didn't", testService.Key()) }