1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-16 14:58:40 +00:00

feat: Make maximum number of results and events configurable (#1110)

This commit is contained in:
TwiN
2025-05-17 16:10:28 -04:00
committed by GitHub
parent c411b001eb
commit 04692d15ba
29 changed files with 231 additions and 161 deletions

View File

@@ -383,11 +383,13 @@ Here are some examples of conditions you can use:
### Storage
| Parameter | Description | Default |
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|
|:------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|
| `storage` | Storage configuration | `{}` |
| `storage.path` | Path to persist the data in. Only supported for types `sqlite` and `postgres`. | `""` |
| `storage.type` | Type of storage. Valid types: `memory`, `sqlite`, `postgres`. | `"memory"` |
| `storage.caching` | Whether to use write-through caching. Improves loading time for large dashboards. <br />Only supported if `storage.type` is `sqlite` or `postgres` | `false` |
| `storage.maximum-number-of-results` | The maximum number of results that an endpoint can have | `100` |
| `storage.maximum-number-of-events` | The maximum number of events that an endpoint can have | `50` |
The results for each endpoint health check as well as the data for uptime and the past events must be persisted
so that they can be displayed on the dashboard. These parameters allow you to configure the storage in question.
@@ -398,6 +400,8 @@ so that they can be displayed on the dashboard. These parameters allow you to co
# Because the data is stored in memory, the data will not survive a restart.
storage:
type: memory
maximum-number-of-results: 200
maximum-number-of-events: 5
```
- If `storage.type` is `sqlite`, `storage.path` must not be blank:
```yaml

View File

@@ -126,6 +126,6 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
}
}
protectedAPIRouter.Get("/v1/endpoints/statuses", EndpointStatuses(cfg))
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus)
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
return app
}

View File

