mirror of
https://github.com/TwiN/gatus.git
synced 2025-02-17 18:51:15 +01:00
Add GetUptimeByKey to store interface
This commit is contained in:
parent
968b960283
commit
0b6fc6b520
@ -6,9 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
|
||||||
"github.com/TwinProduction/gatus/storage"
|
"github.com/TwinProduction/gatus/storage"
|
||||||
"github.com/TwinProduction/gatus/storage/store/paging"
|
"github.com/TwinProduction/gatus/storage/store/common"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,22 +18,31 @@ import (
|
|||||||
func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
variables := mux.Vars(request)
|
variables := mux.Vars(request)
|
||||||
duration := variables["duration"]
|
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.WriteHeader(http.StatusBadRequest)
|
||||||
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
|
_, _ = writer.Write([]byte("Durations supported: 7d, 24h, 1h"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
identifier := variables["identifier"]
|
identifier := variables["identifier"]
|
||||||
key := strings.TrimSuffix(identifier, ".svg")
|
key := strings.TrimSuffix(identifier, ".svg")
|
||||||
serviceStatus := storage.Get().GetServiceStatusByKey(key, paging.NewServiceStatusParams().WithUptime())
|
uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now())
|
||||||
if serviceStatus == nil {
|
if err != nil {
|
||||||
|
if err == common.ErrServiceNotFound {
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
_, _ = writer.Write([]byte("Requested service not found"))
|
} else if err == common.ErrInvalidTimeRange {
|
||||||
return
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
}
|
} else {
|
||||||
if serviceStatus.Uptime == nil {
|
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = writer.Write([]byte("Failed to compute uptime"))
|
}
|
||||||
|
_, _ = writer.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
formattedDate := time.Now().Format(http.TimeFormat)
|
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("Date", formattedDate)
|
||||||
writer.Header().Set("Expires", formattedDate)
|
writer.Header().Set("Expires", formattedDate)
|
||||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
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 labelWidth, valueWidth, valueWidthAdjustment int
|
||||||
var value float64
|
|
||||||
switch duration {
|
switch duration {
|
||||||
case "7d":
|
case "7d":
|
||||||
labelWidth = 65
|
labelWidth = 65
|
||||||
value = uptime.LastSevenDays
|
|
||||||
case "24h":
|
case "24h":
|
||||||
labelWidth = 70
|
labelWidth = 70
|
||||||
value = uptime.LastTwentyFourHours
|
|
||||||
case "1h":
|
case "1h":
|
||||||
labelWidth = 65
|
labelWidth = 65
|
||||||
value = uptime.LastHour
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
color := getBadgeColorFromUptime(value)
|
color := getBadgeColorFromUptime(uptime)
|
||||||
sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", value*100), "0"), ".") + "%"
|
sanitizedValue := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", uptime*100), "0"), ".") + "%"
|
||||||
if strings.Contains(sanitizedValue, ".") {
|
if strings.Contains(sanitizedValue, ".") {
|
||||||
valueWidthAdjustment = -10
|
valueWidthAdjustment = -10
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/core"
|
|
||||||
"github.com/TwinProduction/gatus/security"
|
"github.com/TwinProduction/gatus/security"
|
||||||
"github.com/TwinProduction/gatus/storage"
|
"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/gocache"
|
||||||
"github.com/TwinProduction/health"
|
"github.com/TwinProduction/health"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -138,7 +138,7 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||||
vars := mux.Vars(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 {
|
if serviceStatus == nil {
|
||||||
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
|
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/storage/store/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -15,7 +15,7 @@ const (
|
|||||||
DefaultPageSize = 20
|
DefaultPageSize = 20
|
||||||
|
|
||||||
// MaximumPageSize is the maximum page size allowed
|
// MaximumPageSize is the maximum page size allowed
|
||||||
MaximumPageSize = core.MaximumNumberOfResults
|
MaximumPageSize = common.MaximumNumberOfResults
|
||||||
)
|
)
|
||||||
|
|
||||||
func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) {
|
func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) {
|
||||||
|
@ -1,13 +1,5 @@
|
|||||||
package core
|
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
|
// ServiceStatus contains the evaluation Results of a Service
|
||||||
type ServiceStatus struct {
|
type ServiceStatus struct {
|
||||||
// Name of the service
|
// 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.
|
// 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
|
// However, the detailed service page does leverage this by including it to a map that will be
|
||||||
// marshalled alongside the ServiceStatus.
|
// 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:"-"`
|
Uptime *Uptime `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
storage/store/common/errors.go
Normal file
8
storage/store/common/errors.go
Normal file
@ -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
|
||||||
|
)
|
9
storage/store/common/limits.go
Normal file
9
storage/store/common/limits.go
Normal file
@ -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
|
||||||
|
)
|
@ -6,7 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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/gatus/util"
|
||||||
"github.com/TwinProduction/gocache"
|
"github.com/TwinProduction/gocache"
|
||||||
)
|
)
|
||||||
@ -69,6 +70,35 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa
|
|||||||
return ShallowCopyServiceStatus(serviceStatus.(*core.ServiceStatus), params)
|
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
|
// Insert adds the observed result for the specified service into the store
|
||||||
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
||||||
key := service.Key()
|
key := service.Key()
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/storage/store/paging"
|
"github.com/TwinProduction/gatus/storage/store/common/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -2,7 +2,8 @@ package memory
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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
|
// 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
|
// Check if there's any change since the last result
|
||||||
if ss.Results[len(ss.Results)-1].Success != result.Success {
|
if ss.Results[len(ss.Results)-1].Success != result.Success {
|
||||||
ss.Events = append(ss.Events, core.NewEventFromResult(result))
|
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
|
// 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
|
// 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
|
// 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 {
|
} else {
|
||||||
@ -75,11 +76,11 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) {
|
|||||||
ss.Events = append(ss.Events, core.NewEventFromResult(result))
|
ss.Events = append(ss.Events, core.NewEventFromResult(result))
|
||||||
}
|
}
|
||||||
ss.Results = append(ss.Results, 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
|
// 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
|
// 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
|
// 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)
|
processUptimeAfterResult(ss.Uptime, result)
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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) {
|
func BenchmarkShallowCopyServiceStatus(b *testing.B) {
|
||||||
service := &testService
|
service := &testService
|
||||||
serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name)
|
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)
|
AddResult(serviceStatus, &testSuccessfulResult)
|
||||||
}
|
}
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
|
@ -5,20 +5,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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) {
|
func TestAddResult(t *testing.T) {
|
||||||
service := &core.Service{Name: "name", Group: "group"}
|
service := &core.Service{Name: "name", Group: "group"}
|
||||||
serviceStatus := core.NewServiceStatus(service.Key(), service.Group, service.Name)
|
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()})
|
AddResult(serviceStatus, &core.Result{Success: i%2 == 0, Timestamp: time.Now()})
|
||||||
}
|
}
|
||||||
if len(serviceStatus.Results) != core.MaximumNumberOfResults {
|
if len(serviceStatus.Results) != common.MaximumNumberOfResults {
|
||||||
t.Errorf("expected serviceStatus.Results to not exceed a length of %d", core.MaximumNumberOfResults)
|
t.Errorf("expected serviceStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
|
||||||
}
|
}
|
||||||
if len(serviceStatus.Events) != core.MaximumNumberOfEvents {
|
if len(serviceStatus.Events) != common.MaximumNumberOfEvents {
|
||||||
t.Errorf("expected serviceStatus.Events to not exceed a length of %d", core.MaximumNumberOfEvents)
|
t.Errorf("expected serviceStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
|
||||||
}
|
}
|
||||||
// Try to add nil serviceStatus
|
// Try to add nil serviceStatus
|
||||||
AddResult(nil, &core.Result{Timestamp: time.Now()})
|
AddResult(nil, &core.Result{Timestamp: time.Now()})
|
||||||
|
@ -9,7 +9,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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/gatus/util"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@ -25,8 +26,8 @@ const (
|
|||||||
arraySeparator = "|~|"
|
arraySeparator = "|~|"
|
||||||
|
|
||||||
uptimeCleanUpThreshold = 10 * 24 * time.Hour // Maximum uptime age before triggering a clean up
|
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
|
eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a clean up
|
||||||
resultsCleanUpThreshold = core.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up
|
resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results before triggering a clean up
|
||||||
|
|
||||||
uptimeRetention = 7 * 24 * time.Hour
|
uptimeRetention = 7 * 24 * time.Hour
|
||||||
)
|
)
|
||||||
@ -38,7 +39,6 @@ var (
|
|||||||
// ErrDatabaseDriverNotSpecified is the error returned when the driver parameter passed in NewStore is blank
|
// ErrDatabaseDriverNotSpecified is the error returned when the driver parameter passed in NewStore is blank
|
||||||
ErrDatabaseDriverNotSpecified = errors.New("database driver cannot be empty")
|
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")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -194,6 +194,31 @@ func (s *Store) GetServiceStatusByKey(key string, params *paging.ServiceStatusPa
|
|||||||
return serviceStatus
|
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
|
// Insert adds the observed result for the specified service into the store
|
||||||
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
@ -203,7 +228,7 @@ func (s *Store) Insert(service *core.Service, result *core.Result) {
|
|||||||
//start := time.Now()
|
//start := time.Now()
|
||||||
serviceID, err := s.getServiceID(tx, service)
|
serviceID, err := s.getServiceID(tx, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == errServiceNotFoundInDatabase {
|
if err == common.ErrServiceNotFound {
|
||||||
// Service doesn't exist in the database, insert it
|
// Service doesn't exist in the database, insert it
|
||||||
if serviceID, err = s.insertService(tx, service); err != nil {
|
if serviceID, err = s.insertService(tx, service); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
@ -503,7 +528,7 @@ func (s *Store) getServiceIDGroupAndNameByKey(tx *sql.Tx, key string) (id int64,
|
|||||||
}
|
}
|
||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return 0, "", "", errServiceNotFoundInDatabase
|
return 0, "", "", common.ErrServiceNotFound
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -642,7 +667,7 @@ func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) {
|
|||||||
}
|
}
|
||||||
_ = rows.Close()
|
_ = rows.Close()
|
||||||
if !found {
|
if !found {
|
||||||
return 0, errServiceNotFoundInDatabase
|
return 0, common.ErrServiceNotFound
|
||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
@ -735,7 +760,7 @@ func (s *Store) deleteOldServiceEvents(tx *sql.Tx, serviceID int64) error {
|
|||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
serviceID,
|
serviceID,
|
||||||
core.MaximumNumberOfEvents,
|
common.MaximumNumberOfEvents,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -760,7 +785,7 @@ func (s *Store) deleteOldServiceResults(tx *sql.Tx, serviceID int64) error {
|
|||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
serviceID,
|
serviceID,
|
||||||
core.MaximumNumberOfResults,
|
common.MaximumNumberOfResults,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -5,7 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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 (
|
var (
|
||||||
@ -157,7 +158,7 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
|
|||||||
for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ {
|
for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ {
|
||||||
store.Insert(&testService, &testSuccessfulResult)
|
store.Insert(&testService, &testSuccessfulResult)
|
||||||
store.Insert(&testService, &testUnsuccessfulResult)
|
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 {
|
if len(ss.Results) > resultsCleanUpThreshold+1 {
|
||||||
t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results))
|
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, _ := NewStore("sqlite", file)
|
||||||
store.Insert(&testService, &testSuccessfulResult)
|
store.Insert(&testService, &testSuccessfulResult)
|
||||||
store.Insert(&testService, &testUnsuccessfulResult)
|
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 {
|
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()
|
store.Close()
|
||||||
t.Fatal("sanity check failed")
|
t.Fatal("sanity check failed")
|
||||||
@ -181,7 +182,7 @@ func TestStore_Persistence(t *testing.T) {
|
|||||||
store.Close()
|
store.Close()
|
||||||
store, _ = NewStore("sqlite", file)
|
store, _ = NewStore("sqlite", file)
|
||||||
defer store.Close()
|
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 {
|
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")
|
t.Fatal("failed sanity check")
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,10 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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/memory"
|
||||||
"github.com/TwinProduction/gatus/storage/store/paging"
|
|
||||||
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store is the interface that each stores should implement
|
// 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 returns the service status for a given key
|
||||||
GetServiceStatusByKey(key string, params *paging.ServiceStatusParams) *core.ServiceStatus
|
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 adds the observed result for the specified service into the store
|
||||||
Insert(service *core.Service, result *core.Result)
|
Insert(service *core.Service, result *core.Result)
|
||||||
|
|
||||||
|
@ -5,8 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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/memory"
|
||||||
"github.com/TwinProduction/gatus/storage/store/paging"
|
|
||||||
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"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/memory"
|
||||||
"github.com/TwinProduction/gatus/storage/store/paging"
|
|
||||||
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
"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, &firstResult)
|
||||||
scenario.Store.Insert(&testService, &secondResult)
|
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 {
|
if serviceStatus == nil {
|
||||||
t.Fatalf("serviceStatus shouldn't have been nil")
|
t.Fatalf("serviceStatus shouldn't have been nil")
|
||||||
}
|
}
|
||||||
@ -165,15 +166,15 @@ func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
|
|||||||
for _, scenario := range scenarios {
|
for _, scenario := range scenarios {
|
||||||
t.Run(scenario.Name, func(t *testing.T) {
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
scenario.Store.Insert(&testService, &testSuccessfulResult)
|
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 {
|
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 = 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 {
|
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 = 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 {
|
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)
|
||||||
}
|
}
|
||||||
@ -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) {
|
func TestStore_Insert(t *testing.T) {
|
||||||
scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert")
|
scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert")
|
||||||
defer cleanUp(scenarios)
|
defer cleanUp(scenarios)
|
||||||
@ -285,7 +316,7 @@ func TestStore_Insert(t *testing.T) {
|
|||||||
scenario.Store.Insert(&testService, &testSuccessfulResult)
|
scenario.Store.Insert(&testService, &testSuccessfulResult)
|
||||||
scenario.Store.Insert(&testService, &testUnsuccessfulResult)
|
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 {
|
if ss == nil {
|
||||||
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user