Compare commits

..
10 Commits
Author SHA1 Message Date
TwiN 9287e2f9e2 Move store initialization to store package
This will allow importing storage.Config without importing every SQL drivers in the known universe
2021-10-28 19:35:46 -04:00
TwiN 257f859825 Rename getPagerDutyIntegrationKeyForGroup to getIntegrationKeyForGroup 2021-10-27 23:16:05 -04:00
TwiN 3a4ab62ddd #191: Handle memory issue caused by migration from Service to Endpoint 2021-10-24 21:20:01 -04:00
TwiN a4e9d8e9b0 Revert "Add GATUS_DONT_EXPAND_ENV env var" 2021-10-24 18:34:39 -04:00
TwiN 3be6d04d29 Add GATUS_DONT_EXPAND_ENV env var 2021-10-24 16:20:24 -04:00
TwiN b59ff6f89e Add ServiceAccount to Kubernetes example 2021-10-24 15:33:15 -04:00
TwiN 813fea93ee #167: Rename examples/minimal to .examples/docker-minimal 2021-10-24 15:27:25 -04:00
TwiN 8f50e44b45 #167: Rename examples/ to .examples/ 2021-10-24 15:20:39 -04:00
TwiN fb2448c15a Omit fields that are not set 2021-10-24 15:03:41 -04:00
TwiN db575aad13 Remove comments that no longer apply 2021-10-24 14:51:49 -04:00
40 changed files with 304 additions and 269 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
examples
.examples
Dockerfile
.github
.idea
@@ -9,14 +9,17 @@ data:
endpoints:
- name: website
url: https://twin.sh/health
interval: 1m
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- name: github
url: https://api.github.com/healthz
interval: 5m
conditions:
- "[STATUS] == 200"
- name: cat-fact
url: "https://cat-fact.herokuapp.com/facts/random"
interval: 5m
@@ -27,11 +30,18 @@ data:
- "[BODY].text == pat(*cat*)"
- "[STATUS] == pat(2*)"
- "[CONNECTED] == true"
- name: example
url: https://example.com/
conditions:
- "[STATUS] == 200"
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gatus
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -41,14 +51,16 @@ spec:
replicas: 1
selector:
matchLabels:
k8s-app: gatus
app: gatus
template:
metadata:
labels:
k8s-app: gatus
app: gatus
name: gatus
namespace: kube-system
spec:
serviceAccountName: gatus
terminationGracePeriodSeconds: 5
containers:
- image: twinproduction/gatus
imagePullPolicy: IfNotPresent
@@ -84,4 +96,4 @@ spec:
protocol: TCP
targetPort: 8080
selector:
k8s-app: gatus
app: gatus
+10 -10
View File
@@ -19,7 +19,7 @@ core applications: https://status.twin.sh/
<details>
<summary><b>Quick start</b></summary>
```
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus
```
For more details, see [Usage](#usage)
@@ -246,7 +246,7 @@ storage:
type: sqlite
file: data.db
```
See [examples/docker-compose-sqlite-storage](examples/docker-compose-sqlite-storage) for an example.
See [examples/docker-compose-sqlite-storage](.examples/docker-compose-sqlite-storage) for an example.
- If `storage.type` is `postgres`, `storage.file` must be the connection URL:
```yaml
@@ -254,7 +254,7 @@ storage:
type: postgres
file: "postgres://user:password@127.0.0.1:5432/gatus?sslmode=disable"
```
See [examples/docker-compose-postgres-storage](examples/docker-compose-postgres-storage) for an example.
See [examples/docker-compose-postgres-storage](.examples/docker-compose-postgres-storage) for an example.
### Client configuration
@@ -798,29 +798,29 @@ maintenance:
## Deployment
Many examples can be found in the [examples](examples) folder, but this section will focus on the most popular ways of deploying Gatus.
Many examples can be found in the [.examples](.examples) folder, but this section will focus on the most popular ways of deploying Gatus.
### Docker
To run Gatus locally with Docker:
```
```console
docker run -p 8080:8080 --name gatus twinproduction/gatus
```
Other than using one of the examples provided in the `examples` folder, you can also try it out locally by
Other than using one of the examples provided in the [.examples](.examples) folder, you can also try it out locally by
creating a configuration file, we'll call it `config.yaml` for this example, and running the following
command:
```
```console
docker run -p 8080:8080 --mount type=bind,source="$(pwd)"/config.yaml,target=/config/config.yaml --name gatus twinproduction/gatus
```
If you're on Windows, replace `"$(pwd)"` by the absolute path to your current directory, e.g.:
```
```console
docker run -p 8080:8080 --mount type=bind,source=C:/Users/Chris/Desktop/config.yaml,target=/config/config.yaml --name gatus twinproduction/gatus
```
To build the image locally:
```
```console
docker build . -t twinproduction/gatus
```
@@ -845,7 +845,7 @@ Gatus can be deployed on Terraform by using the following module: [terraform-kub
## Running the tests
```
```console
go test ./... -mod vendor
```
+4 -7
View File
@@ -71,15 +71,15 @@ func (provider *AlertProvider) ToCustomAlertProvider(endpoint *core.Endpoint, al
"source": "%s",
"severity": "critical"
}
}`, provider.getPagerDutyIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name),
}`, provider.getIntegrationKeyForGroup(endpoint.Group), resolveKey, eventAction, message, endpoint.Name),
Headers: map[string]string{
"Content-Type": "application/json",
},
}
}
// getPagerDutyIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group
func (provider *AlertProvider) getPagerDutyIntegrationKeyForGroup(group string) string {
// getIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group
func (provider *AlertProvider) getIntegrationKeyForGroup(group string) string {
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if group == override.Group {
@@ -87,10 +87,7 @@ func (provider *AlertProvider) getPagerDutyIntegrationKeyForGroup(group string)
}
}
}
if provider.IntegrationKey != "" {
return provider.IntegrationKey
}
return ""
return provider.IntegrationKey
}
// GetDefaultAlert returns the provider's default alert configuration
@@ -161,7 +161,7 @@ func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlertAndOverride(t *tes
}
}
func TestAlertProvider_getPagerDutyIntegrationKey(t *testing.T) {
func TestAlertProvider_getIntegrationKeyForGroup(t *testing.T) {
scenarios := []struct {
Name string
Provider AlertProvider
@@ -217,7 +217,7 @@ func TestAlertProvider_getPagerDutyIntegrationKey(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
if output := scenario.Provider.getPagerDutyIntegrationKeyForGroup(scenario.InputGroup); output != scenario.ExpectedOutput {
if output := scenario.Provider.getIntegrationKeyForGroup(scenario.InputGroup); output != scenario.ExpectedOutput {
t.Errorf("expected %s, got %s", scenario.ExpectedOutput, output)
}
})
+18 -29
View File
@@ -43,28 +43,28 @@ var (
// Config is the main configuration structure
type Config struct {
// Debug Whether to enable debug logs
Debug bool `yaml:"debug"`
Debug bool `yaml:"debug,omitempty"`
// Metrics Whether to expose metrics at /metrics
Metrics bool `yaml:"metrics"`
Metrics bool `yaml:"metrics,omitempty"`
// SkipInvalidConfigUpdate Whether to make the application ignore invalid configuration
// if the configuration file is updated while the application is running
SkipInvalidConfigUpdate bool `yaml:"skip-invalid-config-update"`
SkipInvalidConfigUpdate bool `yaml:"skip-invalid-config-update,omitempty"`
// DisableMonitoringLock Whether to disable the monitoring lock
// The monitoring lock is what prevents multiple endpoints from being processed at the same time.
// Disabling this may lead to inaccurate response times
DisableMonitoringLock bool `yaml:"disable-monitoring-lock"`
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
// Security Configuration for securing access to Gatus
Security *security.Config `yaml:"security"`
Security *security.Config `yaml:"security,omitempty"`
// Alerting Configuration for alerting
Alerting *alerting.Config `yaml:"alerting"`
Alerting *alerting.Config `yaml:"alerting,omitempty"`
// Endpoints List of endpoints to monitor
Endpoints []*core.Endpoint `yaml:"endpoints"`
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
// Services List of endpoints to monitor
//
@@ -72,19 +72,19 @@ type Config struct {
// XXX: This is not a typo -- not v4.0.0, but v5.0.0 -- I want to give enough time for people to migrate
//
// Deprecated in favor of Endpoints
Services []*core.Endpoint `yaml:"services"`
Services []*core.Endpoint `yaml:"services,omitempty"`
// Storage is the configuration for how the data is stored
Storage *storage.Config `yaml:"storage"`
Storage *storage.Config `yaml:"storage,omitempty"`
// Web is the web configuration for the application
Web *web.Config `yaml:"web"`
Web *web.Config `yaml:"web,omitempty"`
// UI is the configuration for the UI
UI *ui.Config `yaml:"ui"`
UI *ui.Config `yaml:"ui,omitempty"`
// Maintenance is the configuration for creating a maintenance window in which no alerts are sent
Maintenance *maintenance.Config `yaml:"maintenance"`
Maintenance *maintenance.Config `yaml:"maintenance,omitempty"`
filePath string // path to the file from which config was loaded from
lastFileModTime time.Time // last modification time
@@ -149,12 +149,12 @@ func readConfigurationFile(fileName string) (config *Config, err error) {
return
}
// parseAndValidateConfigBytes parses a Gatus configuration file into a Config struct and validates its parameters
func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
// Expand environment variables
yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
// Parse configuration file
err = yaml.Unmarshal(yamlBytes, &config)
if err != nil {
if err = yaml.Unmarshal(yamlBytes, &config); err != nil {
return
}
if config != nil && len(config.Services) > 0 { // XXX: Remove this in v5.0.0
@@ -167,8 +167,6 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
err = ErrNoEndpointInConfig
} else {
// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
// invalid configurations
validateAlertingConfig(config.Alerting, config.Endpoints, config.Debug)
if err := validateSecurityConfig(config); err != nil {
return nil, err
@@ -197,19 +195,10 @@ func validateStorageConfig(config *Config) error {
config.Storage = &storage.Config{
Type: storage.TypeMemory,
}
}
err := storage.Initialize(config.Storage)
if err != nil {
return err
}
// Remove all EndpointStatus that represent endpoints which no longer exist in the configuration
var keys []string
for _, endpoint := range config.Endpoints {
keys = append(keys, endpoint.Key())
}
numberOfEndpointStatusesDeleted := storage.Get().DeleteAllEndpointStatusesNotInKeys(keys)
if numberOfEndpointStatusesDeleted > 0 {
log.Printf("[config][validateStorageConfig] Deleted %d endpoint statuses because their matching endpoints no longer existed", numberOfEndpointStatusesDeleted)
} else {
if err := config.Storage.ValidateAndSetDefaults(); err != nil {
return err
}
}
return nil
}
+3 -3
View File
@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/storage/store/common"
"github.com/gorilla/mux"
)
@@ -40,7 +40,7 @@ func UptimeBadge(writer http.ResponseWriter, request *http.Request) {
return
}
key := variables["key"]
uptime, err := storage.Get().GetUptimeByKey(key, from, time.Now())
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
if err != nil {
if err == common.ErrEndpointNotFound {
writer.WriteHeader(http.StatusNotFound)
@@ -79,7 +79,7 @@ func ResponseTimeBadge(writer http.ResponseWriter, request *http.Request) {
return
}
key := variables["key"]
averageResponseTime, err := storage.Get().GetAverageResponseTimeByKey(key, from, time.Now())
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
if err != nil {
if err == common.ErrEndpointNotFound {
writer.WriteHeader(http.StatusNotFound)
+2 -2
View File
@@ -9,12 +9,12 @@ import (
"github.com/TwiN/gatus/v3/config"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/watchdog"
)
func TestUptimeBadge(t *testing.T) {
defer storage.Get().Clear()
defer store.Get().Clear()
defer cache.Clear()
cfg := &config.Config{
Metrics: true,
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"sort"
"time"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/storage/store/common"
"github.com/gorilla/mux"
"github.com/wcharczuk/go-chart/v2"
@@ -42,7 +42,7 @@ func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) {
http.Error(writer, "Durations supported: 7d, 24h", http.StatusBadRequest)
return
}
hourlyAverageResponseTime, err := storage.Get().GetHourlyAverageResponseTimeByKey(vars["key"], from, time.Now())
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(vars["key"], from, time.Now())
if err != nil {
if err == common.ErrEndpointNotFound {
writer.WriteHeader(http.StatusNotFound)
+2 -2
View File
@@ -8,12 +8,12 @@ import (
"github.com/TwiN/gatus/v3/config"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/watchdog"
)
func TestResponseTimeChart(t *testing.T) {
defer storage.Get().Clear()
defer store.Get().Clear()
defer cache.Clear()
cfg := &config.Config{
Metrics: true,
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"strings"
"time"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/storage/store/common"
"github.com/TwiN/gatus/v3/storage/store/common/paging"
"github.com/TwiN/gocache"
@@ -44,7 +44,7 @@ func EndpointStatuses(writer http.ResponseWriter, r *http.Request) {
var err error
buffer := &bytes.Buffer{}
gzipWriter := gzip.NewWriter(buffer)
endpointStatuses, err := storage.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
if err != nil {
log.Printf("[handler][EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
http.Error(writer, err.Error(), http.StatusInternalServerError)
@@ -76,7 +76,7 @@ func EndpointStatuses(writer http.ResponseWriter, r *http.Request) {
func EndpointStatus(writer http.ResponseWriter, r *http.Request) {
page, pageSize := extractPageAndPageSizeFromRequest(r)
vars := mux.Vars(r)
endpointStatus, err := storage.Get().GetEndpointStatusByKey(vars["key"], paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
endpointStatus, err := store.Get().GetEndpointStatusByKey(vars["key"], paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, common.MaximumNumberOfEvents))
if err != nil {
if err == common.ErrEndpointNotFound {
http.Error(writer, err.Error(), http.StatusNotFound)
+5 -5
View File
@@ -8,7 +8,7 @@ import (
"github.com/TwiN/gatus/v3/config"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/watchdog"
)
@@ -84,7 +84,7 @@ var (
)
func TestEndpointStatus(t *testing.T) {
defer storage.Get().Clear()
defer store.Get().Clear()
defer cache.Clear()
cfg := &config.Config{
Metrics: true,
@@ -153,12 +153,12 @@ func TestEndpointStatus(t *testing.T) {
}
func TestEndpointStatuses(t *testing.T) {
defer storage.Get().Clear()
defer store.Get().Clear()
defer cache.Clear()
firstResult := &testSuccessfulResult
secondResult := &testUnsuccessfulResult
storage.Get().Insert(&testEndpoint, firstResult)
storage.Get().Insert(&testEndpoint, secondResult)
store.Get().Insert(&testEndpoint, firstResult)
store.Get().Insert(&testEndpoint, secondResult)
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
firstResult.Timestamp = time.Time{}
secondResult.Timestamp = time.Time{}
+2 -2
View File
@@ -8,12 +8,12 @@ import (
"github.com/TwiN/gatus/v3/config"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/watchdog"
)
func TestSinglePageApplication(t *testing.T) {
defer storage.Get().Clear()
defer store.Get().Clear()
defer cache.Clear()
cfg := &config.Config{
Metrics: true,
+5 -5
View File
@@ -79,19 +79,19 @@ type Endpoint struct {
Conditions []*Condition `yaml:"conditions"`
// Alerts is the alerting configuration for the endpoint in case of failure
Alerts []*alert.Alert `yaml:"alerts"`
Alerts []*alert.Alert `yaml:"alerts,omitempty"`
// ClientConfig is the configuration of the client used to communicate with the endpoint's target
ClientConfig *client.Config `yaml:"client"`
ClientConfig *client.Config `yaml:"client,omitempty"`
// UIConfig is the configuration for the UI
UIConfig *ui.Config `yaml:"ui"`
UIConfig *ui.Config `yaml:"ui,omitempty"`
// NumberOfFailuresInARow is the number of unsuccessful evaluations in a row
NumberOfFailuresInARow int
NumberOfFailuresInARow int `yaml:"-"`
// NumberOfSuccessesInARow is the number of successful evaluations in a row
NumberOfSuccessesInARow int
NumberOfSuccessesInARow int `yaml:"-"`
}
// IsEnabled returns whether the endpoint is enabled or not
+24 -3
View File
@@ -9,7 +9,7 @@ import (
"github.com/TwiN/gatus/v3/config"
"github.com/TwiN/gatus/v3/controller"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/watchdog"
)
@@ -18,6 +18,7 @@ func main() {
if err != nil {
panic(err)
}
initializeStorage(cfg)
start(cfg)
// Wait for termination signal
signalChannel := make(chan os.Signal, 1)
@@ -46,8 +47,7 @@ func stop() {
}
func save() {
err := storage.Get().Save()
if err != nil {
if err := store.Get().Save(); err != nil {
log.Println("Failed to save storage provider:", err.Error())
}
}
@@ -62,6 +62,27 @@ func loadConfiguration() (cfg *config.Config, err error) {
return
}
// initializeStorage initializes the storage provider
//
// Q: "TwiN, why are you putting this here? Wouldn't it make more sense to have this in the config?!"
// A: Yes. Yes it would make more sense to have it in the config package. But I don't want to import
// the massive SQL dependencies just because I want to import the config, so here we are.
func initializeStorage(cfg *config.Config) {
err := store.Initialize(cfg.Storage)
if err != nil {
panic(err)
}
// Remove all EndpointStatus that represent endpoints which no longer exist in the configuration
var keys []string
for _, endpoint := range cfg.Endpoints {
keys = append(keys, endpoint.Key())
}
numberOfEndpointStatusesDeleted := store.Get().DeleteAllEndpointStatusesNotInKeys(keys)
if numberOfEndpointStatusesDeleted > 0 {
log.Printf("[config][validateStorageConfig] Deleted %d endpoint statuses because their matching endpoints no longer existed", numberOfEndpointStatusesDeleted)
}
}
func listenToConfigurationFileChanges(cfg *config.Config) {
for {
time.Sleep(30 * time.Second)
+14
View File
@@ -1,5 +1,11 @@
package storage
import "errors"
var (
ErrSQLStorageRequiresFile = errors.New("sql storage requires a non-empty file to be defined")
)
// Config is the configuration for storage
type Config struct {
// File is the path of the file to use for persistence
@@ -12,3 +18,11 @@ type Config struct {
// If blank, uses the default in-memory store
Type Type `yaml:"type"`
}
// ValidateAndSetDefaults validates the configuration and sets the default values (if applicable)
func (c *Config) ValidateAndSetDefaults() error {
if (c.Type == TypePostgres || c.Type == TypeSQLite) && len(c.File) == 0 {
return ErrSQLStorageRequiresFile
}
return nil
}
-91
View File
@@ -1,91 +0,0 @@
package storage
import (
"context"
"log"
"time"
"github.com/TwiN/gatus/v3/storage/store"
"github.com/TwiN/gatus/v3/storage/store/memory"
"github.com/TwiN/gatus/v3/storage/store/sql"
)
var (
provider store.Store
// initialized keeps track of whether the storage provider was initialized
// Because store.Store is an interface, a nil check wouldn't be sufficient, so instead of doing reflection
// every single time Get is called, we'll just lazily keep track of its existence through this variable
initialized bool
ctx context.Context
cancelFunc context.CancelFunc
)
// Get retrieves the storage provider
func Get() store.Store {
if !initialized {
log.Println("[storage][Get] Provider requested before it was initialized, automatically initializing")
err := Initialize(nil)
if err != nil {
panic("failed to automatically initialize store: " + err.Error())
}
}
return provider
}
// Initialize instantiates the storage provider based on the Config provider
func Initialize(cfg *Config) error {
initialized = true
var err error
if cancelFunc != nil {
// Stop the active autoSaveStore task, if there's already one
cancelFunc()
}
if cfg == nil {
cfg = &Config{}
}
if len(cfg.File) == 0 && cfg.Type != TypePostgres {
log.Printf("[storage][Initialize] Creating storage provider with type=%s and file=%s", cfg.Type, cfg.File)
} else {
log.Printf("[storage][Initialize] Creating storage provider with type=%s", cfg.Type)
}
ctx, cancelFunc = context.WithCancel(context.Background())
switch cfg.Type {
case TypeSQLite, TypePostgres:
provider, err = sql.NewStore(string(cfg.Type), cfg.File)
if err != nil {
return err
}
case TypeMemory:
fallthrough
default:
if len(cfg.File) > 0 {
provider, err = memory.NewStore(cfg.File)
if err != nil {
return err
}
go autoSaveStore(ctx, provider, 7*time.Minute)
} else {
provider, _ = memory.NewStore("")
}
}
return nil
}
// autoSaveStore automatically calls the Save function of the provider at every interval
func autoSaveStore(ctx context.Context, provider store.Store, interval time.Duration) {
for {
select {
case <-ctx.Done():
log.Printf("[storage][autoSaveStore] Stopping active job")
return
case <-time.After(interval):
log.Printf("[storage][autoSaveStore] Saving")
err := provider.Save()
if err != nil {
log.Println("[storage][autoSaveStore] Save failed:", err.Error())
}
}
}
}
-94
View File
@@ -1,94 +0,0 @@
package storage
import (
"testing"
"time"
"github.com/TwiN/gatus/v3/storage/store/sql"
)
func TestGet(t *testing.T) {
store := Get()
if store == nil {
t.Error("store should've been automatically initialized")
}
}
func TestInitialize(t *testing.T) {
type Scenario struct {
Name string
Cfg *Config
ExpectedErr error
}
scenarios := []Scenario{
{
Name: "nil",
Cfg: nil,
ExpectedErr: nil,
},
{
Name: "blank",
Cfg: &Config{},
ExpectedErr: nil,
},
{
Name: "memory-no-file",
Cfg: &Config{Type: TypeMemory},
ExpectedErr: nil,
},
{
Name: "memory-with-file",
Cfg: &Config{Type: TypeMemory, File: t.TempDir() + "/TestInitialize_memory-with-file.db"},
ExpectedErr: nil,
},
{
Name: "sqlite-no-file",
Cfg: &Config{Type: TypeSQLite},
ExpectedErr: sql.ErrFilePathNotSpecified,
},
{
Name: "sqlite-with-file",
Cfg: &Config{Type: TypeSQLite, File: t.TempDir() + "/TestInitialize_sqlite-with-file.db"},
ExpectedErr: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
err := Initialize(scenario.Cfg)
if err != scenario.ExpectedErr {
t.Errorf("expected %v, got %v", scenario.ExpectedErr, err)
}
if err != nil {
return
}
if cancelFunc == nil {
t.Error("cancelFunc shouldn't have been nil")
}
if ctx == nil {
t.Error("ctx shouldn't have been nil")
}
if provider == nil {
t.Fatal("provider shouldn't have been nit")
}
provider.Close()
// Try to initialize it again
err = Initialize(scenario.Cfg)
if err != scenario.ExpectedErr {
t.Errorf("expected %v, got %v", scenario.ExpectedErr, err)
return
}
provider.Close()
})
}
}
func TestAutoSave(t *testing.T) {
file := t.TempDir() + "/TestAutoSave.db"
if err := Initialize(&Config{File: file}); err != nil {
t.Fatal("shouldn't have returned an error")
}
go autoSaveStore(ctx, provider, 3*time.Millisecond)
time.Sleep(15 * time.Millisecond)
cancelFunc()
time.Sleep(50 * time.Millisecond)
}
+18
View File
@@ -2,7 +2,11 @@ package memory
import (
"encoding/gob"
"io/fs"
"io/ioutil"
"log"
"sort"
"strings"
"sync"
"time"
@@ -40,6 +44,20 @@ func NewStore(file string) (*Store, error) {
if len(file) > 0 {
_, err := store.cache.ReadFromFile(file)
if err != nil {
// XXX: Remove the block below in v4.0.0
if data, err2 := ioutil.ReadFile(file); err2 == nil {
isFromOldVersion := strings.Contains(string(data), "*core.ServiceStatus")
if isFromOldVersion {
log.Println("WARNING: Couldn't read file due to recent change in v3.3.0, see https://github.com/TwiN/gatus/issues/191")
log.Println("WARNING: Will automatically rename old file to " + file + ".old and overwrite the current file")
if err = ioutil.WriteFile(file+".old", data, fs.ModePerm); err != nil {
log.Println("WARNING: Tried my best to keep the old file, but it wasn't enough. Sorry, your file will be overwritten :(")
}
// Return the store regardless of whether there was an error or not
return store, nil
}
}
// XXX: Remove the block above in v4.0.0
return nil, err
}
}
+82
View File
@@ -1,9 +1,12 @@
package store
import (
"context"
"log"
"time"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store/common/paging"
"github.com/TwiN/gatus/v3/storage/store/memory"
"github.com/TwiN/gatus/v3/storage/store/sql"
@@ -56,3 +59,82 @@ var (
_ Store = (*memory.Store)(nil)
_ Store = (*sql.Store)(nil)
)
var (
store Store
// initialized keeps track of whether the storage provider was initialized
// Because store.Store is an interface, a nil check wouldn't be sufficient, so instead of doing reflection
// every single time Get is called, we'll just lazily keep track of its existence through this variable
initialized bool
ctx context.Context
cancelFunc context.CancelFunc
)
func Get() Store {
if !initialized {
log.Println("[store][Get] Provider requested before it was initialized, automatically initializing")
err := Initialize(nil)
if err != nil {
panic("failed to automatically initialize store: " + err.Error())
}
}
return store
}
// Initialize instantiates the storage provider based on the Config provider
func Initialize(cfg *storage.Config) error {
initialized = true
var err error
if cancelFunc != nil {
// Stop the active autoSave task, if there's already one
cancelFunc()
}
if cfg == nil {
cfg = &storage.Config{}
}
if len(cfg.File) == 0 && cfg.Type != storage.TypePostgres {
log.Printf("[store][Initialize] Creating storage provider with type=%s and file=%s", cfg.Type, cfg.File)
} else {
log.Printf("[store][Initialize] Creating storage provider with type=%s", cfg.Type)
}
ctx, cancelFunc = context.WithCancel(context.Background())
switch cfg.Type {
case storage.TypeSQLite, storage.TypePostgres:
store, err = sql.NewStore(string(cfg.Type), cfg.File)
if err != nil {
return err
}
case storage.TypeMemory:
fallthrough
default:
if len(cfg.File) > 0 {
store, err = memory.NewStore(cfg.File)
if err != nil {
return err
}
go autoSave(ctx, store, 7*time.Minute)
} else {
store, _ = memory.NewStore("")
}
}
return nil
}
// autoSave automatically calls the Save function of the provider at every interval
func autoSave(ctx context.Context, store Store, interval time.Duration) {
for {
select {
case <-ctx.Done():
log.Printf("[store][autoSave] Stopping active job")
return
case <-time.After(interval):
log.Printf("[store][autoSave] Saving")
err := store.Save()
if err != nil {
log.Println("[store][autoSave] Save failed:", err.Error())
}
}
}
}
+87
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store/common"
"github.com/TwiN/gatus/v3/storage/store/common/paging"
"github.com/TwiN/gatus/v3/storage/store/memory"
@@ -520,3 +521,89 @@ func TestStore_DeleteAllEndpointStatusesNotInKeys(t *testing.T) {
})
}
}
func TestGet(t *testing.T) {
store := Get()
if store == nil {
t.Error("store should've been automatically initialized")
}
}
func TestInitialize(t *testing.T) {
type Scenario struct {
Name string
Cfg *storage.Config
ExpectedErr error
}
scenarios := []Scenario{
{
Name: "nil",
Cfg: nil,
ExpectedErr: nil,
},
{
Name: "blank",
Cfg: &storage.Config{},
ExpectedErr: nil,
},
{
Name: "memory-no-file",
Cfg: &storage.Config{Type: storage.TypeMemory},
ExpectedErr: nil,
},
{
Name: "memory-with-file",
Cfg: &storage.Config{Type: storage.TypeMemory, File: t.TempDir() + "/TestInitialize_memory-with-file.db"},
ExpectedErr: nil,
},
{
Name: "sqlite-no-file",
Cfg: &storage.Config{Type: storage.TypeSQLite},
ExpectedErr: sql.ErrFilePathNotSpecified,
},
{
Name: "sqlite-with-file",
Cfg: &storage.Config{Type: storage.TypeSQLite, File: t.TempDir() + "/TestInitialize_sqlite-with-file.db"},
ExpectedErr: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
err := Initialize(scenario.Cfg)
if err != scenario.ExpectedErr {
t.Errorf("expected %v, got %v", scenario.ExpectedErr, err)
}
if err != nil {
return
}
if cancelFunc == nil {
t.Error("cancelFunc shouldn't have been nil")
}
if ctx == nil {
t.Error("ctx shouldn't have been nil")
}
if store == nil {
t.Fatal("provider shouldn't have been nit")
}
store.Close()
// Try to initialize it again
err = Initialize(scenario.Cfg)
if err != scenario.ExpectedErr {
t.Errorf("expected %v, got %v", scenario.ExpectedErr, err)
return
}
store.Close()
})
}
}
func TestAutoSave(t *testing.T) {
file := t.TempDir() + "/TestAutoSave.db"
if err := Initialize(&storage.Config{File: file}); err != nil {
t.Fatal("shouldn't have returned an error")
}
go autoSave(ctx, store, 3*time.Millisecond)
time.Sleep(15 * time.Millisecond)
cancelFunc()
time.Sleep(50 * time.Millisecond)
}
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"github.com/TwiN/gatus/v3/config/maintenance"
"github.com/TwiN/gatus/v3/core"
"github.com/TwiN/gatus/v3/metric"
"github.com/TwiN/gatus/v3/storage"
"github.com/TwiN/gatus/v3/storage/store"
)
var (
@@ -29,13 +29,13 @@ func Monitor(cfg *config.Config) {
for _, endpoint := range cfg.Endpoints {
if endpoint.IsEnabled() {
// To prevent multiple requests from running at the same time, we'll wait for a little before each iteration
time.Sleep(1111 * time.Millisecond)
time.Sleep(777 * time.Millisecond)
go monitor(endpoint, cfg.Alerting, cfg.Maintenance, cfg.DisableMonitoringLock, cfg.Metrics, cfg.Debug, ctx)
}
}
}
// monitor monitors a single endpoint in a loop
// monitor a single endpoint in a loop
func monitor(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenanceConfig *maintenance.Config, disableMonitoringLock, enabledMetrics, debug bool, ctx context.Context) {
// Run it immediately on start
execute(endpoint, alertingConfig, maintenanceConfig, disableMonitoringLock, enabledMetrics, debug)
@@ -88,7 +88,7 @@ func execute(endpoint *core.Endpoint, alertingConfig *alerting.Config, maintenan
// UpdateEndpointStatuses updates the slice of endpoint statuses
func UpdateEndpointStatuses(endpoint *core.Endpoint, result *core.Result) {
if err := storage.Get().Insert(endpoint, result); err != nil {
if err := store.Get().Insert(endpoint, result); err != nil {
log.Println("[watchdog][UpdateEndpointStatuses] Failed to insert data in storage:", err.Error())
}
}