1
0
mirror of https://github.com/TwiN/gatus.git synced 2026-02-15 14:25:04 +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

@@ -382,12 +382,14 @@ Here are some examples of conditions you can use:
### Storage ### Storage
| Parameter | Description | Default | | Parameter | Description | Default |
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------| |:------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|
| `storage` | Storage configuration | `{}` | | `storage` | Storage configuration | `{}` |
| `storage.path` | Path to persist the data in. Only supported for types `sqlite` and `postgres`. | `""` | | `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.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.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 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. 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. # Because the data is stored in memory, the data will not survive a restart.
storage: storage:
type: memory type: memory
maximum-number-of-results: 200
maximum-number-of-events: 5
``` ```
- If `storage.type` is `sqlite`, `storage.path` must not be blank: - If `storage.type` is `sqlite`, `storage.path` must not be blank:
```yaml ```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/statuses", EndpointStatuses(cfg))
protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus) protectedAPIRouter.Get("/v1/endpoints/:key/statuses", EndpointStatus(cfg))
return app 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. // Due to how intensive this operation can be on the storage, this function leverages a cache.
func EndpointStatuses(cfg *config.Config) fiber.Handler { func EndpointStatuses(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error { 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)) value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
var data []byte var data []byte
if !exists { if !exists {
@@ -83,25 +83,27 @@ func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*end
} }
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name // EndpointStatus retrieves a single endpoint.Status by group and endpoint name
func EndpointStatus(c *fiber.Ctx) error { func EndpointStatus(cfg *config.Config) fiber.Handler {
page, pageSize := extractPageAndPageSizeFromRequest(c) return func(c *fiber.Ctx) error {
endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents)) page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
if err != nil { endpointStatus, err := store.Get().GetEndpointStatusByKey(c.Params("key"), paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, cfg.Storage.MaximumNumberOfEvents))
if errors.Is(err, common.ErrEndpointNotFound) { if err != nil {
return c.Status(404).SendString(err.Error()) if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
}
logr.Errorf("[api.EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
return c.Status(500).SendString(err.Error())
} }
logr.Errorf("[api.EndpointStatus] Failed to retrieve endpoint status: %s", err.Error()) if endpointStatus == nil { // XXX: is this check necessary?
return c.Status(500).SendString(err.Error()) logr.Errorf("[api.EndpointStatus] Endpoint with key=%s not found", c.Params("key"))
return c.Status(404).SendString("not found")
}
output, err := json.Marshal(endpointStatus)
if err != nil {
logr.Errorf("[api.EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
return c.Status(500).SendString("unable to marshal object to JSON")
}
c.Set("Content-Type", "application/json")
return c.Status(200).Send(output)
} }
if endpointStatus == nil { // XXX: is this check necessary?
logr.Errorf("[api.EndpointStatus] Endpoint with key=%s not found", c.Params("key"))
return c.Status(404).SendString("not found")
}
output, err := json.Marshal(endpointStatus)
if err != nil {
logr.Errorf("[api.EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
return c.Status(500).SendString("unable to marshal object to JSON")
}
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"
"github.com/TwiN/gatus/v5/config/endpoint" "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/storage/store"
"github.com/TwiN/gatus/v5/watchdog" "github.com/TwiN/gatus/v5/watchdog"
) )
@@ -95,6 +96,10 @@ func TestEndpointStatus(t *testing.T) {
Group: "core", 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[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()}) 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 // Can't be bothered dealing with timezone issues on the worker that runs the automated tests
firstResult.Timestamp = time.Time{} firstResult.Timestamp = time.Time{}
secondResult.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() router := api.Router()
type Scenario struct { type Scenario struct {
Name string Name string

View File

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

View File

@@ -4,54 +4,62 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/TwiN/gatus/v5/storage"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
func TestExtractPageAndPageSizeFromRequest(t *testing.T) { func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
type Scenario struct { type Scenario struct {
Name string Name string
Page string Page string
PageSize string PageSize string
ExpectedPage int ExpectedPage int
ExpectedPageSize int ExpectedPageSize int
MaximumNumberOfResults int
} }
scenarios := []Scenario{ scenarios := []Scenario{
{ {
Page: "1", Page: "1",
PageSize: "20", PageSize: "20",
ExpectedPage: 1, ExpectedPage: 1,
ExpectedPageSize: 20, ExpectedPageSize: 20,
MaximumNumberOfResults: 20,
}, },
{ {
Page: "2", Page: "2",
PageSize: "10", PageSize: "10",
ExpectedPage: 2, ExpectedPage: 2,
ExpectedPageSize: 10, ExpectedPageSize: 10,
MaximumNumberOfResults: 40,
}, },
{ {
Page: "2", Page: "2",
PageSize: "10", PageSize: "10",
ExpectedPage: 2, ExpectedPage: 2,
ExpectedPageSize: 10, ExpectedPageSize: 10,
MaximumNumberOfResults: 200,
}, },
{ {
Page: "1", Page: "1",
PageSize: "999999", PageSize: "999999",
ExpectedPage: 1, ExpectedPage: 1,
ExpectedPageSize: MaximumPageSize, ExpectedPageSize: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfResults: 100,
}, },
{ {
Page: "-1", Page: "-1",
PageSize: "-1", PageSize: "-1",
ExpectedPage: DefaultPage, ExpectedPage: DefaultPage,
ExpectedPageSize: DefaultPageSize, ExpectedPageSize: DefaultPageSize,
MaximumNumberOfResults: 20,
}, },
{ {
Page: "invalid", Page: "invalid",
PageSize: "invalid", PageSize: "invalid",
ExpectedPage: DefaultPage, ExpectedPage: DefaultPage,
ExpectedPageSize: DefaultPageSize, ExpectedPageSize: DefaultPageSize,
MaximumNumberOfResults: 100,
}, },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
@@ -61,7 +69,7 @@ func TestExtractPageAndPageSizeFromRequest(t *testing.T) {
c := app.AcquireCtx(&fasthttp.RequestCtx{}) c := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(c) defer app.ReleaseCtx(c)
c.Request().SetRequestURI(fmt.Sprintf("/api/v1/statuses?page=%s&pageSize=%s", scenario.Page, scenario.PageSize)) 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 { if actualPage != scenario.ExpectedPage {
t.Errorf("expected %d, got %d", scenario.ExpectedPage, actualPage) 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 { if err := validateConnectivityConfig(config); err != nil {
return nil, err return nil, err
} }
// Cross-config changes
config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults
} }
return return
} }
@@ -303,7 +305,9 @@ func validateRemoteConfig(config *Config) error {
func validateStorageConfig(config *Config) error { func validateStorageConfig(config *Config) error {
if config.Storage == nil { if config.Storage == nil {
config.Storage = &storage.Config{ config.Storage = &storage.Config{
Type: storage.TypeMemory, Type: storage.TypeMemory,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
MaximumNumberOfEvents: storage.DefaultMaximumNumberOfEvents,
} }
} else { } else {
if err := config.Storage.ValidateAndSetDefaults(); err != nil { if err := config.Storage.ValidateAndSetDefaults(); err != nil {

View File

@@ -330,6 +330,8 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
storage: storage:
type: sqlite type: sqlite
path: %s path: %s
maximum-number-of-results: 10
maximum-number-of-events: 5
maintenance: maintenance:
enabled: true enabled: true
@@ -386,6 +388,9 @@ endpoints:
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite { 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) 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" { 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) 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" "errors"
"html/template" "html/template"
"github.com/TwiN/gatus/v5/storage"
static "github.com/TwiN/gatus/v5/web" 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 Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page 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 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 { func (cfg *Config) IsDarkMode() bool {
@@ -59,13 +62,14 @@ func (btn *Button) Validate() error {
// GetDefaultConfig returns a Config struct with the default values // GetDefaultConfig returns a Config struct with the default values
func GetDefaultConfig() *Config { func GetDefaultConfig() *Config {
return &Config{ return &Config{
Title: defaultTitle, Title: defaultTitle,
Description: defaultDescription, Description: defaultDescription,
Header: defaultHeader, Header: defaultHeader,
Logo: defaultLogo, Logo: defaultLogo,
Link: defaultLink, Link: defaultLink,
CustomCSS: defaultCustomCSS, CustomCSS: defaultCustomCSS,
DarkMode: &defaultDarkMode, DarkMode: &defaultDarkMode,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
} }
} }

View File

@@ -4,6 +4,11 @@ import (
"errors" "errors"
) )
const (
DefaultMaximumNumberOfResults = 100
DefaultMaximumNumberOfEvents = 50
)
var ( var (
ErrSQLStorageRequiresPath = errors.New("sql storage requires a non-empty path to be defined") 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") 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. // as they happen, also known as the write-through caching strategy.
// Does not apply if Config.Type is not TypePostgres or TypeSQLite. // Does not apply if Config.Type is not TypePostgres or TypeSQLite.
Caching bool `yaml:"caching,omitempty"` 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) // 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 { if c.Type == TypeMemory && len(c.Path) > 0 {
return ErrMemoryStorageDoesNotSupportPath return ErrMemoryStorageDoesNotSupportPath
} }
if c.MaximumNumberOfResults <= 0 {
c.MaximumNumberOfResults = DefaultMaximumNumberOfResults
}
if c.MaximumNumberOfEvents <= 0 {
c.MaximumNumberOfEvents = DefaultMaximumNumberOfEvents
}
return nil 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 sync.RWMutex
cache *gocache.Cache 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 // NewStore creates a new store using gocache.Cache
// //
// This store holds everything in memory, and if the file parameter is not blank, // This store holds everything in memory, and if the file parameter is not blank,
// supports eventual persistence. // supports eventual persistence.
func NewStore() (*Store, error) { func NewStore(maximumNumberOfResults, maximumNumberOfEvents int) (*Store, error) {
store := &Store{ store := &Store{
cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize),
maximumNumberOfResults: maximumNumberOfResults,
maximumNumberOfEvents: maximumNumberOfEvents,
} }
return store, nil return store, nil
} }
@@ -151,7 +156,7 @@ func (s *Store) Insert(ep *endpoint.Endpoint, result *endpoint.Result) error {
Timestamp: time.Now(), Timestamp: time.Now(),
}) })
} }
AddResult(status.(*endpoint.Status), result) AddResult(status.(*endpoint.Status), result, s.maximumNumberOfResults, s.maximumNumberOfEvents)
s.cache.Set(key, status) s.cache.Set(key, status)
s.Unlock() s.Unlock()
return nil return nil

View File

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

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config/endpoint" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage"
) )
func TestProcessUptimeAfterResult(t *testing.T) { func TestProcessUptimeAfterResult(t *testing.T) {
@@ -50,7 +51,7 @@ func TestAddResultUptimeIsCleaningUpAfterItself(t *testing.T) {
// Start 12 days ago // Start 12 days ago
timestamp := now.Add(-12 * 24 * time.Hour) timestamp := now.Add(-12 * 24 * time.Hour)
for timestamp.Unix() <= now.Unix() { 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 { 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)) 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 ( import (
"github.com/TwiN/gatus/v5/config/endpoint" "github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/TwiN/gatus/v5/storage/store/common/paging" "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 // AddResult adds a Result to Status.Results and makes sure that there are
// no more than MaximumNumberOfResults results in the Results slice // 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 { if ss == nil {
return return
} }
@@ -59,11 +58,11 @@ func AddResult(ss *endpoint.Status, result *endpoint.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, endpoint.NewEventFromResult(result)) 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 // 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)-common.MaximumNumberOfEvents:] ss.Events = ss.Events[len(ss.Events)-maximumNumberOfEvents:]
} }
} }
} else { } else {
@@ -71,11 +70,11 @@ func AddResult(ss *endpoint.Status, result *endpoint.Result) {
ss.Events = append(ss.Events, endpoint.NewEventFromResult(result)) ss.Events = append(ss.Events, endpoint.NewEventFromResult(result))
} }
ss.Results = append(ss.Results, 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 // 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)-common.MaximumNumberOfResults:] ss.Results = ss.Results[len(ss.Results)-maximumNumberOfResults:]
} }
processUptimeAfterResult(ss.Uptime, result) processUptimeAfterResult(ss.Uptime, result)
} }

View File

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

View File

@@ -5,24 +5,24 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config/endpoint" "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" "github.com/TwiN/gatus/v5/storage/store/common/paging"
) )
func TestAddResult(t *testing.T) { func TestAddResult(t *testing.T) {
ep := &endpoint.Endpoint{Name: "name", Group: "group"} ep := &endpoint.Endpoint{Name: "name", Group: "group"}
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name) endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
for i := 0; i < (common.MaximumNumberOfResults+common.MaximumNumberOfEvents)*2; i++ { for i := 0; i < (storage.DefaultMaximumNumberOfResults+storage.DefaultMaximumNumberOfEvents)*2; i++ {
AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()}) AddResult(endpointStatus, &endpoint.Result{Success: i%2 == 0, Timestamp: time.Now()}, storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
} }
if len(endpointStatus.Results) != common.MaximumNumberOfResults { if len(endpointStatus.Results) != storage.DefaultMaximumNumberOfResults {
t.Errorf("expected endpointStatus.Results to not exceed a length of %d", common.MaximumNumberOfResults) t.Errorf("expected endpointStatus.Results to not exceed a length of %d", storage.DefaultMaximumNumberOfResults)
} }
if len(endpointStatus.Events) != common.MaximumNumberOfEvents { if len(endpointStatus.Events) != storage.DefaultMaximumNumberOfEvents {
t.Errorf("expected endpointStatus.Events to not exceed a length of %d", common.MaximumNumberOfEvents) t.Errorf("expected endpointStatus.Events to not exceed a length of %d", storage.DefaultMaximumNumberOfEvents)
} }
// Try to add nil endpointStatus // 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) { func TestShallowCopyEndpointStatus(t *testing.T) {
@@ -30,7 +30,7 @@ func TestShallowCopyEndpointStatus(t *testing.T) {
endpointStatus := endpoint.NewStatus(ep.Group, ep.Name) endpointStatus := endpoint.NewStatus(ep.Group, ep.Name)
ts := time.Now().Add(-25 * time.Hour) ts := time.Now().Add(-25 * time.Hour)
for i := 0; i < 25; i++ { 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) ts = ts.Add(time.Hour)
} }
if len(ShallowCopyEndpointStatus(endpointStatus, paging.NewEndpointStatusParams().WithResults(-1, -1)).Results) != 0 { 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. // for aesthetic purposes, I deemed it wasn't worth the performance impact of yet another one-to-many table.
arraySeparator = "|~|" arraySeparator = "|~|"
eventsCleanUpThreshold = common.MaximumNumberOfEvents + 10 // Maximum number of events before triggering a cleanup eventsAboveMaximumCleanUpThreshold = 10 // Maximum number of events above the configured maximum before triggering a cleanup
resultsCleanUpThreshold = common.MaximumNumberOfResults + 10 // Maximum number of results 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 uptimeTotalEntriesMergeThreshold = 100 // Maximum number of uptime entries before triggering a merge
uptimeAgeCleanUpThreshold = 32 * 24 * time.Hour // Maximum uptime age before triggering a cleanup 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 // writeThroughCache is a cache used to drastically decrease read latency by pre-emptively
// caching writes as they happen. If nil, writes are not cached. // caching writes as they happen. If nil, writes are not cached.
writeThroughCache *gocache.Cache 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 // 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 { if len(driver) == 0 {
return nil, ErrDatabaseDriverNotSpecified return nil, ErrDatabaseDriverNotSpecified
} }
if len(path) == 0 { if len(path) == 0 {
return nil, ErrPathNotSpecified return nil, ErrPathNotSpecified
} }
store := &Store{driver: driver, path: path} store := &Store{
driver: driver,
path: path,
maximumNumberOfResults: maximumNumberOfResults,
maximumNumberOfEvents: maximumNumberOfEvents,
}
var err error var err error
if store.db, err = sql.Open(driver, path); err != nil { if store.db, err = sql.Open(driver, path); err != nil {
return nil, err 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 // 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) // (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 { 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()) 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 { if err != nil {
logr.Errorf("[sql.Insert] Failed to retrieve total number of results for endpoint with key=%s: %s", ep.Key(), err.Error()) logr.Errorf("[sql.Insert] Failed to retrieve total number of results for endpoint with key=%s: %s", ep.Key(), err.Error())
} else { } else {
if numberOfResults > resultsCleanUpThreshold { if numberOfResults > int64(s.maximumNumberOfResults+resultsAboveMaximumCleanUpThreshold) {
if err = s.deleteOldEndpointResults(tx, endpointID); err != nil { 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()) 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, endpointID,
common.MaximumNumberOfEvents, s.maximumNumberOfEvents,
) )
return err return err
} }
@@ -961,7 +969,7 @@ func (s *Store) deleteOldEndpointResults(tx *sql.Tx, endpointID int64) error {
) )
`, `,
endpointID, endpointID,
common.MaximumNumberOfResults, s.maximumNumberOfResults,
) )
return err return err
} }

View File

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

View File

@@ -111,7 +111,10 @@ func Initialize(cfg *storage.Config) error {
if cfg == nil { if cfg == nil {
// This only happens in tests // 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.") 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 { if len(cfg.Path) == 0 && cfg.Type != storage.TypePostgres {
logr.Infof("[store.Initialize] Creating storage provider of type=%s", cfg.Type) 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()) ctx, cancelFunc = context.WithCancel(context.Background())
switch cfg.Type { switch cfg.Type {
case storage.TypeSQLite, storage.TypePostgres: 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 { if err != nil {
return err return err
} }
case storage.TypeMemory: case storage.TypeMemory:
fallthrough fallthrough
default: default:
store, _ = memory.NewStore() store, _ = memory.NewStore(cfg.MaximumNumberOfResults, cfg.MaximumNumberOfEvents)
} }
return nil return nil
} }

View File

@@ -6,17 +6,18 @@ import (
"time" "time"
"github.com/TwiN/gatus/v5/config/endpoint" "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/common/paging"
"github.com/TwiN/gatus/v5/storage/store/memory" "github.com/TwiN/gatus/v5/storage/store/memory"
"github.com/TwiN/gatus/v5/storage/store/sql" "github.com/TwiN/gatus/v5/storage/store/sql"
) )
func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) { func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
memoryStore, err := memory.NewStore() memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil { if err != nil {
b.Fatal("failed to create store:", err.Error()) 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 { if err != nil {
b.Fatal("failed to create store:", err.Error()) b.Fatal("failed to create store:", err.Error())
} }
@@ -81,11 +82,11 @@ func BenchmarkStore_GetAllEndpointStatuses(b *testing.B) {
} }
func BenchmarkStore_Insert(b *testing.B) { func BenchmarkStore_Insert(b *testing.B) {
memoryStore, err := memory.NewStore() memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil { if err != nil {
b.Fatal("failed to create store:", err.Error()) 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 { if err != nil {
b.Fatal("failed to create store:", err.Error()) b.Fatal("failed to create store:", err.Error())
} }
@@ -153,11 +154,11 @@ func BenchmarkStore_Insert(b *testing.B) {
} }
func BenchmarkStore_GetEndpointStatusByKey(b *testing.B) { func BenchmarkStore_GetEndpointStatusByKey(b *testing.B) {
memoryStore, err := memory.NewStore() memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil { if err != nil {
b.Fatal("failed to create store:", err.Error()) 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 { if err != nil {
b.Fatal("failed to create store:", err.Error()) 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 { func initStoresAndBaseScenarios(t *testing.T, testName string) []*Scenario {
memoryStore, err := memory.NewStore() memoryStore, err := memory.NewStore(storage.DefaultMaximumNumberOfResults, storage.DefaultMaximumNumberOfEvents)
if err != nil { if err != nil {
t.Fatal("failed to create store:", err.Error()) 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 { if err != nil {
t.Fatal("failed to create store:", err.Error()) 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 { if err != nil {
t.Fatal("failed to create store:", err.Error()) 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) { t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &firstResult) scenario.Store.Insert(&testEndpoint, &firstResult)
scenario.Store.Insert(&testEndpoint, &secondResult) 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 { if err != nil {
t.Fatal("shouldn't have returned an error, got", err.Error()) 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") t.Error("The result at index 0 should've been older than the result at index 1")
} }
scenario.Store.Insert(&testEndpoint, &thirdResult) 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 { if err != nil {
t.Fatal("shouldn't have returned an error, got", err.Error()) 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 { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &testSuccessfulResult) 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) { if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err) t.Error("should've returned ErrEndpointNotFound, got", err)
} }
if endpointStatus != nil { 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) 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) { if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err) t.Error("should've returned ErrEndpointNotFound, got", err)
} }
if endpointStatus != nil { 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") 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) { if !errors.Is(err, common.ErrEndpointNotFound) {
t.Error("should've returned ErrEndpointNotFound, got", err) 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) { t.Run(scenario.Name, func(t *testing.T) {
scenario.Store.Insert(&testEndpoint, &firstResult) scenario.Store.Insert(&testEndpoint, &firstResult)
scenario.Store.Insert(&testEndpoint, &secondResult) 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 { if err != nil {
t.Error("shouldn't have returned an error, got", err) t.Error("shouldn't have returned an error, got", err)
} }

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script type="text/javascript"> <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> </script>
<title>{{ .UI.Title }}</title> <title>{{ .UI.Title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="mt-3 flex"> <div class="mt-3 flex">
<div class="flex-1"> <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>
<div class="flex-1 text-right"> <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> <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> <script>
export default { export default {
name: 'Pagination', name: 'Pagination',
props: {
numberOfResultsPerPage: Number,
},
components: {}, components: {},
emits: ['page'], emits: ['page'],
methods: { methods: {
@@ -25,6 +28,11 @@ export default {
this.$emit('page', this.currentPage); this.$emit('page', this.currentPage);
} }
}, },
computed: {
maxPages() {
return Math.ceil(parseInt(window.config.maximumNumberOfResults) / this.numberOfResultsPerPage)
}
},
data() { data() {
return { return {
currentPage: 1, currentPage: 1,

View File

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

View File

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