@@ -20,7 +20,7 @@ import (
// Due to how intensive this operation can be on the storage, this function leverages a cache.
func EndpointStatuses(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c)
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
var data []byte
if !exists {
@@ -83,9 +83,10 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*end
}
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
func EndpointStatus(c *fiber.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c)
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
func EndpointStatus(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, cfg.Storage.MaximumNumberOfEvents))
if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
@@ -104,4 +105,5 @@ func EndpointStatus(c *fiber.Ctx) error {
}
c.Set("Content-Type", "application/json")
return c.Status(200).Send(output)
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog"
)
@@ -95,6 +96,10 @@ func TestEndpointStatus(t *testing.T) {
Group: "core",
},
},
Storage: &storage.Config{
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents,
},
}
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
@@ -156,7 +161,13 @@ func TestEndpointStatuses(t *testing.T) {
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
firstResult.Timestamp = time.Time{}
secondResult.Timestamp = time.Time{}
api := New(&config.Config{Metrics: true})
api := New(&config.Config{
Metrics: true,
Storage: &storage.Config{
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents,
},
})
router := api.Router()
type Scenario struct {
Name string

View File

@@ -3,7 +3,6 @@ package api
import (
"strconv"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/gofiber/fiber/v2"
)
@@ -13,12 +12,9 @@ const (
// DefaultPageSize is the default page siZE to use if none is specified or an invalid value is provided
DefaultPageSize = 20
// MaximumPageSize is the maximum page size allowed
MaximumPageSize = common.MaximumNumberOfResults
)
func extractPageAndPageSizeFromRequest(c *fiber.Ctx) (page, pageSize int) {
func extractPageAndPageSizeFromRequest(c *fiber.Ctx, maximumNumberOfResults int) (page, pageSize int) {
var err error
if pageParameter := c.Query("page"); len(pageParameter) == 0 {
page = DefaultPage
@@ -38,8 +34,8 @@ func extractPageAndPageSizeFromRequest(c *fiber.Ctx) (page, pageSize int) {
if err != nil {
pageSize = DefaultPageSize
}
if pageSize > MaximumPageSize {
pageSize = MaximumPageSize
if pageSize > maximumNumberOfResults {
pageSize = maximumNumberOfResults
} else if pageSize < 1 {
pageSize = DefaultPageSize
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"testing"
"github.com/TwiN/gatus/v5/storage"
"github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp"
)
@@ -15,6 +16,7 @@ func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
PageSize string
ExpectedPage int
ExpectedPageSize int
MaximumNumberOfResults int
}
scenarios := []Scenario{
{
@@ -22,36 +24,42 @@ func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
PageSize: "20",
ExpectedPage: 1,
ExpectedPageSize: 20,
MaximumNumberOfResults: 20,
},
{
Page: "2",
PageSize: "10",
ExpectedPage: 2,
ExpectedPageSize: 10,
MaximumNumberOfResults: 40,
},
{
Page: "2",
PageSize: "10",
ExpectedPage: 2,
ExpectedPageSize: 10,
MaximumNumberOfResults: 200,
},
{
Page: "1",
PageSize: "999999",
ExpectedPage: 1,
ExpectedPageSize: MaximumPageSize,
ExpectedPageSize: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfResults: 100,
},
{
Page: "-1",
PageSize: "-1",
ExpectedPage: DefaultPage,
ExpectedPageSize: DefaultPageSize,
MaximumNumberOfResults: 20,
},
{
Page: "invalid",
PageSize: "invalid",
ExpectedPage: DefaultPage,
ExpectedPageSize: DefaultPageSize,
MaximumNumberOfResults: 100,
},
}
for _, scenario := range scenarios {
@@ -61,7 +69,7 @@ func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c)
c.Request().SetRequestURI(fmt.Sprintf("/api/v1/statuses?page=%s&pageSize=%s", scenario.Page, scenario.PageSize))
actualPage, actualPageSize := extractPageAndPageSizeFromRequest(c)
actualPage, actualPageSize := extractPageAndPageSizeFromRequest(c, scenario.MaximumNumberOfResults)
if actualPage != scenario.ExpectedPage {
t.Errorf("expected %d, got %d", scenario.ExpectedPage, actualPage)
}

View File

@@ -280,6 +280,8 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
if err := validateConnectivityConfig(config); err != nil {
return nil, err
}
// Cross-config changes
config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults
}
return
}
@@ -304,6 +306,8 @@ func validateStorageConfig(config *Config) error {
if config.Storage == nil {
config.Storage = &storage.Config{
Type: storage.TypeMemory,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents,
}
} else {
if err := config.Storage.ValidateAndSetDefaults(); err != nil {

View File

@@ -330,6 +330,8 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
storage:
type: sqlite
path: %s
maximum-number-of-results: 10
maximum-number-of-events: 5
maintenance:
enabled: true
@@ -386,6 +388,9 @@ endpoints:
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
t.Error("expected storage to be set to sqlite, got", config.Storage)
}
if config.Storage == nil || config.Storage.MaximumNumberOfResults != 10 || config.Storage.MaximumNumberOfEvents != 5 {
t.Error("expected MaximumNumberOfResults and MaximumNumberOfEvents to be set to 10 and 5, got", config.Storage.MaximumNumberOfResults, config.Storage.MaximumNumberOfEvents)
}
if config.UI == nil || config.UI.Title != "T" || config.UI.Header != "H" || config.UI.Link != "https://example.org" || len(config.UI.Buttons) != 2 || config.UI.Buttons[0].Name != "Home" || config.UI.Buttons[0].Link != "https://example.org" || config.UI.Buttons[1].Name != "Status page" || config.UI.Buttons[1].Link != "https://status.example.org" {
t.Error("expected ui to be set to T, H, https://example.org, 2 buttons, Home and Status page, got", config.UI)
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"html/template"
"github.com/TwiN/gatus/v5/storage"
static "github.com/TwiN/gatus/v5/web"
)
@@ -33,6 +34,8 @@ type Config struct {
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
MaximumNumberOfResults int // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
}
func (cfg *Config) IsDarkMode() bool {
@@ -66,6 +69,7 @@ func GetDefaultConfig() *Config {
Link: defaultLink,
CustomCSS: defaultCustomCSS,
DarkMode: &defaultDarkMode,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
}
}

View File

@@ -4,6 +4,11 @@ import (
"errors"
)
const (
DefaultMaximumNumberOfResults = 100
DefaultMaximumNumberOfEvents = 50
)
var (
ErrSQLStorageRequiresPath = errors.New("sql storage requires a non-empty path to be defined")
ErrMemoryStorageDoesNotSupportPath = errors.New("memory storage does not support persistence, use sqlite if you want persistence on file")
@@ -25,6 +30,12 @@ type Config struct {
// as they happen, also known as the write-through caching strategy.
// Does not apply if Config.Type is not TypePostgres or TypeSQLite.
Caching bool `yaml:"caching,omitempty"`
// MaximumNumberOfResults is the number of results each endpoint should be able to provide
MaximumNumberOfResults int `yaml:"maximum-number-of-results,omitempty"`
// MaximumNumberOfEvents is the number of events each endpoint should be able to provide
MaximumNumberOfEvents int `yaml:"maximum-number-of-events,omitempty"`
}
// ValidateAndSetDefaults validates the configuration and sets the default values (if applicable)
@@ -38,5 +49,11 @@ func (c *Config) ValidateAndSetDefaults() error {
if c.Type == TypeMemory && len(c.Path) > 0 {
return ErrMemoryStorageDoesNotSupportPath
}
if c.MaximumNumberOfResults <= 0 {
c.MaximumNumberOfResults = DefaultMaximumNumberOfResults
}
if c.MaximumNumberOfEvents <= 0 {
c.MaximumNumberOfEvents = DefaultMaximumNumberOfEvents
}
return nil
}

View File

@@ -1,9 +0,0 @@
package common
const (
// MaximumNumberOfResults is the maximum number of results that an endpoint can have
MaximumNumberOfResults = 100
// MaximumNumberOfEvents is the maximum number of events that an endpoint can have
MaximumNumberOfEvents = 50
)

View File

@@ -17,15 +17,20 @@ type Store struct {
sync.RWMutex
cache *gocache.Cache
maximumNumberOfResults int // maximum number of results that an endpoint can have
maximumNumberOfEvents int // maximum number of events that an endpoint can have
}
// NewStore creates a new store using gocache.Cache
//
// This store holds everything in memory, and if the file parameter is not blank,
// supports eventual persistence.
func NewStore() (*Store, error) {
func NewStore(maximumNumberOfResults, maximumNumberOfEvents int) (*Store, error) {
store := &Store{
cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize),
maximumNumberOfResults: maximumNumberOfResults,
maximumNumberOfEvents: maximumNumberOfEvents,
}
return store, nil
}
@@ -151,7 +156,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
Timestamp: time.Now(),
})
}
AddResult(status.(*endpoint.Status), result)
AddResult(status.(*endpoint.Status), result, s.maximumNumberOfResults, s.maximumNumberOfEvents)
s.cache.Set(key, status)
s.Unlock()
return nil

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
)
@@ -82,7 +83,7 @@ var (
// Note that are much more extensive tests in /storage/store/store_test.go.
// This test is simply an extra sanity check
func TestStore_SanityCheck(t *testing.T) {
store, _ := NewStore()
store, _ := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
store.Insert(&testEndpoint, &testSuccessfulResult)
endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
@@ -122,7 +123,7 @@ func TestStore_SanityCheck(t *testing.T) {
}
func TestStore_Save(t *testing.T) {
store, err := NewStore()
store, err := NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
t.Fatal("expected no error, got", err.Error())
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage"
)
func TestProcessUptimeAfterResult(t *testing.T) {
@@ -50,7 +51,7 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
// Start 12 days ago
timestamp := now.Add(-12 * 24 * time.Hour)
for timestamp.Unix() <= now.Unix() {
AddResult(status, &endpoint.Result{Timestamp: timestamp, Success: true})
AddResult(status, &endpoint.Result{Timestamp: timestamp, Success: true}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if len(status.Uptime.HourlyStatistics) > uptimeCleanUpThreshold {
t.Errorf("At no point in time should there be more than %d entries in status.SuccessfulExecutionsPerHour, but there are %d", uptimeCleanUpThreshold, len(status.Uptime.HourlyStatistics))
}

View File

@@ -2,7 +2,6 @@ package memory
import (
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
)
@@ -51,7 +50,7 @@ func getStartAndEndIndex(numberOfResults int, page, pageSize int) (int, int) {
// AddResult adds a Result to Status.Results and makes sure that there are
// no more than MaximumNumberOfResults results in the Results slice
func AddResult(ss *endpoint.Status, result *endpoint.Result) {
func AddResult(ss *endpoint.Status, result *endpoint.Result, maximumNumberOfResults, maximumNumberOfEvents int) {
if ss == nil {
return
}
@@ -59,11 +58,11 @@ func AddResult(ss *endpoint.Status, result *endpoint.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, endpoint.NewEventFromResult(result))
if len(ss.Events) > common.MaximumNumberOfEvents {
if len(ss.Events) > 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)-common.MaximumNumberOfEvents:]
ss.Events = ss.Events[len(ss.Events)-maximumNumberOfEvents:]
}
}
} else {
@@ -71,11 +70,11 @@ func AddResult(ss *endpoint.Status, result *endpoint.Result) {
ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
}
ss.Results = append(ss.Results, result)
if len(ss.Results) > common.MaximumNumberOfResults {
if len(ss.Results) > 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)-common.MaximumNumberOfResults:]
ss.Results = ss.Results[len(ss.Results)-maximumNumberOfResults:]
}
processUptimeAfterResult(ss.Uptime, result)
}

View File

@@ -4,15 +4,15 @@ import (
"testing"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
)
func BenchmarkShallowCopyEndpointStatus(b *testing.B) {
ep := &testEndpoint
status := endpoint.NewStatus(ep.Group, ep.Name)
for i := 0; i < common.MaximumNumberOfResults; i++ {
AddResult(status, &testSuccessfulResult)
for i := 0; i < storage.DefaultMaximumNumberOfResults; i++ {
AddResult(status, &testSuccessfulResult, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
}
for n := 0; n < b.N; n++ {
ShallowCopyEndpointStatus(status, paging.NewEndpointStatusParams().WithResults(1, 20))

View File

@@ -5,24 +5,24 @@ import (
"time"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
)
func TestAddResult(t *testing.T) {
ep := &endpoint.Endpoint{Name: "name", Group: "group"}
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ {
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()})
for i := 0; i < (storage.DefaultMaximumNumberOfResults+storage.DefaultMaximumNumberOfEvents)*2; i++ {
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
}
if len(endpointStatus.Results) != common.MaximumNumberOfResults {
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults)
if len(endpointStatus.Results) != storage.DefaultMaximumNumberOfResults {
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", storage.DefaultMaximumNumberOfResults)
}
if len(endpointStatus.Events) != common.MaximumNumberOfEvents {
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents)
if len(endpointStatus.Events) != storage.DefaultMaximumNumberOfEvents {
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", storage.DefaultMaximumNumberOfEvents)
}
// Try to add nil endpointStatus
AddResult(nil, &endpoint.Result{Timestamp: time.Now()})
AddResult(nil, &endpoint.Result{Timestamp: time.Now()}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
}
func TestShallowCopyEndpointStatus(t *testing.T) {
@@ -30,7 +30,7 @@ func TestShallowCopyEndpointStatus(t *testing.T) {
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
ts := time.Now().Add(-25 * time.Hour)
for i := 0; i < 25; i++ {
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts})
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: ts}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
ts = ts.Add(time.Hour)
}
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 {

View File

@@ -28,8 +28,8 @@ const (
// for aesthetic purposes, I deemed it wasn't worth the performance impact of yet another one-to-many table.
arraySeparator = "|~|"
eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a cleanup
resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results before triggering a cleanup
eventsAboveMaximumCleanUpThreshold = 10 // Maximum number of events above the configured maximum before triggering a cleanup
resultsAboveMaximumCleanUpThreshold = 10 // Maximum number of results above the configured maximum before triggering a cleanup
uptimeTotalEntriesMergeThreshold = 100 // Maximum number of uptime entries before triggering a merge
uptimeAgeCleanUpThreshold = 32 * 24 * time.Hour // Maximum uptime age before triggering a cleanup
@@ -58,17 +58,25 @@ type Store struct {
// writeThroughCache is a cache used to drastically decrease read latency by pre-emptively
// caching writes as they happen. If nil, writes are not cached.
writeThroughCache *gocache.Cache
maximumNumberOfResults int // maximum number of results that an endpoint can have
maximumNumberOfEvents int // maximum number of events that an endpoint can have
}
// NewStore initializes the database and creates the schema if it doesn't already exist in the path specified
func NewStore(driver, path string, caching bool) (*Store, error) {
func NewStore(driver, path string, caching bool, maximumNumberOfResults, maximumNumberOfEvents int) (*Store, error) {
if len(driver) == 0 {
return nil, ErrDatabaseDriverNotSpecified
}
if len(path) == 0 {
return nil, ErrPathNotSpecified
}
store := &Store{driver: driver, path: path}
store := &Store{
driver: driver,
path: path,
maximumNumberOfResults: maximumNumberOfResults,
maximumNumberOfEvents: maximumNumberOfEvents,
}
var err error
if store.db, err = sql.Open(driver, path); err != nil {
return nil, err
@@ -293,10 +301,10 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
}
}
}
// Clean up old events if there's more than twice the maximum number of events
// Clean up old events if we're above the threshold
// This lets us both keep the table clean without impacting performance too much
// (since we're only deleting MaximumNumberOfEvents at a time instead of 1)
if numberOfEvents > eventsCleanUpThreshold {
if numberOfEvents > int64(s.maximumNumberOfEvents+eventsAboveMaximumCleanUpThreshold) {
if err = s.deleteOldEndpointEvents(tx, endpointID); err != nil {
logr.Errorf("[sql.Insert] Failed to delete old events for endpoint with key=%s: %s", ep.Key(), err.Error())
}
@@ -313,7 +321,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
if err != nil {
logr.Errorf("[sql.Insert] Failed to retrieve total number of results for endpoint with key=%s: %s", ep.Key(), err.Error())
} else {
if numberOfResults > resultsCleanUpThreshold {
if numberOfResults > int64(s.maximumNumberOfResults+resultsAboveMaximumCleanUpThreshold) {
if err = s.deleteOldEndpointResults(tx, endpointID); err != nil {
logr.Errorf("[sql.Insert] Failed to delete old results for endpoint with key=%s: %s", ep.Key(), err.Error())
}
@@ -941,7 +949,7 @@ func (s *Store) deleteOldEndpointEvents(tx *sql.Tx, endpointID int64) error {
)
`,
endpointID,
common.MaximumNumberOfEvents,
s.maximumNumberOfEvents,
)
return err
}
@@ -961,7 +969,7 @@ func (s *Store) deleteOldEndpointResults(tx *sql.Tx, endpointID int64) error {
)
`,
endpointID,
common.MaximumNumberOfResults,
s.maximumNumberOfResults,
)
return err
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
)
@@ -84,13 +84,13 @@ var (
)
func TestNewStore(t *testing.T) {
if _, err := NewStore("", t.TempDir()+"/TestNewStore.db", false); !errors.Is(err, ErrDatabaseDriverNotSpecified) {
if _, err := NewStore("", t.TempDir()+"/TestNewStore.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents); !errors.Is(err, ErrDatabaseDriverNotSpecified) {
t.Error("expected error due to blank driver parameter")
}
if _, err := NewStore("sqlite", "", false); !errors.Is(err, ErrPathNotSpecified) {
if _, err := NewStore("sqlite", "", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents); !errors.Is(err, ErrPathNotSpecified) {
t.Error("expected error due to blank path parameter")
}
if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db", true); err != nil {
if store, err := NewStore("sqlite", t.TempDir()+"/TestNewStore.db", true, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents); err != nil {
t.Error("shouldn't have returned any error, got", err.Error())
} else {
_ = store.db.Close()
@@ -98,7 +98,7 @@ func TestNewStore(t *testing.T) {
}
func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
now := time.Now().Truncate(time.Hour)
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
@@ -155,7 +155,7 @@ func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) {
}
func TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
now := time.Now().Truncate(time.Hour)
now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
@@ -212,7 +212,7 @@ func TestStore_HourlyUptimeEntriesAreMergedIntoDailyUptimeEntriesProperly(t *tes
}
func TestStore_getEndpointUptime(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Clear()
defer store.Close()
// Add 768 hourly entries (32 days)
@@ -274,13 +274,15 @@ func TestStore_getEndpointUptime(t *testing.T) {
}
func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpEventsAndResultsProperly.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Clear()
defer store.Close()
resultsCleanUpThreshold := store.maximumNumberOfResults + resultsAboveMaximumCleanUpThreshold
eventsCleanUpThreshold := store.maximumNumberOfEvents + eventsAboveMaximumCleanUpThreshold
for i := 0; i < resultsCleanUpThreshold+eventsCleanUpThreshold; i++ {
store.Insert(&testEndpoint, &testSuccessfulResult)
store.Insert(&testEndpoint, &testUnsuccessfulResult)
ss, _ := store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults*5).WithEvents(1, common.MaximumNumberOfEvents*5))
ss, _ := store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithResults(1, storage.DefaultMaximumNumberOfResults*5).WithEvents(1, storage.DefaultMaximumNumberOfEvents*5))
if len(ss.Results) > resultsCleanUpThreshold+1 {
t.Errorf("number of results shouldn't have exceeded %d, reached %d", resultsCleanUpThreshold, len(ss.Results))
}
@@ -291,7 +293,7 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) {
}
func TestStore_InsertWithCaching(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertWithCaching.db", true)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertWithCaching.db", true, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
// Add 2 results
store.Insert(&testEndpoint, &testSuccessfulResult)
@@ -326,7 +328,7 @@ func TestStore_InsertWithCaching(t *testing.T) {
func TestStore_Persistence(t *testing.T) {
path := t.TempDir() + "/TestStore_Persistence.db"
store, _ := NewStore("sqlite", path, false)
store, _ := NewStore("sqlite", path, false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
store.Insert(&testEndpoint, &testSuccessfulResult)
store.Insert(&testEndpoint, &testUnsuccessfulResult)
if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour), time.Now()); uptime != 0.5 {
@@ -341,15 +343,15 @@ func TestStore_Persistence(t *testing.T) {
if uptime, _ := store.GetUptimeByKey(testEndpoint.Key(), time.Now().Add(-time.Hour*24*30), time.Now()); uptime != 0.5 {
t.Errorf("the uptime over the past 30d should've been 0.5, got %f", uptime)
}
ssFromOldStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
ssFromOldStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, storage.DefaultMaximumNumberOfResults).WithEvents(1, storage.DefaultMaximumNumberOfEvents))
if ssFromOldStore == nil || ssFromOldStore.Group != "group" || ssFromOldStore.Name != "name" || len(ssFromOldStore.Events) != 3 || len(ssFromOldStore.Results) != 2 {
store.Close()
t.Fatal("sanity check failed")
}
store.Close()
store, _ = NewStore("sqlite", path, false)
store, _ = NewStore("sqlite", path, false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
ssFromNewStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, common.MaximumNumberOfResults).WithEvents(1, common.MaximumNumberOfEvents))
ssFromNewStore, _ := store.GetEndpointStatus(testEndpoint.Group, testEndpoint.Name, paging.NewEndpointStatusParams().WithResults(1, storage.DefaultMaximumNumberOfResults).WithEvents(1, storage.DefaultMaximumNumberOfEvents))
if ssFromNewStore == nil || ssFromNewStore.Group != "group" || ssFromNewStore.Name != "name" || len(ssFromNewStore.Events) != 3 || len(ssFromNewStore.Results) != 2 {
t.Fatal("failed sanity check")
}
@@ -411,7 +413,7 @@ func TestStore_Persistence(t *testing.T) {
}
func TestStore_Save(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_Save.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_Save.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
if store.Save() != nil {
t.Error("Save shouldn't do anything for this store")
@@ -421,7 +423,7 @@ func TestStore_Save(t *testing.T) {
// Note that are much more extensive tests in /storage/store/store_test.go.
// This test is simply an extra sanity check
func TestStore_SanityCheck(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_SanityCheck.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_SanityCheck.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
store.Insert(&testEndpoint, &testSuccessfulResult)
endpointStatuses, _ := store.GetAllEndpointStatuses(paging.NewEndpointStatusParams())
@@ -465,7 +467,7 @@ func TestStore_SanityCheck(t *testing.T) {
// TestStore_InvalidTransaction tests what happens if an invalid transaction is passed as parameter
func TestStore_InvalidTransaction(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InvalidTransaction.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InvalidTransaction.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
tx, _ := store.db.Begin()
tx.Commit()
@@ -523,7 +525,7 @@ func TestStore_InvalidTransaction(t *testing.T) {
}
func TestStore_NoRows(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_NoRows.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_NoRows.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
tx, _ := store.db.Begin()
defer tx.Rollback()
@@ -537,7 +539,7 @@ func TestStore_NoRows(t *testing.T) {
// This tests very unlikely cases where a table is deleted.
func TestStore_BrokenSchema(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_BrokenSchema.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_BrokenSchema.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
if err := store.Insert(&testEndpoint, &testSuccessfulResult); err != nil {
t.Fatal("expected no error, got", err.Error())
@@ -725,7 +727,7 @@ func TestCacheKey(t *testing.T) {
}
func TestTriggeredEndpointAlertsPersistence(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestTriggeredEndpointAlertsPersistence.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestTriggeredEndpointAlertsPersistence.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
yes, desc := false, "description"
ep := testEndpoint
@@ -791,7 +793,7 @@ func TestTriggeredEndpointAlertsPersistence(t *testing.T) {
}
func TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint(t *testing.T) {
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint.db", false)
store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllTriggeredAlertsNotInChecksumsByEndpoint.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
defer store.Close()
yes, desc := false, "description"
ep1 := testEndpoint

View File

@@ -111,7 +111,10 @@ func Initialize(cfg *storage.Config) error {
if cfg == nil {
// This only happens in tests
logr.Warn("[store.Initialize] nil storage config passed as parameter. This should only happen in tests. Defaulting to an empty config.")
cfg = &storage.Config{}
cfg = &storage.Config{
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents,
}
}
if len(cfg.Path) == 0 && cfg.Type != storage.TypePostgres {
logr.Infof("[store.Initialize] Creating storage provider of type=%s", cfg.Type)
@@ -119,14 +122,14 @@ func Initialize(cfg *storage.Config) error {
ctx, cancelFunc = context.WithCancel(context.Background())
switch cfg.Type {
case storage.TypeSQLite, storage.TypePostgres:
store, err = sql.NewStore(string(cfg.Type), cfg.Path, cfg.Caching)
store, err = sql.NewStore(string(cfg.Type), cfg.Path, cfg.Caching, cfg.MaximumNumberOfResults, cfg.MaximumNumberOfEvents)
if err != nil {
return err
}
case storage.TypeMemory:
fallthrough
default:
store, _ = memory.NewStore()
store, _ = memory.NewStore(cfg.MaximumNumberOfResults, cfg.MaximumNumberOfEvents)
}
return nil
}

View File

@@ -6,17 +6,18 @@ import (
"time"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/storage/store/common/paging"
"github.com/TwiN/gatus/v5/storage/store/memory"
"github.com/TwiN/gatus/v5/storage/store/sql"
)
func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
memoryStore, err := memory.NewStore()
memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_GetAllEndpointStatuses.db", false)
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_GetAllEndpointStatuses.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}
@@ -81,11 +82,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
}
func BenchmarkStore_Insert(b *testing.B) {
memoryStore, err := memory.NewStore()
memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_Insert.db", false)
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_Insert.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}
@@ -153,11 +154,11 @@ func BenchmarkStore_Insert(b *testing.B) {
}
func BenchmarkStore_GetEndpointStatusByKey(b *testing.B) {
memoryStore, err := memory.NewStore()
memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_GetEndpointStatusByKey.db", false)
sqliteStore, err := sql.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_GetEndpointStatusByKey.db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
b.Fatal("failed to create store:", err.Error())
}

View File

@@ -91,15 +91,15 @@ type Scenario struct {
}
func initStoresAndBaseScenarios(t *testing.T, testName string) []*Scenario {
memoryStore, err := memory.NewStore()
memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
t.Fatal("failed to create store:", err.Error())
}
sqliteStore, err := sql.NewStore("sqlite", t.TempDir()+"/"+testName+".db", false)
sqliteStore, err := sql.NewStore("sqlite", t.TempDir()+"/"+testName+".db", false, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
t.Fatal("failed to create store:", err.Error())
}
sqliteStoreWithCaching, err := sql.NewStore("sqlite", t.TempDir()+"/"+testName+"-with-caching.db", true)
sqliteStoreWithCaching, err := sql.NewStore("sqlite", t.TempDir()+"/"+testName+"-with-caching.db", true, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil {
t.Fatal("failed to create store:", err.Error())
}
@@ -138,7 +138,7 @@ func TestStore_GetEndpointStatusByKey(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &firstResult)
scenario.Store.Insert(&testEndpoint, &secondResult)
endpointStatus, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
endpointStatus, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if err != nil {
t.Fatal("shouldn't have returned an error, got", err.Error())
}
@@ -158,7 +158,7 @@ func TestStore_GetEndpointStatusByKey(t *testing.T) {
t.Error("The result at index 0 should've been older than the result at index 1")
}
scenario.Store.Insert(&testEndpoint, &thirdResult)
endpointStatus, err = scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
endpointStatus, err = scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if err != nil {
t.Fatal("shouldn't have returned an error, got", err.Error())
}
@@ -176,21 +176,21 @@ func TestStore_GetEndpointStatusForMissingStatusReturnsNil(t *testing.T) {
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &testSuccessfulResult)
endpointStatus, err := scenario.Store.GetEndpointStatus("nonexistantgroup", "nonexistantname", paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
endpointStatus, err := scenario.Store.GetEndpointStatus("nonexistantgroup", "nonexistantname", paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err)
}
if endpointStatus != nil {
t.Errorf("Returned endpoint status for group '%s' and name '%s' not nil after inserting the endpoint into the store", testEndpoint.Group, testEndpoint.Name)
}
endpointStatus, err = scenario.Store.GetEndpointStatus(testEndpoint.Group, "nonexistantname", paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
endpointStatus, err = scenario.Store.GetEndpointStatus(testEndpoint.Group, "nonexistantname", paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err)
}
if endpointStatus != nil {
t.Errorf("Returned endpoint status for group '%s' and name '%s' not nil after inserting the endpoint into the store", testEndpoint.Group, "nonexistantname")
}
endpointStatus, err = scenario.Store.GetEndpointStatus("nonexistantgroup", testEndpoint.Name, paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
endpointStatus, err = scenario.Store.GetEndpointStatus("nonexistantgroup", testEndpoint.Name, paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err)
}
@@ -470,7 +470,7 @@ func TestStore_Insert(t *testing.T) {
t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &firstResult)
scenario.Store.Insert(&testEndpoint, &secondResult)
ss, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, common.MaximumNumberOfEvents).WithResults(1, common.MaximumNumberOfResults))
ss, err := scenario.Store.GetEndpointStatusByKey(testEndpoint.Key(), paging.NewEndpointStatusParams().WithEvents(1, storage.DefaultMaximumNumberOfEvents).WithResults(1, storage.DefaultMaximumNumberOfResults))
if err != nil {
t.Error("shouldn't have returned an error, got", err)
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<script type="text/javascript">
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: []};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
</script>
<title>{{ .UI.Title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

View File

@@ -1,7 +1,7 @@
<template>
<div class="mt-3 flex">
<div class="flex-1">
<button v-if="currentPage < 5" @click="nextPage" class="bg-gray-100 hover:bg-gray-200 text-gray-500 border border-gray-200 px-2 rounded font-mono dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600">&lt;</button>
<button v-if="currentPage < maxPages" @click="nextPage" class="bg-gray-100 hover:bg-gray-200 text-gray-500 border border-gray-200 px-2 rounded font-mono dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600">&lt;</button>
</div>
<div class="flex-1 text-right">
<button v-if="currentPage > 1" @click="previousPage" class="bg-gray-100 hover:bg-gray-200 text-gray-500 border border-gray-200 px-2 rounded font-mono dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600">&gt;</button>
@@ -13,6 +13,9 @@
<script>
export default {
name: 'Pagination',
props: {
numberOfResultsPerPage: Number,
},
components: {},
emits: ['page'],
methods: {
@@ -25,6 +28,11 @@ export default {
this.$emit('page', this.currentPage);
}
},
computed: {
maxPages() {
return Math.ceil(parseInt(window.config.maximumNumberOfResults) / this.numberOfResultsPerPage)
}
},
data() {
return {
currentPage: 1,

View File

@@ -14,7 +14,7 @@
@toggleShowAverageResponseTime="toggleShowAverageResponseTime"
:showAverageResponseTime="showAverageResponseTime"
/>
<Pagination @page="changePage"/>
<Pagination @page="changePage" :numberOfResultsPerPage="20" />
</slot>
<div v-if="endpointStatus && endpointStatus.key" class="mt-12">
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">UPTIME</h1>

View File

@@ -9,7 +9,7 @@
@toggleShowAverageResponseTime="toggleShowAverageResponseTime"
:showAverageResponseTime="showAverageResponseTime"
/>
<Pagination v-show="retrievedData" @page="changePage"/>
<Pagination v-show="retrievedData" @page="changePage" :numberOfResultsPerPage="20" />
</slot>
<Settings @refreshData="fetchData"/>
</template>

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: []};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>

File diff suppressed because one or more lines are too long