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:
16
README.md
16
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
api/util.go
10
api/util.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"><</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"><</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">></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">></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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
Reference in New Issue
Block a user