diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed455b6b..52e9c8a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,9 +24,9 @@ jobs: run: go build -mod vendor - name: Test # We're using "sudo" because one of the tests leverages ping, which requires super-user privileges. - # As for the "PATH=$PATH", we need it to use the same "go" executable that was configured by the "Set - # up Go" step (otherwise, it'd use sudo's "go" executable) - run: sudo "PATH=$PATH" go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic + # As for the 'env "PATH=$PATH" "GOROOT=$GOROOT"', we need it to use the same "go" executable that + # was configured by the "Set up Go 1.15" step (otherwise, it'd use sudo's "go" executable) + run: sudo env "PATH=$PATH" "GOROOT=$GOROOT" go test -mod vendor ./... -race -coverprofile=coverage.txt -covermode=atomic - name: Codecov uses: codecov/codecov-action@v1.0.14 with: diff --git a/README.md b/README.md index 78b603f7..ad9f42e0 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ Note that you can also add environment variables in the configuration file (i.e. |:---------------------------------------- |:----------------------------------------------------------------------------- |:-------------- | | `debug` | Whether to enable debug logs | `false` | | `metrics` | Whether to expose metrics at /metrics | `false` | +| `storage` | Storage configuration | `{}` | +| `storage.file` | File to persist the data in. If not set, storage is in-memory only. | `""` | | `services` | List of services to monitor | Required `[]` | | `services[].name` | Name of the service. Can be anything. | Required `""` | | `services[].group` | Group name. Used to group multiple services together on the dashboard. See [Service groups](#service-groups). | `""` | diff --git a/config/config.go b/config/config.go index 33cb256d..e5174bf9 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,8 @@ import ( "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/k8s" "github.com/TwinProduction/gatus/security" + "github.com/TwinProduction/gatus/storage" + "github.com/TwinProduction/gatus/util" "gopkg.in/yaml.v2" ) @@ -71,6 +73,9 @@ type Config struct { // Kubernetes is the Kubernetes configuration Kubernetes *k8s.Config `yaml:"kubernetes"` + // Storage is the configuration for how the data is stored + Storage *storage.Config `yaml:"storage"` + // Web is the configuration for the web listener Web *WebConfig `yaml:"web"` } @@ -144,10 +149,30 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) { validateServicesConfig(config) validateKubernetesConfig(config) validateWebConfig(config) + validateStorageConfig(config) } return } +func validateStorageConfig(config *Config) { + if config.Storage == nil { + config.Storage = &storage.Config{} + } + err := storage.Initialize(config.Storage) + if err != nil { + panic(err) + } + // Remove all ServiceStatus that represent services which no longer exist in the configuration + var keys []string + for _, service := range config.Services { + keys = append(keys, util.ConvertGroupAndServiceToKey(service.Group, service.Name)) + } + numberOfServiceStatusesDeleted := storage.Get().DeleteAllServiceStatusesNotInKeys(keys) + if numberOfServiceStatusesDeleted > 0 { + log.Printf("[config][validateStorageConfig] Deleted %d service statuses because their matching services no longer existed", numberOfServiceStatusesDeleted) + } +} + func validateWebConfig(config *Config) { if config.Web == nil { config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort} diff --git a/config/config_test.go b/config/config_test.go index 5b9fb8ef..5f5f295a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -42,7 +42,10 @@ func TestLoadDefaultConfigurationFile(t *testing.T) { } func TestParseAndValidateConfigBytes(t *testing.T) { - config, err := parseAndValidateConfigBytes([]byte(` + file := t.TempDir() + "/test.db" + config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` +storage: + file: %s services: - name: twinnation url: https://twinnation.org/health @@ -54,9 +57,9 @@ services: conditions: - "[STATUS] != 400" - "[STATUS] != 500" -`)) +`, file))) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -99,7 +102,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -132,7 +135,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -167,7 +170,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -201,7 +204,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -250,7 +253,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -288,7 +291,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -332,7 +335,6 @@ badconfig: func TestParseAndValidateConfigBytesWithAlerting(t *testing.T) { config, err := parseAndValidateConfigBytes([]byte(` debug: true - alerting: slack: webhook-url: "http://example.com" @@ -359,7 +361,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -452,7 +454,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -486,7 +488,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -525,7 +527,7 @@ services: - "[STATUS] == 200" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -578,7 +580,7 @@ services: - "[STATUS] == 200" `, expectedUsername, expectedPasswordHash))) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") @@ -645,7 +647,7 @@ kubernetes: target-path: "/health" `)) if err != nil { - t.Error("No error should've been returned") + t.Error("expected no error, got", err.Error()) } if config == nil { t.Fatal("Config shouldn't have been nil") diff --git a/controller/controller.go b/controller/controller.go index 815e2f6e..8c65f124 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -55,7 +55,7 @@ func Handle() { WriteTimeout: 15 * time.Second, IdleTimeout: 15 * time.Second, } - log.Println("[controller][Handle] Listening on" + cfg.Web.SocketAddress()) + log.Println("[controller][Handle] Listening on " + cfg.Web.SocketAddress()) if os.Getenv("ROUTER_TEST") == "true" { return } @@ -140,10 +140,11 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { } data := map[string]interface{}{ "serviceStatus": serviceStatus, - // This is my lazy way of exposing events even though they're not visible from the json annotation - // present in ServiceStatus. We do this because creating a separate object for each endpoints - // would be wasteful (one with and one without Events) + // The following fields, while present on core.ServiceStatus, are annotated to remain hidden so that we can + // expose only the necessary data on /api/v1/statuses. + // Since the /api/v1/statuses/{key} endpoint does need this data, however, we explicitly expose it here "events": serviceStatus.Events, + "uptime": serviceStatus.Uptime, } output, err := json.Marshal(data) if err != nil { diff --git a/controller/controller_test.go b/controller/controller_test.go index 8f7dfea7..e07975b4 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -32,97 +32,97 @@ func TestCreateRouter(t *testing.T) { watchdog.UpdateServiceStatuses(cfg.Services[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()}) router := CreateRouter(cfg) type Scenario struct { - Description string + Name string Path string ExpectedCode int Gzip bool } scenarios := []Scenario{ { - Description: "health", + Name: "health", Path: "/health", ExpectedCode: http.StatusOK, }, { - Description: "metrics", + Name: "metrics", Path: "/metrics", ExpectedCode: http.StatusOK, }, { - Description: "badges-1h", + Name: "badges-1h", Path: "/api/v1/badges/uptime/1h/core_frontend.svg", ExpectedCode: http.StatusOK, }, { - Description: "badges-24h", + Name: "badges-24h", Path: "/api/v1/badges/uptime/24h/core_backend.svg", ExpectedCode: http.StatusOK, }, { - Description: "badges-7d", + Name: "badges-7d", Path: "/api/v1/badges/uptime/7d/core_frontend.svg", ExpectedCode: http.StatusOK, }, { - Description: "badges-with-invalid-duration", + Name: "badges-with-invalid-duration", Path: "/api/v1/badges/uptime/3d/core_backend.svg", ExpectedCode: http.StatusBadRequest, }, { - Description: "badges-for-invalid-key", + Name: "badges-for-invalid-key", Path: "/api/v1/badges/uptime/7d/invalid_key.svg", ExpectedCode: http.StatusNotFound, }, { - Description: "service-statuses", + Name: "service-statuses", Path: "/api/v1/statuses", ExpectedCode: http.StatusOK, }, { - Description: "service-statuses-gzip", + Name: "service-statuses-gzip", Path: "/api/v1/statuses", ExpectedCode: http.StatusOK, Gzip: true, }, { - Description: "service-status", + Name: "service-status", Path: "/api/v1/statuses/core_frontend", ExpectedCode: http.StatusOK, }, { - Description: "service-status-gzip", + Name: "service-status-gzip", Path: "/api/v1/statuses/core_frontend", ExpectedCode: http.StatusOK, Gzip: true, }, { - Description: "service-status-for-invalid-key", + Name: "service-status-for-invalid-key", Path: "/api/v1/statuses/invalid_key", ExpectedCode: http.StatusNotFound, }, { - Description: "favicon", + Name: "favicon", Path: "/favicon.ico", ExpectedCode: http.StatusOK, }, { - Description: "frontend-home", + Name: "frontend-home", Path: "/", ExpectedCode: http.StatusOK, }, { - Description: "frontend-assets", + Name: "frontend-assets", Path: "/js/app.js", ExpectedCode: http.StatusOK, }, { - Description: "frontend-service", + Name: "frontend-service", Path: "/services/core_frontend", ExpectedCode: http.StatusOK, }, } for _, scenario := range scenarios { - t.Run(scenario.Description, func(t *testing.T) { + t.Run(scenario.Name, func(t *testing.T) { request, _ := http.NewRequest("GET", scenario.Path, nil) if scenario.Gzip { request.Header.Set("Accept-Encoding", "gzip") diff --git a/core/service-status.go b/core/service-status.go index d6ec34f1..29fee39b 100644 --- a/core/service-status.go +++ b/core/service-status.go @@ -22,13 +22,17 @@ type ServiceStatus struct { // Events is a list of events // - // We don't expose this through JSON, because the main dashboard doesn't need to have these events. + // We don't expose this through JSON, because the main dashboard doesn't need to have this data. // However, the detailed service page does leverage this by including it to a map that will be // marshalled alongside the ServiceStatus. Events []*Event `json:"-"` // Uptime information on the service's uptime - Uptime *Uptime `json:"uptime"` + // + // We don't expose this through JSON, because the main dashboard doesn't need to have this data. + // However, the detailed service page does leverage this by including it to a map that will be + // marshalled alongside the ServiceStatus. + Uptime *Uptime `json:"-"` } // NewServiceStatus creates a new ServiceStatus diff --git a/core/uptime.go b/core/uptime.go index d20dfded..603907b7 100644 --- a/core/uptime.go +++ b/core/uptime.go @@ -25,15 +25,20 @@ type Uptime struct { // LastHour is the uptime percentage over the past hour LastHour float64 `json:"1h"` - successCountPerHour map[string]uint64 - totalCountPerHour map[string]uint64 + // SuccessCountPerHour is a map containing the number of successes per hour, per timestamp following the + // custom RFC3339WithoutMinutesAndSeconds format + SuccessCountPerHour map[string]uint64 `json:"-"` + + // TotalCountPerHour is a map containing the total number of checks per hour, per timestamp following the + // custom RFC3339WithoutMinutesAndSeconds format + TotalCountPerHour map[string]uint64 `json:"-"` } // NewUptime creates a new Uptime func NewUptime() *Uptime { return &Uptime{ - successCountPerHour: make(map[string]uint64), - totalCountPerHour: make(map[string]uint64), + SuccessCountPerHour: make(map[string]uint64), + TotalCountPerHour: make(map[string]uint64), } } @@ -42,16 +47,16 @@ func NewUptime() *Uptime { func (uptime *Uptime) ProcessResult(result *Result) { timestampDateWithHour := result.Timestamp.Format(RFC3339WithoutMinutesAndSeconds) if result.Success { - uptime.successCountPerHour[timestampDateWithHour]++ + uptime.SuccessCountPerHour[timestampDateWithHour]++ } - uptime.totalCountPerHour[timestampDateWithHour]++ + uptime.TotalCountPerHour[timestampDateWithHour]++ // Clean up only when we're starting to have too many useless keys // Note that this is only triggered when there are more entries than there should be after // 10 days, despite the fact that we are deleting everything that's older than 7 days. // This is to prevent re-iterating on every `ProcessResult` as soon as the uptime has been logged for 7 days. - if len(uptime.totalCountPerHour) > numberOfHoursInTenDays { + if len(uptime.TotalCountPerHour) > numberOfHoursInTenDays { sevenDaysAgo := time.Now().Add(-(sevenDays + time.Hour)) - for k := range uptime.totalCountPerHour { + for k := range uptime.TotalCountPerHour { dateWithHour, err := time.Parse(time.RFC3339, k) if err != nil { // This shouldn't happen, but we'll log it in case it does happen @@ -59,8 +64,8 @@ func (uptime *Uptime) ProcessResult(result *Result) { continue } if sevenDaysAgo.Unix() > dateWithHour.Unix() { - delete(uptime.totalCountPerHour, k) - delete(uptime.successCountPerHour, k) + delete(uptime.TotalCountPerHour, k) + delete(uptime.SuccessCountPerHour, k) } } } @@ -88,8 +93,8 @@ func (uptime *Uptime) recalculate() { timestamp := now.Add(-sevenDays) for now.Sub(timestamp) >= 0 { timestampDateWithHour := timestamp.Format(RFC3339WithoutMinutesAndSeconds) - successCountForTimestamp := uptime.successCountPerHour[timestampDateWithHour] - totalCountForTimestamp := uptime.totalCountPerHour[timestampDateWithHour] + successCountForTimestamp := uptime.SuccessCountPerHour[timestampDateWithHour] + totalCountForTimestamp := uptime.TotalCountPerHour[timestampDateWithHour] uptimeBrackets["7d_success"] += successCountForTimestamp uptimeBrackets["7d_total"] += totalCountForTimestamp if now.Sub(timestamp) <= 24*time.Hour { diff --git a/core/uptime_test.go b/core/uptime_test.go index 824ffcaf..817ca438 100644 --- a/core/uptime_test.go +++ b/core/uptime_test.go @@ -51,8 +51,8 @@ func TestServiceStatus_AddResultUptimeIsCleaningUpAfterItself(t *testing.T) { timestamp := now.Add(-12 * 24 * time.Hour) for timestamp.Unix() <= now.Unix() { serviceStatus.AddResult(&Result{Timestamp: timestamp, Success: true}) - if len(serviceStatus.Uptime.successCountPerHour) > numberOfHoursInTenDays { - t.Errorf("At no point in time should there be more than %d entries in serviceStatus.successCountPerHour", numberOfHoursInTenDays) + if len(serviceStatus.Uptime.SuccessCountPerHour) > numberOfHoursInTenDays { + t.Errorf("At no point in time should there be more than %d entries in serviceStatus.SuccessCountPerHour", numberOfHoursInTenDays) } //fmt.Printf("timestamp=%s; uptimeDuringLastHour=%f; timeAgo=%s\n", timestamp.Format(time.RFC3339), serviceStatus.UptimeDuringLastHour, time.Since(timestamp)) if now.Sub(timestamp) > time.Hour && serviceStatus.Uptime.LastHour != 0 { diff --git a/go.mod b/go.mod index 084191f5..ffafa681 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( cloud.google.com/go v0.74.0 // indirect - github.com/TwinProduction/gocache v1.1.0 + github.com/TwinProduction/gocache v1.2.0 github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663 github.com/google/gofuzz v1.2.0 // indirect github.com/gorilla/mux v1.8.0 diff --git a/go.sum b/go.sum index 94540c8f..81cd012e 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/TwinProduction/gocache v1.1.0 h1:mibBUyccd8kGHlm5dXhTMDOvWBK4mjNqGyOOkG8mib8= -github.com/TwinProduction/gocache v1.1.0/go.mod h1:+qH57V/K4oAcX9C7CvgJTwUX4lzfIUXQC/6XaRSOS1Y= +github.com/TwinProduction/gocache v1.2.0 h1:iZBUuri5VydxYhNkWEOZm/JzX/X2b3OZzfLrPaRWKjk= +github.com/TwinProduction/gocache v1.2.0/go.mod h1:+qH57V/K4oAcX9C7CvgJTwUX4lzfIUXQC/6XaRSOS1Y= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/storage/config.go b/storage/config.go new file mode 100644 index 00000000..8264d4cb --- /dev/null +++ b/storage/config.go @@ -0,0 +1,7 @@ +package storage + +// Config is the configuration for alerting providers +type Config struct { + // File is the path of the file to use when using file.Store + File string `yaml:"file"` +} diff --git a/storage/memory.go b/storage/memory.go deleted file mode 100644 index b1b4f6ec..00000000 --- a/storage/memory.go +++ /dev/null @@ -1,69 +0,0 @@ -package storage - -import ( - "encoding/json" - "sync" - - "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/util" -) - -// InMemoryStore implements an in-memory store -type InMemoryStore struct { - serviceStatuses map[string]*core.ServiceStatus - serviceResultsMutex sync.RWMutex -} - -// NewInMemoryStore returns an in-memory store. Note that the store acts as a singleton, so although new-ing -// up in-memory stores will give you a unique reference to a struct each time, all structs returned -// by this function will act on the same in-memory store. -func NewInMemoryStore() *InMemoryStore { - return &InMemoryStore{ - serviceStatuses: make(map[string]*core.ServiceStatus), - } -} - -// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus -func (ims *InMemoryStore) GetAllAsJSON() ([]byte, error) { - ims.serviceResultsMutex.RLock() - serviceStatuses, err := json.Marshal(ims.serviceStatuses) - ims.serviceResultsMutex.RUnlock() - return serviceStatuses, err -} - -// GetServiceStatus returns the service status for a given service name in the given group -func (ims *InMemoryStore) GetServiceStatus(groupName, serviceName string) *core.ServiceStatus { - key := util.ConvertGroupAndServiceToKey(groupName, serviceName) - ims.serviceResultsMutex.RLock() - serviceStatus := ims.serviceStatuses[key] - ims.serviceResultsMutex.RUnlock() - return serviceStatus -} - -// GetServiceStatusByKey returns the service status for a given key -func (ims *InMemoryStore) GetServiceStatusByKey(key string) *core.ServiceStatus { - ims.serviceResultsMutex.RLock() - serviceStatus := ims.serviceStatuses[key] - ims.serviceResultsMutex.RUnlock() - return serviceStatus -} - -// Insert inserts the observed result for the specified service into the in memory store -func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) { - key := util.ConvertGroupAndServiceToKey(service.Group, service.Name) - ims.serviceResultsMutex.Lock() - serviceStatus, exists := ims.serviceStatuses[key] - if !exists { - serviceStatus = core.NewServiceStatus(service) - ims.serviceStatuses[key] = serviceStatus - } - serviceStatus.AddResult(result) - ims.serviceResultsMutex.Unlock() -} - -// Clear will empty all the results from the in memory store -func (ims *InMemoryStore) Clear() { - ims.serviceResultsMutex.Lock() - ims.serviceStatuses = make(map[string]*core.ServiceStatus) - ims.serviceResultsMutex.Unlock() -} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 00000000..abd51e16 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,48 @@ +package storage + +import ( + "log" + "time" + + "github.com/TwinProduction/gatus/storage/store" + "github.com/TwinProduction/gatus/storage/store/memory" +) + +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 +) + +// 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 cfg == nil || len(cfg.File) == 0 { + log.Println("[storage][Initialize] Creating storage provider") + provider, err = memory.NewStore("") + } else { + log.Printf("[storage][Initialize] Creating storage provider with file=%s", cfg.File) + provider, err = memory.NewStore(cfg.File) + if err != nil { + return err + } + go provider.(*memory.Store).AutoSave(7 * time.Minute) + } + return nil +} diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go new file mode 100644 index 00000000..75c991c3 --- /dev/null +++ b/storage/store/memory/memory.go @@ -0,0 +1,110 @@ +package memory + +import ( + "encoding/gob" + "encoding/json" + "log" + "time" + + "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/util" + "github.com/TwinProduction/gocache" +) + +func init() { + gob.Register(&core.ServiceStatus{}) + gob.Register(&core.Uptime{}) + gob.Register(&core.Result{}) + gob.Register(&core.Event{}) +} + +// Store that leverages gocache +type Store struct { + file string + cache *gocache.Cache +} + +// NewStore creates a new store +func NewStore(file string) (*Store, error) { + store := &Store{ + file: file, + cache: gocache.NewCache().WithMaxSize(gocache.NoMaxSize), + } + if len(file) > 0 { + _, err := store.cache.ReadFromFile(file) + if err != nil { + return nil, err + } + } + return store, nil +} + +// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus +func (s *Store) GetAllAsJSON() ([]byte, error) { + return json.Marshal(s.cache.GetAll()) +} + +// GetServiceStatus returns the service status for a given service name in the given group +func (s *Store) GetServiceStatus(groupName, serviceName string) *core.ServiceStatus { + return s.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(groupName, serviceName)) +} + +// GetServiceStatusByKey returns the service status for a given key +func (s *Store) GetServiceStatusByKey(key string) *core.ServiceStatus { + serviceStatus := s.cache.GetValue(key) + if serviceStatus == nil { + return nil + } + return serviceStatus.(*core.ServiceStatus) +} + +// Insert adds the observed result for the specified service into the store +func (s *Store) Insert(service *core.Service, result *core.Result) { + key := util.ConvertGroupAndServiceToKey(service.Group, service.Name) + serviceStatus, exists := s.cache.Get(key) + if !exists { + serviceStatus = core.NewServiceStatus(service) + } + serviceStatus.(*core.ServiceStatus).AddResult(result) + s.cache.Set(key, serviceStatus) +} + +// DeleteAllServiceStatusesNotInKeys removes all ServiceStatus that are not within the keys provided +func (s *Store) DeleteAllServiceStatusesNotInKeys(keys []string) int { + var keysToDelete []string + for _, existingKey := range s.cache.GetKeysByPattern("*", 0) { + shouldDelete := true + for _, key := range keys { + if existingKey == key { + shouldDelete = false + break + } + } + if shouldDelete { + keysToDelete = append(keysToDelete, existingKey) + } + } + return s.cache.DeleteAll(keysToDelete) +} + +// Clear deletes everything from the store +func (s *Store) Clear() { + s.cache.Clear() +} + +// Save persists the cache to the store file +func (s *Store) Save() error { + return s.cache.SaveToFile(s.file) +} + +// AutoSave automatically calls the Save function at every interval +func (s *Store) AutoSave(interval time.Duration) { + for { + time.Sleep(interval) + log.Printf("[memory][AutoSave] Persisting data to file") + err := s.Save() + if err != nil { + log.Printf("[memory][AutoSave] failed to save to file=%s: %s", s.file, err.Error()) + } + } +} diff --git a/storage/memory_test.go b/storage/store/memory/memory_test.go similarity index 81% rename from storage/memory_test.go rename to storage/store/memory/memory_test.go index e1f23230..3c92d40c 100644 --- a/storage/memory_test.go +++ b/storage/store/memory/memory_test.go @@ -1,4 +1,4 @@ -package storage +package memory import ( "fmt" @@ -83,17 +83,17 @@ var ( } ) -func TestInMemoryStore_Insert(t *testing.T) { - store := NewInMemoryStore() +func TestStore_Insert(t *testing.T) { + store, _ := NewStore("") store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) - if len(store.serviceStatuses) != 1 { - t.Fatalf("expected 1 ServiceStatus, got %d", len(store.serviceStatuses)) + if store.cache.Count() != 1 { + t.Fatalf("expected 1 ServiceStatus, got %d", store.cache.Count()) } key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) - serviceStatus, exists := store.serviceStatuses[key] - if !exists { + serviceStatus := store.GetServiceStatusByKey(key) + if serviceStatus == nil { t.Fatalf("Store should've had key '%s', but didn't", key) } if len(serviceStatus.Results) != 2 { @@ -140,8 +140,8 @@ func TestInMemoryStore_Insert(t *testing.T) { } } -func TestInMemoryStore_GetServiceStatus(t *testing.T) { - store := NewInMemoryStore() +func TestStore_GetServiceStatus(t *testing.T) { + store, _ := NewStore("") store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) @@ -163,8 +163,8 @@ func TestInMemoryStore_GetServiceStatus(t *testing.T) { } } -func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { - store := NewInMemoryStore() +func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { + store, _ := NewStore("") store.Insert(&testService, &testSuccessfulResult) serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname") @@ -181,8 +181,8 @@ func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) } } -func TestInMemoryStore_GetServiceStatusByKey(t *testing.T) { - store := NewInMemoryStore() +func TestStore_GetServiceStatusByKey(t *testing.T) { + store, _ := NewStore("") store.Insert(&testService, &testSuccessfulResult) store.Insert(&testService, &testUnsuccessfulResult) @@ -204,8 +204,8 @@ func TestInMemoryStore_GetServiceStatusByKey(t *testing.T) { } } -func TestInMemoryStore_GetAllAsJSON(t *testing.T) { - store := NewInMemoryStore() +func TestStore_GetAllAsJSON(t *testing.T) { + store, _ := NewStore("") firstResult := &testSuccessfulResult secondResult := &testUnsuccessfulResult store.Insert(&testService, firstResult) @@ -217,8 +217,36 @@ func TestInMemoryStore_GetAllAsJSON(t *testing.T) { if err != nil { t.Fatal("shouldn't have returned an error, got", err.Error()) } - expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}` + expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}]}}` if string(output) != expectedOutput { t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output)) } } + +func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) { + store, _ := NewStore("") + firstService := core.Service{Name: "service-1", Group: "group"} + secondService := core.Service{Name: "service-2", Group: "group"} + result := &testSuccessfulResult + store.Insert(&firstService, result) + store.Insert(&secondService, result) + if store.cache.Count() != 2 { + t.Errorf("expected cache to have 2 keys, got %d", store.cache.Count()) + } + if store.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(firstService.Group, firstService.Name)) == nil { + t.Fatal("firstService should exist") + } + if store.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(secondService.Group, secondService.Name)) == nil { + t.Fatal("secondService should exist") + } + store.DeleteAllServiceStatusesNotInKeys([]string{util.ConvertGroupAndServiceToKey(firstService.Group, firstService.Name)}) + if store.cache.Count() != 1 { + t.Fatalf("expected cache to have 1 keys, got %d", store.cache.Count()) + } + if store.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(firstService.Group, firstService.Name)) == nil { + t.Error("secondService should've been deleted") + } + if store.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(secondService.Group, secondService.Name)) != nil { + t.Error("firstService should still exist") + } +} diff --git a/storage/store/store.go b/storage/store/store.go new file mode 100644 index 00000000..f969e227 --- /dev/null +++ b/storage/store/store.go @@ -0,0 +1,34 @@ +package store + +import ( + "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/memory" +) + +// Store is the interface that each stores should implement +type Store interface { + // GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus + GetAllAsJSON() ([]byte, error) + + // GetServiceStatus returns the service status for a given service name in the given group + GetServiceStatus(groupName, serviceName string) *core.ServiceStatus + + // GetServiceStatusByKey returns the service status for a given key + GetServiceStatusByKey(key string) *core.ServiceStatus + + // Insert adds the observed result for the specified service into the store + Insert(service *core.Service, result *core.Result) + + // DeleteAllServiceStatusesNotInKeys removes all ServiceStatus that are not within the keys provided + // + // Used to delete services that have been persisted but are no longer part of the configured services + DeleteAllServiceStatusesNotInKeys(keys []string) int + + // Clear deletes everything from the store + Clear() +} + +var ( + // Validate interface implementation on compile + _ Store = (*memory.Store)(nil) +) diff --git a/storage/store/store_bench_test.go b/storage/store/store_bench_test.go new file mode 100644 index 00000000..10fba2b2 --- /dev/null +++ b/storage/store/store_bench_test.go @@ -0,0 +1,136 @@ +package store + +import ( + "testing" + "time" + + "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage/store/memory" +) + +var ( + firstCondition = core.Condition("[STATUS] == 200") + secondCondition = core.Condition("[RESPONSE_TIME] < 500") + thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") + + timestamp = time.Now() + + testService = core.Service{ + Name: "name", + Group: "group", + URL: "https://example.org/what/ever", + Method: "GET", + Body: "body", + Interval: 30 * time.Second, + Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition}, + Alerts: nil, + Insecure: false, + NumberOfFailuresInARow: 0, + NumberOfSuccessesInARow: 0, + } + testSuccessfulResult = core.Result{ + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Body: []byte("body"), + Errors: nil, + Connected: true, + Success: true, + Timestamp: timestamp, + Duration: 150 * time.Millisecond, + CertificateExpiration: 10 * time.Hour, + ConditionResults: []*core.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: true, + }, + { + Condition: "[CERTIFICATE_EXPIRATION] < 72h", + Success: true, + }, + }, + } + testUnsuccessfulResult = core.Result{ + Hostname: "example.org", + IP: "127.0.0.1", + HTTPStatus: 200, + Body: []byte("body"), + Errors: []string{"error-1", "error-2"}, + Connected: true, + Success: false, + Timestamp: timestamp, + Duration: 750 * time.Millisecond, + CertificateExpiration: 10 * time.Hour, + ConditionResults: []*core.ConditionResult{ + { + Condition: "[STATUS] == 200", + Success: true, + }, + { + Condition: "[RESPONSE_TIME] < 500", + Success: false, + }, + { + Condition: "[CERTIFICATE_EXPIRATION] < 72h", + Success: false, + }, + }, + } +) + +func BenchmarkStore_GetAllAsJSON(b *testing.B) { + memoryStore, err := memory.NewStore("") + if err != nil { + b.Fatal("failed to create store:", err.Error()) + } + type Scenario struct { + Name string + Store Store + } + scenarios := []Scenario{ + { + Name: "memory", + Store: memoryStore, + }, + } + for _, scenario := range scenarios { + scenario.Store.Insert(&testService, &testSuccessfulResult) + scenario.Store.Insert(&testService, &testUnsuccessfulResult) + b.Run(scenario.Name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + scenario.Store.GetAllAsJSON() + } + b.ReportAllocs() + }) + } +} + +func BenchmarkStore_Insert(b *testing.B) { + memoryStore, err := memory.NewStore("") + if err != nil { + b.Fatal("failed to create store:", err.Error()) + } + type Scenario struct { + Name string + Store Store + } + scenarios := []Scenario{ + { + Name: "memory", + Store: memoryStore, + }, + } + for _, scenario := range scenarios { + b.Run(scenario.Name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + scenario.Store.Insert(&testService, &testSuccessfulResult) + scenario.Store.Insert(&testService, &testUnsuccessfulResult) + } + b.ReportAllocs() + }) + } +} diff --git a/vendor/github.com/TwinProduction/gocache/README.md b/vendor/github.com/TwinProduction/gocache/README.md index 87f0f3df..72746671 100644 --- a/vendor/github.com/TwinProduction/gocache/README.md +++ b/vendor/github.com/TwinProduction/gocache/README.md @@ -306,9 +306,9 @@ If you do not start the janitor, there will be no passive deletion of expired ke For the sake of convenience, a ready-to-go cache server is available through the `gocacheserver` package. -The reason why the server is in a different package is because `gocache` does not use -any external dependencies, but rather than re-inventing the wheel, the server -implementation uses redcon, which is a Redis server framework for Go. +The reason why the server is in a different package is because `gocache` limit its external dependencies to the strict +minimum (e.g. boltdb for persistence), however, rather than re-inventing the wheel, the server implementation uses +redcon, which is a very good Redis server framework for Go. That way, those who desire to use gocache without the server will not add any extra dependencies as long as they don't import the `gocacheserver` package. @@ -323,7 +323,7 @@ import ( func main() { cache := gocache.NewCache().WithEvictionPolicy(gocache.LeastRecentlyUsed).WithMaxSize(100000) - server := gocacheserver.NewServer(cache) + server := gocacheserver.NewServer(cache).WithPort(6379) server.Start() } ``` @@ -382,43 +382,67 @@ but if you're looking into using a library like gocache, odds are, you want more | mem | 32G DDR4 | ``` -BenchmarkMap_Get-8 95936680 26.3 ns/op -BenchmarkMap_SetSmallValue-8 7738132 424 ns/op -BenchmarkMap_SetMediumValue-8 7766346 424 ns/op -BenchmarkMap_SetLargeValue-8 7947063 435 ns/op -BenchmarkCache_Get-8 54549049 45.7 ns/op -BenchmarkCache_SetSmallValue-8 35225013 69.2 ns/op -BenchmarkCache_SetMediumValue-8 5952064 412 ns/op -BenchmarkCache_SetLargeValue-8 5969121 411 ns/op -BenchmarkCache_GetUsingLRU-8 54545949 45.6 ns/op -BenchmarkCache_SetSmallValueUsingLRU-8 5909504 419 ns/op -BenchmarkCache_SetMediumValueUsingLRU-8 5910885 418 ns/op -BenchmarkCache_SetLargeValueUsingLRU-8 5867544 419 ns/op -BenchmarkCache_SetSmallValueWhenUsingMaxMemoryUsage-8 5477178 462 ns/op -BenchmarkCache_SetMediumValueWhenUsingMaxMemoryUsage-8 5417595 475 ns/op -BenchmarkCache_SetLargeValueWhenUsingMaxMemoryUsage-8 5215263 479 ns/op -BenchmarkCache_SetSmallValueWithMaxSize10-8 10115574 236 ns/op -BenchmarkCache_SetMediumValueWithMaxSize10-8 10242792 241 ns/op -BenchmarkCache_SetLargeValueWithMaxSize10-8 10201894 241 ns/op -BenchmarkCache_SetSmallValueWithMaxSize1000-8 9637113 253 ns/op -BenchmarkCache_SetMediumValueWithMaxSize1000-8 9635175 253 ns/op -BenchmarkCache_SetLargeValueWithMaxSize1000-8 9598982 260 ns/op -BenchmarkCache_SetSmallValueWithMaxSize100000-8 7642584 337 ns/op -BenchmarkCache_SetMediumValueWithMaxSize100000-8 7407571 344 ns/op -BenchmarkCache_SetLargeValueWithMaxSize100000-8 7071360 345 ns/op -BenchmarkCache_SetSmallValueWithMaxSize100000AndLRU-8 7544194 332 ns/op -BenchmarkCache_SetMediumValueWithMaxSize100000AndLRU-8 7667004 344 ns/op -BenchmarkCache_SetLargeValueWithMaxSize100000AndLRU-8 7357642 338 ns/op -BenchmarkCache_GetAndSetMultipleConcurrently-8 1442306 1684 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndLRU-8 5117271 477 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndFIFO-8 5228412 475 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndLRU-8 5139195 529 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithRandomKeysAndNoEvictionAndFIFO-8 5251639 511 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndLRU-8 7384626 334 ns/op -BenchmarkCache_GetAndSetConcurrentlyWithFrequentEvictionsAndFIFO-8 7361985 332 ns/op -BenchmarkCache_GetConcurrentlyWithLRU-8 3370784 726 ns/op -BenchmarkCache_GetConcurrentlyWithFIFO-8 3749994 681 ns/op -BenchmarkCache_GetKeysThatDoNotExistConcurrently-8 17647344 143 ns/op +// Normal map +BenchmarkMap_Get +BenchmarkMap_Get-8 46087372 26.7 ns/op +BenchmarkMap_Set +BenchmarkMap_Set/small_value-8 3841911 389 ns/op +BenchmarkMap_Set/medium_value-8 3887074 391 ns/op +BenchmarkMap_Set/large_value-8 3921956 393 ns/op +// Gocache +BenchmarkCache_Get +BenchmarkCache_Get/FirstInFirstOut-8 27273036 46.4 ns/op +BenchmarkCache_Get/LeastRecentlyUsed-8 26648248 46.3 ns/op +BenchmarkCache_Set +BenchmarkCache_Set/FirstInFirstOut_small_value-8 2919584 405 ns/op +BenchmarkCache_Set/FirstInFirstOut_medium_value-8 2990841 391 ns/op +BenchmarkCache_Set/FirstInFirstOut_large_value-8 2970513 391 ns/op +BenchmarkCache_Set/LeastRecentlyUsed_small_value-8 2962939 402 ns/op +BenchmarkCache_Set/LeastRecentlyUsed_medium_value-8 2962963 390 ns/op +BenchmarkCache_Set/LeastRecentlyUsed_large_value-8 2962928 394 ns/op +BenchmarkCache_SetUsingMaxMemoryUsage +BenchmarkCache_SetUsingMaxMemoryUsage/small_value-8 2683356 447 ns/op +BenchmarkCache_SetUsingMaxMemoryUsage/medium_value-8 2637578 441 ns/op +BenchmarkCache_SetUsingMaxMemoryUsage/large_value-8 2672434 443 ns/op +BenchmarkCache_SetWithMaxSize +BenchmarkCache_SetWithMaxSize/100_small_value-8 4782966 252 ns/op +BenchmarkCache_SetWithMaxSize/10000_small_value-8 4067967 296 ns/op +BenchmarkCache_SetWithMaxSize/100000_small_value-8 3762055 328 ns/op +BenchmarkCache_SetWithMaxSize/100_medium_value-8 4760479 252 ns/op +BenchmarkCache_SetWithMaxSize/10000_medium_value-8 4081050 295 ns/op +BenchmarkCache_SetWithMaxSize/100000_medium_value-8 3785050 330 ns/op +BenchmarkCache_SetWithMaxSize/100_large_value-8 4732909 254 ns/op +BenchmarkCache_SetWithMaxSize/10000_large_value-8 4079533 297 ns/op +BenchmarkCache_SetWithMaxSize/100000_large_value-8 3712820 331 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU +BenchmarkCache_SetWithMaxSizeAndLRU/100_small_value-8 4761732 254 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/10000_small_value-8 4084474 296 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/100000_small_value-8 3761402 329 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/100_medium_value-8 4783075 254 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/10000_medium_value-8 4103980 296 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/100000_medium_value-8 3646023 331 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/100_large_value-8 4779025 254 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/10000_large_value-8 4096192 296 ns/op +BenchmarkCache_SetWithMaxSizeAndLRU/100000_large_value-8 3726823 331 ns/op +BenchmarkCache_GetSetMultipleConcurrent +BenchmarkCache_GetSetMultipleConcurrent-8 707142 1698 ns/op +BenchmarkCache_GetSetConcurrentWithFrequentEviction +BenchmarkCache_GetSetConcurrentWithFrequentEviction/FirstInFirstOut-8 3616256 334 ns/op +BenchmarkCache_GetSetConcurrentWithFrequentEviction/LeastRecentlyUsed-8 3636367 331 ns/op +BenchmarkCache_GetConcurrentWithLRU +BenchmarkCache_GetConcurrentWithLRU/FirstInFirstOut-8 4405557 268 ns/op +BenchmarkCache_GetConcurrentWithLRU/LeastRecentlyUsed-8 4445475 269 ns/op +BenchmarkCache_WithForceNilInterfaceOnNilPointer +BenchmarkCache_WithForceNilInterfaceOnNilPointer/true_with_nil_struct_pointer-8 6184591 191 ns/op +BenchmarkCache_WithForceNilInterfaceOnNilPointer/true-8 6090482 191 ns/op +BenchmarkCache_WithForceNilInterfaceOnNilPointer/false_with_nil_struct_pointer-8 6184629 187 ns/op +BenchmarkCache_WithForceNilInterfaceOnNilPointer/false-8 6281781 186 ns/op +(Trimmed "BenchmarkCache_" for readability) +WithForceNilInterfaceOnNilPointerWithConcurrency +WithForceNilInterfaceOnNilPointerWithConcurrency/true_with_nil_struct_pointer-8 4379564 268 ns/op +WithForceNilInterfaceOnNilPointerWithConcurrency/true-8 4379558 265 ns/op +WithForceNilInterfaceOnNilPointerWithConcurrency/false_with_nil_struct_pointer-8 4444456 261 ns/op +WithForceNilInterfaceOnNilPointerWithConcurrency/false-8 4493896 262 ns/op ``` diff --git a/vendor/github.com/TwinProduction/gocache/gocache.go b/vendor/github.com/TwinProduction/gocache/gocache.go index e3eb89af..1f22d447 100644 --- a/vendor/github.com/TwinProduction/gocache/gocache.go +++ b/vendor/github.com/TwinProduction/gocache/gocache.go @@ -170,7 +170,7 @@ func (cache *Cache) WithEvictionPolicy(policy EvictionPolicy) *Cache { // value, _ := cache.Get("key") // // the following returns true, because the interface{} was forcefully set to nil // if value == nil {} -// // the following will panic, because the value has been casted to its type +// // the following will panic, because the value has been casted to its type (which is nil) // if value.(*Struct) == nil {} // // If set to false: @@ -218,7 +218,8 @@ func (cache *Cache) Set(key string, value interface{}) { // The TTL provided must be greater than 0, or NoExpiration (-1). If a negative value that isn't -1 (NoExpiration) is // provided, the entry will not be created if the key doesn't exist func (cache *Cache) SetWithTTL(key string, value interface{}, ttl time.Duration) { - // An interface is only nil if both its value and its type are nil, however, passing a pointer + // An interface is only nil if both its value and its type are nil, however, passing a nil pointer as an interface{} + // means that the interface itself is not nil, because the interface value is nil but not the type. if cache.forceNilInterfaceOnNilPointer { if value != nil && (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) { value = nil @@ -334,6 +335,13 @@ func (cache *Cache) Get(key string) (interface{}, bool) { return entry.Value, true } +// GetValue retrieves an entry using the key passed as parameter +// Unlike Get, this function only returns the value +func (cache *Cache) GetValue(key string) interface{} { + value, _ := cache.Get(key) + return value +} + // GetByKeys retrieves multiple entries using the keys passed as parameter // All keys are returned in the map, regardless of whether they exist or not, however, entries that do not exist in the // cache will return nil, meaning that there is no way of determining whether a key genuinely has the value nil, or diff --git a/vendor/github.com/TwinProduction/gocache/policy.go b/vendor/github.com/TwinProduction/gocache/policy.go index f3c040ba..9505c91e 100644 --- a/vendor/github.com/TwinProduction/gocache/policy.go +++ b/vendor/github.com/TwinProduction/gocache/policy.go @@ -3,6 +3,30 @@ package gocache type EvictionPolicy string var ( + // LeastRecentlyUsed is an eviction policy that causes the most recently accessed cache entry to be moved to the + // head of the cache. Effectively, this causes the cache entries that have not been accessed for some time to + // gradually move closer and closer to the tail, and since the tail is the entry that gets deleted when an eviction + // is required, it allows less used cache entries to be evicted while keeping recently accessed entries at or close + // to the head. + // + // For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would + // put 3 at the head and 1 at the tail: + // 3 (head) -> 2 -> 1 (tail) + // If the cache entry 1 was then accessed, 1 would become the head and 2 the tail: + // 1 (head) -> 3 -> 2 (tail) + // If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (2) would then be evicted: + // 4 (head) -> 1 -> 3 (tail) LeastRecentlyUsed EvictionPolicy = "LeastRecentlyUsed" - FirstInFirstOut EvictionPolicy = "FirstInFirstOut" + + // FirstInFirstOut is an eviction policy that causes cache entries to be evicted in the same order that they are + // created. + // + // For instance, creating a Cache with a Cache.MaxSize of 3 and creating the entries 1, 2 and 3 in that order would + // put 3 at the head and 1 at the tail: + // 3 (head) -> 2 -> 1 (tail) + // If the cache entry 1 was then accessed, unlike with LeastRecentlyUsed, nothing would change: + // 3 (head) -> 2 -> 1 (tail) + // If a cache entry 4 was then created, because the Cache.MaxSize is 3, the tail (1) would then be evicted: + // 4 (head) -> 3 -> 2 (tail) + FirstInFirstOut EvictionPolicy = "FirstInFirstOut" ) diff --git a/vendor/modules.txt b/vendor/modules.txt index 163db83b..cb60fa79 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,7 +1,7 @@ # cloud.google.com/go v0.74.0 ## explicit cloud.google.com/go/compute/metadata -# github.com/TwinProduction/gocache v1.1.0 +# github.com/TwinProduction/gocache v1.2.0 ## explicit github.com/TwinProduction/gocache # github.com/beorn7/perks v1.0.1 diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 44cf1579..07317e2f 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -13,8 +13,6 @@ import ( ) var ( - store = storage.NewInMemoryStore() - // monitoringMutex is used to prevent multiple services from being evaluated at the same time. // Without this, conditions using response time may become inaccurate. monitoringMutex sync.Mutex @@ -22,12 +20,12 @@ var ( // GetServiceStatusesAsJSON the JSON encoding of all core.ServiceStatus recorded func GetServiceStatusesAsJSON() ([]byte, error) { - return store.GetAllAsJSON() + return storage.Get().GetAllAsJSON() } // GetUptimeByKey returns the uptime of a service based on the ServiceStatus key func GetUptimeByKey(key string) *core.Uptime { - serviceStatus := store.GetServiceStatusByKey(key) + serviceStatus := storage.Get().GetServiceStatusByKey(key) if serviceStatus == nil { return nil } @@ -36,7 +34,7 @@ func GetUptimeByKey(key string) *core.Uptime { // GetServiceStatusByKey returns the uptime of a service based on its ServiceStatus key func GetServiceStatusByKey(key string) *core.ServiceStatus { - return store.GetServiceStatusByKey(key) + return storage.Get().GetServiceStatusByKey(key) } // Monitor loops over each services and starts a goroutine to monitor each services separately @@ -88,5 +86,5 @@ func monitor(service *core.Service) { // UpdateServiceStatuses updates the slice of service statuses func UpdateServiceStatuses(service *core.Service, result *core.Result) { - store.Insert(service, result) + storage.Get().Insert(service, result) } diff --git a/web/app/src/views/Details.vue b/web/app/src/views/Details.vue index 066048e2..4ae2ac1c 100644 --- a/web/app/src/views/Details.vue +++ b/web/app/src/views/Details.vue @@ -8,26 +8,26 @@
-
+

UPTIME


- {{ prettifyUptime(serviceStatus.uptime['7d']) }} + {{ prettifyUptime(uptime['7d']) }}

Last 7 days

- {{ prettifyUptime(serviceStatus.uptime['24h']) }} + {{ prettifyUptime(uptime['24h']) }}

Last 24 hours

- {{ prettifyUptime(serviceStatus.uptime['1h']) }} + {{ prettifyUptime(uptime['1h']) }}

Last hour


BADGES

-
+
7d uptime badge
@@ -90,6 +90,7 @@ export default { .then(data => { if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) { this.serviceStatus = data.serviceStatus; + this.uptime = data.uptime; let events = []; for (let i = data.events.length-1; i >= 0; i--) { let event = data.events[i]; @@ -143,6 +144,7 @@ export default { return { serviceStatus: {}, events: [], + uptime: {"7d": 0, "24h": 0, "1h": 0}, // Since this page isn't at the root, we need to modify the server URL a bit serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL, } diff --git a/web/static/css/app.css b/web/static/css/app.css index 77259a75..fe8cbfa9 100644 --- a/web/static/css/app.css +++ b/web/static/css/app.css @@ -1,3 +1,3 @@ #social[data-v-1cbbc992]{position:fixed;right:5px;bottom:5px;padding:5px;margin:0;z-index:100}#social img[data-v-1cbbc992]{opacity:.3}#social img[data-v-1cbbc992]:hover{opacity:1}#tooltip{position:fixed;background-color:#fff;border:1px solid #d3d3d3;border-radius:4px;padding:6px;font-size:13px}#tooltip code{color:#212529;line-height:1}#tooltip .tooltip-title{font-weight:700;margin-bottom:0;display:block;margin-top:8px}#tooltip>.tooltip-title:first-child{margin-top:0}html{height:100%}body,html{background-color:#f7f9fb}#global,#results{max-width:1200px} -/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.justify-end{justify-content:flex-end}.flex-1{flex:1 1 0%}.float-right{float:right}.font-light{font-weight:300}.font-medium{font-weight:500}.font-bold{font-weight:700}.text-sm{font-size:.875rem;line-height:1.25rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-auto{margin-top:auto;margin-bottom:auto}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mt-6{margin-top:1.5rem}.mt-12{margin-top:3rem}.object-scale-down{object-fit:scale-down}.opacity-75{opacity:.75}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.p-3{padding:.75rem}.p-5{padding:1.25rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.pt-2{padding-top:.5rem}.pb-2{padding-bottom:.5rem}.pl-10{padding-left:2.5rem}.pb-12{padding-bottom:3rem}.absolute{position:absolute}.relative{position:relative}.top-2{top:.5rem}.left-2{left:.5rem}.bottom-12{bottom:3rem}*{--tw-shadow:0 0 transparent}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06)}.hover\:shadow-lg:hover,.shadow{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-black{--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgba(251,191,36,var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgba(30,64,175,var(--tw-text-opacity))}.hover\:underline:hover{text-decoration:underline}.invisible{visibility:hidden}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-3\/4{width:75%}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.bg-success{background-color:#28a745}.text-monospace{font-family:Consolas,monospace}@media (min-width:1024px){.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (min-width:1280px){.xl\:rounded{border-radius:.25rem}.xl\:border{border-width:1px}.xl\:text-2xl{font-size:1.5rem;line-height:2rem}.xl\:text-3xl{font-size:1.875rem;line-height:2.25rem}.xl\:text-5xl{font-size:3rem;line-height:1}.xl\:my-5{margin-top:1.25rem;margin-bottom:1.25rem}.xl\:pb-5{padding-bottom:1.25rem}.xl\:shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}}#settings[data-v-31e7281e]{position:fixed;left:5px;bottom:5px;padding:5px}#settings select[data-v-31e7281e]:focus{box-shadow:none}.service:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.service:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-bottom-width:3px;border-color:#dee2e6;border-style:solid}.status{cursor:pointer;transition:all .5s ease-in-out;overflow-x:hidden;color:#fff;width:5%;font-size:75%;font-weight:700;text-align:center}.status:hover{opacity:.7;transition:opacity .1s ease-in-out;color:#000}.status-over-time{overflow:auto}.status-over-time>span:not(:first-child){margin-left:2px}.status-time-ago{color:#6a737d;opacity:.5;margin-top:5px}.status-min-max-ms{overflow-x:hidden}.service-group{cursor:pointer;user-select:none}.service-group h5:hover{color:#1b1e21!important}.service-group-content>div:first-child{border-top-left-radius:0;border-top-right-radius:0}.service[data-v-d47609f0]{border-radius:3px;border-bottom-width:3px;border-color:#dee2e6;border-style:solid} \ No newline at end of file +/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgba(254,226,226,var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220,38,38,var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgba(209,250,229,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239,68,68,var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgba(5,150,105,var(--tw-border-opacity))}.rounded-none{border-radius:0}.rounded{border-radius:.25rem}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-l{border-left-width:1px}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.justify-end{justify-content:flex-end}.flex-1{flex:1 1 0%}.float-right{float:right}.font-light{font-weight:300}.font-medium{font-weight:500}.font-bold{font-weight:700}.text-sm{font-size:.875rem;line-height:1.25rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-auto{margin-top:auto;margin-bottom:auto}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.mt-6{margin-top:1.5rem}.mt-12{margin-top:3rem}.object-scale-down{object-fit:scale-down}.opacity-75{opacity:.75}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.p-3{padding:.75rem}.p-5{padding:1.25rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.pt-2{padding-top:.5rem}.pb-2{padding-bottom:.5rem}.pl-10{padding-left:2.5rem}.pb-12{padding-bottom:3rem}.absolute{position:absolute}.relative{position:relative}.top-2{top:.5rem}.left-2{left:.5rem}.bottom-12{bottom:3rem}*{--tw-shadow:0 0 transparent}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06)}.hover\:shadow-lg:hover,.shadow{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-black{--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgba(251,191,36,var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.hover\:text-blue-800:hover{--tw-text-opacity:1;color:rgba(30,64,175,var(--tw-text-opacity))}.hover\:underline:hover{text-decoration:underline}.invisible{visibility:hidden}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-3\/4{width:75%}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.bg-success{background-color:#28a745}.text-monospace{font-family:Consolas,monospace}@media (min-width:1024px){.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}}@media (min-width:1280px){.xl\:rounded{border-radius:.25rem}.xl\:border{border-width:1px}.xl\:text-2xl{font-size:1.5rem;line-height:2rem}.xl\:text-3xl{font-size:1.875rem;line-height:2.25rem}.xl\:text-5xl{font-size:3rem;line-height:1}.xl\:my-5{margin-top:1.25rem;margin-bottom:1.25rem}.xl\:pb-5{padding-bottom:1.25rem}.xl\:shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}}#settings[data-v-31e7281e]{position:fixed;left:5px;bottom:5px;padding:5px}#settings select[data-v-31e7281e]:focus{box-shadow:none}.service:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.service:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-bottom-width:3px;border-color:#dee2e6;border-style:solid}.status{cursor:pointer;transition:all .5s ease-in-out;overflow-x:hidden;color:#fff;width:5%;font-size:75%;font-weight:700;text-align:center}.status:hover{opacity:.7;transition:opacity .1s ease-in-out;color:#000}.status-over-time{overflow:auto}.status-over-time>span:not(:first-child){margin-left:2px}.status-time-ago{color:#6a737d;opacity:.5;margin-top:5px}.status-min-max-ms{overflow-x:hidden}.service-group{cursor:pointer;user-select:none}.service-group h5:hover{color:#1b1e21!important}.service-group-content>div:first-child{border-top-left-radius:0;border-top-right-radius:0}.service[data-v-484ca9f8]{border-radius:3px;border-bottom-width:3px;border-color:#dee2e6;border-style:solid} \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js index 6a94ffd9..f2390366 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1 +1 @@ -(function(e){function t(t){for(var s,c,o=t[0],a=t[1],l=t[2],h=0,d=[];h/g,">").replace(/"/g,""").replace(/'/g,"'")},reposition:function(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){var e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,n=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+n.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-n.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+n.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(n.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated:function(){this.reposition()},created:function(){this.reposition()},data:function(){return{hidden:!0,top:0,left:0}}};n("1dd9");C.render=S;var R=C,E={name:"App",components:{Social:p,Tooltip:R},methods:{showTooltip:function(e,t){this.tooltip={result:e,event:t}}},data:function(){return{tooltip:{}}}};n("3d28");E.render=a;var D=E,H=(n("a766"),n("6c02"));function P(e,t,n,r,i,c){var o=Object(s["y"])("Services"),a=Object(s["y"])("Settings");return Object(s["q"])(),Object(s["d"])(s["a"],null,[Object(s["h"])(o,{serviceStatuses:i.serviceStatuses,showStatusOnHover:!0,onShowTooltip:c.showTooltip},null,8,["serviceStatuses","onShowTooltip"]),Object(s["h"])(a,{onRefreshData:c.fetchData},null,8,["onRefreshData"])],64)}n("d3b7");var k=Object(s["E"])("data-v-31e7281e");Object(s["t"])("data-v-31e7281e");var U={id:"settings"},M={class:"flex bg-gray-200 rounded border border-gray-300 shadow"},Q=Object(s["h"])("div",{class:"text-sm text-gray-600 rounded-xl py-1 px-2"}," ↻ ",-1),q=Object(s["f"])('',6);Object(s["r"])();var X=k((function(e,t,n,r,i,c){return Object(s["q"])(),Object(s["d"])("div",U,[Object(s["h"])("div",M,[Q,Object(s["h"])("select",{class:"text-center text-gray-500 text-sm",id:"refresh-rate",ref:"refreshInterval",onChange:t[1]||(t[1]=function(){return c.handleChangeRefreshInterval&&c.handleChangeRefreshInterval.apply(c,arguments)})},[q],544)])])})),z={name:"Settings",props:{},methods:{setRefreshInterval:function(e){var t=this;this.refreshIntervalHandler=setInterval((function(){t.refreshData()}),1e3*e)},refreshData:function(){this.$emit("refreshData")},handleChangeRefreshInterval:function(){this.refreshData(),clearInterval(this.refreshIntervalHandler),this.setRefreshInterval(this.$refs.refreshInterval.value)}},created:function(){this.setRefreshInterval(this.refreshInterval)},unmounted:function(){clearInterval(this.refreshIntervalHandler)},data:function(){return{refreshInterval:30,refreshIntervalHandler:0}}};n("49d2");z.render=X,z.__scopeId="data-v-31e7281e";var G=z,F=(n("b0c0"),{id:"results"});function K(e,t,n,r,i,c){var o=Object(s["y"])("ServiceGroup");return Object(s["q"])(),Object(s["d"])("div",F,[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(i.serviceGroups,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[Object(s["h"])(o,{services:t.services,name:t.name,onShowTooltip:c.showTooltip},null,8,["services","name","onShowTooltip"])]}))})),128))])}var Y={class:"text-monospace text-gray-400 text-xl font-medium pb-2 px-3"},J={key:0,class:"text-green-600"},Z={key:1,class:"text-yellow-400"},N={class:"float-right service-group-arrow"};function W(e,t,n,r,i,c){var o=Object(s["y"])("Service");return Object(s["q"])(),Object(s["d"])("div",{class:0===n.services.length?"mt-3":"mt-4"},["undefined"!==n.name?Object(s["x"])(e.$slots,"default",{key:0},(function(){return[Object(s["h"])("div",{class:"service-group pt-2 border",onClick:t[1]||(t[1]=function(){return c.toggleGroup&&c.toggleGroup.apply(c,arguments)})},[Object(s["h"])("h5",Y,[i.healthy?(Object(s["q"])(),Object(s["d"])("span",J,"✓")):(Object(s["q"])(),Object(s["d"])("span",Z,"~")),Object(s["g"])(" "+Object(s["A"])(n.name)+" ",1),Object(s["h"])("span",N,Object(s["A"])(i.collapsed?"▼":"▲"),1)])])]})):Object(s["e"])("",!0),i.collapsed?Object(s["e"])("",!0):(Object(s["q"])(),Object(s["d"])("div",{key:1,class:"undefined"===n.name?"":"service-group-content"},[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.services,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[Object(s["h"])(o,{data:t,onShowTooltip:c.showTooltip,maximumNumberOfResults:20},null,8,["data","onShowTooltip"])]}))})),128))],2))],2)}var L={key:0,class:"service px-3 py-3 border-l border-r border-t rounded-none hover:bg-gray-100"},V={class:"flex flex-wrap mb-2"},$={class:"w-3/4"},_={class:"text-gray-500 font-light"},ee={class:"w-1/4 text-right"},te={class:"font-light status-min-max-ms"},ne={class:"status-over-time flex flex-row"},se=Object(s["h"])("span",{class:"status rounded border border-dashed"},null,-1),re={class:"flex flex-wrap status-time-ago"},ie={class:"w-1/2"},ce={class:"w-1/2 text-right"};function oe(e,t,n,r,i,c){var o=Object(s["y"])("router-link");return n.data&&n.data.results&&n.data.results.length?(Object(s["q"])(),Object(s["d"])("div",L,[Object(s["h"])("div",V,[Object(s["h"])("div",$,[Object(s["h"])(o,{to:c.generatePath(),class:"font-bold hover:text-blue-800 hover:underline",title:"View detailed service health"},{default:Object(s["D"])((function(){return[Object(s["g"])(Object(s["A"])(n.data.name),1)]})),_:1},8,["to"]),Object(s["h"])("span",_," | "+Object(s["A"])(n.data.results[n.data.results.length-1].hostname),1)]),Object(s["h"])("div",ee,[Object(s["h"])("span",te,Object(s["A"])(i.minResponseTime===i.maxResponseTime?i.minResponseTime:i.minResponseTime+"-"+i.maxResponseTime)+"ms ",1)])]),Object(s["h"])("div",null,[Object(s["h"])("div",ne,[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.maximumNumberOfResults-n.data.results.length,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[se]}))})),128)),(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.data.results,(function(n){return Object(s["x"])(e.$slots,"default",{key:n},(function(){return[n.success?(Object(s["q"])(),Object(s["d"])("span",{key:0,class:"status rounded bg-success",onMouseenter:function(e){return c.showTooltip(n,e)},onMouseleave:t[1]||(t[1]=function(e){return c.showTooltip(null,e)})},"✓",40,["onMouseenter"])):(Object(s["q"])(),Object(s["d"])("span",{key:1,class:"status rounded bg-red-600",onMouseenter:function(e){return c.showTooltip(n,e)},onMouseleave:t[2]||(t[2]=function(e){return c.showTooltip(null,e)})},"X",40,["onMouseenter"]))]}))})),128))])]),Object(s["h"])("div",re,[Object(s["h"])("div",ie,Object(s["A"])(e.generatePrettyTimeAgo(n.data.results[0].timestamp)),1),Object(s["h"])("div",ce,Object(s["A"])(e.generatePrettyTimeAgo(n.data.results[n.data.results.length-1].timestamp)),1)])])):Object(s["e"])("",!0)}n("a9e3");var ae={methods:{generatePrettyTimeAgo:function(e){var t=(new Date).getTime()-new Date(e).getTime();if(t>36e5){var n=(t/36e5).toFixed(0);return n+" hour"+("1"!==n?"s":"")+" ago"}if(t>6e4){var s=(t/6e4).toFixed(0);return s+" minute"+("1"!==s?"s":"")+" ago"}return(t/1e3).toFixed(0)+" seconds ago"}}},le={name:"Service",props:{maximumNumberOfResults:Number,data:Object},emits:["showTooltip"],mixins:[ae],methods:{updateMinAndMaxResponseTimes:function(){var e=null,t=null;for(var n in this.data.results){var s=parseInt(this.data.results[n].duration/1e6);(null==e||e>s)&&(e=s),(null==t||t=0;s--){var r=t.events[s];if(s===t.events.length-1)"UNHEALTHY"===r.type?r.fancyText="Service is unhealthy":"HEALTHY"===r.type?r.fancyText="Service is healthy":"START"===r.type&&(r.fancyText="Monitoring started");else{var i=t.events[s+1];"HEALTHY"===r.type?r.fancyText="Service became healthy":"UNHEALTHY"===r.type?r.fancyText=i?"Service was unhealthy for "+e.prettifyTimeDifference(i.timestamp,r.timestamp):"Service became unhealthy":"START"===r.type&&(r.fancyText="Monitoring started")}r.fancyTimeAgo=e.generatePrettyTimeAgo(r.timestamp),n.push(r)}e.events=n}}))},generateBadgeImageURL:function(e){return"".concat(this.serverUrl,"/api/v1/badges/uptime/").concat(e,"/").concat(this.serviceStatus.key)},prettifyUptime:function(e){return e?(100*e).toFixed(2)+"%":"0%"},prettifyTimeDifference:function(e,t){var n=Math.ceil((new Date(e)-new Date(t))/1e3/60);return n+(1===n?" minute":" minutes")},showTooltip:function(e,t){this.$emit("showTooltip",e,t)}},data:function(){return{serviceStatus:{},events:[],serverUrl:"."===it?"..":it}},created:function(){this.fetchData()}});n("1bf4");et.render=_e,et.__scopeId="data-v-d47609f0";var tt=et,nt=[{path:"/",name:"Home",component:pe},{path:"/services/:key",name:"Details",component:tt}],st=Object(H["a"])({history:Object(H["b"])("/"),routes:nt}),rt=st,it=".";Object(s["c"])(D).use(rt).mount("#app")},"66ed":function(e,t){e.exports="data:image/png;base64,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"},"6da3":function(e,t,n){"use strict";n("b73a")},"72e5":function(e,t){e.exports="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAJF0lEQVR4nO2b7VcTVwKHf8S8kGRmkpAJBKyVstbIS3gxAeXFikcRbdVaBJvwEgqoqwuEELqwkJcCMQna0926bvecPdbtdtftnn7YP6EfevoPiau0FXD2w2TCMJkgiKME73PO/ZIzk3Pn99y59+beG4BAIBAIBAKBQCAQCAQCgUAgEAgEAoFAIBAIMqjgcmngcmkA5KUK4RWQB2BPictlcN0K+us+DwaLTlcbAewBkaA4eQDUrMNB1yyM/aEkdu1J8c1rS7VJf4h1OGgAahAJipEKn6Vr4/5Q8c1rS+ZoP2eO+jh7bOjnmsRwlHWwRIJC5AFQw2ajauIjkeLY1V/48Ps5U9THmaI+zh678ktNfGQeNhsFIuGlwocPGOoWRqPFsSu/CuGbU+ELpXB+6GltYjQOwJC6R/Vaa74LWAs/6Z+3z19dXgu/f134pqiPYyI+zjY3uFKdHLsNwAgiYVukwz+cGI0XzQ+tiMOXtn5BABPxcezc4ErNgv8LrL0JpDvaIuk+v+6WP1E0f2V5ffjyrV9crHMDK87kyB/BkoF5q+QBUFtcZaa65OiCPXbl6fPClxNAR/o46+zgsjMx/CdzbakZRMKmUAFQm2trzdXJ0dty4W/U9YjDF4p1dnC5KjF8h2msLACgARkTsqICoDG1OC21Cf/n9thVmfD7OWa652m28Okp769SAXSkjyuYHViuSIze2dvQYAWRIIsKgIZprCyoWfB/Id/y+znTpHeJvnT8flYBHa1f0592PRKHT4V7OSrcy1lnB5cr4iN/JhIySbf86uToHfv81WXLZ59khE9PeX82nD0apw7s7czW9Zjdlb78tsMz1MTl/0kFUOFe/k24OXI31R2RKSpS4ZtrS801ybG7RfNXZMM3TXqXDB80RgE0MOX7P5ALn4n4uIKjNZcB1OvbXBPURNcjcfhUuJczhns5y2cDy5Xx4b+anG9b8Ia/CSoAGpZl6dpbY18VxYZWsoZ/vikEwAmg3FRddjLbwMserz8P4BAAp7HdHTAGuxalAozhXs4y+8lKZXz4nqWszIQ3VIIo/MDfCjcMv3kGQCWAUgD7LK6DLXKtn470cYUnGtsBvA1gP4AKsQSjpJij/avl8ZFvzaWlZrxhElQANAUHDjC1twP3CueHVmXDn0q3/AoAbwFgAdisRyuOZJt2FrU1ngRQmLr2LQDl+nZ3wDjR9UgqwBDq4ZiIb7U8OfKv1O+EN0LCWp9/O3DfNpct/O4lw7mWMIByAHsBWABQACzWxsqGbHN+e3vzCdG1ZgAlAA7pz7oDepEEQ6gnXZiob/VQYviByenc9WOCCoBm78kGa82tsa9tc0OrslPNqe4n1IWWz7AWvhlAPgAdAMbaVFkvFz4d6ePsZ1pbU9frUveYkJZQP2Gc6FqUCjCEejg62rfquPm7B/azx2zYpRJUADS/Od1UWJP03y/MFv6k9zF1sWUWmeGrwQeTFiANnxfQ0gqASV2rBi9CJOHIp4Zg50Nx+PpQN6cPdXNMtG/VkRh+UPp+qx27TIIKgOadC0eKnImRvxdmrGoK4Xc/pi6+N4vM8PcI3wGAYZur3HICqHCvVIAqdW+mhPHORakAfaibo6O+VUfsxr/3XWgrwS6RoAKgKX2/1e5cGP3GNj8oH/5U9xPqomy3I2y0C0vTDNtc5ZZr/RIBatF98hKCXYvi8NMSIr7Vg7Hr3+0GCSoAmv3tx4qrk/5/2uYGZbsdesq7ZPiwOYr14euw/pTDhgKEeb6MAOHeTAnt9RP5wUuPxOHnz/CFl3DjP7ksId3nO5Oj37FZ+nx60rNkONcknu3IhS8gK0D8QyuLACCLBF27O5A/3rkoDl8oVLj32YHY9e9zcUzIA6AuaXWxVbf837NzA8/klpSZSc8Tip/nZ+t2pGQIkC41bCBAQIWMN8EdyB+/tCgVoJvxcsZwz7N34zf+W3S6qXCD79xx7AGgf3fuWkgufFPUxzFT6fArsLnwAYkAafibFJCHTAnlunZ3QCeRoJvxcroZL2cI9zzbHxmMA9Cn6rejEfpq2vx775dyGyn0pGeJOtcUhnZL4QMiAXLhGzcnQKijtDtKSehcFIeflhDsuAeABt8V7ei3IA+AFoDFHPz4rnQHi57yiJcXthI+sIEA49YECPXM6I50p9wB7XjHI3H42mkPZxjr+Ab8L2ztJur5WskD/1BW0/jlr9ZtnEx5nhguNEewuQFXDlkBxhcTINQ1U8IZV1AsQTvt4Qz+j/4BwJq6dkcPxoIAlhnvTAtgJr2PDZnLC1sJH5ARIF1g26IAob6ZEtrrJ7SBS4vaaQ8vYPSjb8Ev8OUjRwRYBQHMpOfxc35kbZZ1AqThv6AAoc4yU9T6CU2g4yHfBaUF5MwbUMAEO//CTHofUx3vhQE4sL3wAZEAufANoZ4XFSDUWyrhoO7skYBmvOOhIZBbXZAWgJm+fj5p/PhEEEAZ+AfaTvjABgKEdZ1tCBDqLpZQDKBM82Hjb41Xz32Zqr8WOSBADcBIux0OAHYANvChbLXPlyIrQLyyuU0BQv0FCUyq7nZdw6GD4PcYdvw0FFh7ACP4ubMRfMvZ7j9YMgRI1/VfggBgbWDWgj9bKjzDdhvQK0NoRRqsrcursP2KrxMgDf8lChBQYW0fQoMcCV8gT1JeBmkBcuHrQ90c+3IFAMo8R86SVYCwnKyAAIIIWQHi9XwiQFkyBEh3s4gAZUkLkNtKJAKUZ0MB+TNEgNJkFSBsohAByiIrQLyLRQQoyzoB0j1cIkB50gLkwicClIcX0CAvQDfjJQIUJqsAYfuQCFCWDAHS0wtEgLKsEyANnwhQnrQAufC10x6OPXO0FUSAYmQVIJxeIAKUJUOAEDwR8GpYJ0AaPhGgPGoANHvE6ZILXzvt4dizjcdBBCiGGgBF1R2oUA+c/kHd3/aj2nfqJ7Xv1E/q/rYf1QOnfzAdcx0Gf3pB/ZrruivZA/6IOIt8lEGLKmhQBw3qoEUV8lEG/hhJThwjz0WEoyIU+GOCJQD2pUpJ6jPh7M6OPjyVq4jP6+jBh02nCpX6bMefXNsNCH8/VUuK8HdWMvi+Qsh5HQKBQCAQCAQCgUAgEAgEAoFAIBCez/8BneC0cjU1kO8AAAAASUVORK5CYII="},"733c":function(e,t){e.exports="data:image/png;base64,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"},a766:function(e,t,n){},ae5b:function(e,t,n){},afea:function(e,t,n){},b73a:function(e,t,n){},b85e:function(e,t,n){},bca1:function(e,t,n){},cf05:function(e,t,n){e.exports=n.p+"img/logo.png"},e007:function(e,t,n){},ef45:function(e,t,n){"use strict";n("3f93")}}); \ No newline at end of file +(function(e){function t(t){for(var s,c,o=t[0],a=t[1],l=t[2],h=0,d=[];h/g,">").replace(/"/g,""").replace(/'/g,"'")},reposition:function(){if(this.event&&this.event.type)if("mouseenter"===this.event.type){var e=this.event.target.getBoundingClientRect().y+30,t=this.event.target.getBoundingClientRect().x,n=this.$refs.tooltip.getBoundingClientRect();t+window.scrollX+n.width+50>document.body.getBoundingClientRect().width&&(t=this.event.target.getBoundingClientRect().x-n.width+this.event.target.getBoundingClientRect().width,t<0&&(t+=-t)),e+window.scrollY+n.height+50>document.body.getBoundingClientRect().height&&e>=0&&(e=this.event.target.getBoundingClientRect().y-(n.height+10),e<0&&(e=this.event.target.getBoundingClientRect().y+30)),this.top=e,this.left=t}else"mouseleave"===this.event.type&&(this.hidden=!0)}},watch:{event:function(e){e&&e.type&&("mouseenter"===e.type?this.hidden=!1:"mouseleave"===e.type&&(this.hidden=!0))}},updated:function(){this.reposition()},created:function(){this.reposition()},data:function(){return{hidden:!0,top:0,left:0}}};n("1dd9");C.render=S;var R=C,E={name:"App",components:{Social:p,Tooltip:R},methods:{showTooltip:function(e,t){this.tooltip={result:e,event:t}}},data:function(){return{tooltip:{}}}};n("3d28");E.render=a;var D=E,H=(n("a766"),n("6c02"));function k(e,t,n,r,i,c){var o=Object(s["y"])("Services"),a=Object(s["y"])("Settings");return Object(s["q"])(),Object(s["d"])(s["a"],null,[Object(s["h"])(o,{serviceStatuses:i.serviceStatuses,showStatusOnHover:!0,onShowTooltip:c.showTooltip},null,8,["serviceStatuses","onShowTooltip"]),Object(s["h"])(a,{onRefreshData:c.fetchData},null,8,["onRefreshData"])],64)}n("d3b7");var P=Object(s["E"])("data-v-31e7281e");Object(s["t"])("data-v-31e7281e");var U={id:"settings"},M={class:"flex bg-gray-200 rounded border border-gray-300 shadow"},Q=Object(s["h"])("div",{class:"text-sm text-gray-600 rounded-xl py-1 px-2"}," ↻ ",-1),q=Object(s["f"])('',6);Object(s["r"])();var X=P((function(e,t,n,r,i,c){return Object(s["q"])(),Object(s["d"])("div",U,[Object(s["h"])("div",M,[Q,Object(s["h"])("select",{class:"text-center text-gray-500 text-sm",id:"refresh-rate",ref:"refreshInterval",onChange:t[1]||(t[1]=function(){return c.handleChangeRefreshInterval&&c.handleChangeRefreshInterval.apply(c,arguments)})},[q],544)])])})),z={name:"Settings",props:{},methods:{setRefreshInterval:function(e){var t=this;this.refreshIntervalHandler=setInterval((function(){t.refreshData()}),1e3*e)},refreshData:function(){this.$emit("refreshData")},handleChangeRefreshInterval:function(){this.refreshData(),clearInterval(this.refreshIntervalHandler),this.setRefreshInterval(this.$refs.refreshInterval.value)}},created:function(){this.setRefreshInterval(this.refreshInterval)},unmounted:function(){clearInterval(this.refreshIntervalHandler)},data:function(){return{refreshInterval:30,refreshIntervalHandler:0}}};n("49d2");z.render=X,z.__scopeId="data-v-31e7281e";var G=z,F=(n("b0c0"),{id:"results"});function K(e,t,n,r,i,c){var o=Object(s["y"])("ServiceGroup");return Object(s["q"])(),Object(s["d"])("div",F,[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(i.serviceGroups,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[Object(s["h"])(o,{services:t.services,name:t.name,onShowTooltip:c.showTooltip},null,8,["services","name","onShowTooltip"])]}))})),128))])}var Y={class:"text-monospace text-gray-400 text-xl font-medium pb-2 px-3"},J={key:0,class:"text-green-600"},Z={key:1,class:"text-yellow-400"},N={class:"float-right service-group-arrow"};function W(e,t,n,r,i,c){var o=Object(s["y"])("Service");return Object(s["q"])(),Object(s["d"])("div",{class:0===n.services.length?"mt-3":"mt-4"},["undefined"!==n.name?Object(s["x"])(e.$slots,"default",{key:0},(function(){return[Object(s["h"])("div",{class:"service-group pt-2 border",onClick:t[1]||(t[1]=function(){return c.toggleGroup&&c.toggleGroup.apply(c,arguments)})},[Object(s["h"])("h5",Y,[i.healthy?(Object(s["q"])(),Object(s["d"])("span",J,"✓")):(Object(s["q"])(),Object(s["d"])("span",Z,"~")),Object(s["g"])(" "+Object(s["A"])(n.name)+" ",1),Object(s["h"])("span",N,Object(s["A"])(i.collapsed?"▼":"▲"),1)])])]})):Object(s["e"])("",!0),i.collapsed?Object(s["e"])("",!0):(Object(s["q"])(),Object(s["d"])("div",{key:1,class:"undefined"===n.name?"":"service-group-content"},[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.services,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[Object(s["h"])(o,{data:t,onShowTooltip:c.showTooltip,maximumNumberOfResults:20},null,8,["data","onShowTooltip"])]}))})),128))],2))],2)}var L={key:0,class:"service px-3 py-3 border-l border-r border-t rounded-none hover:bg-gray-100"},V={class:"flex flex-wrap mb-2"},$={class:"w-3/4"},_={class:"text-gray-500 font-light"},ee={class:"w-1/4 text-right"},te={class:"font-light status-min-max-ms"},ne={class:"status-over-time flex flex-row"},se=Object(s["h"])("span",{class:"status rounded border border-dashed"},null,-1),re={class:"flex flex-wrap status-time-ago"},ie={class:"w-1/2"},ce={class:"w-1/2 text-right"};function oe(e,t,n,r,i,c){var o=Object(s["y"])("router-link");return n.data&&n.data.results&&n.data.results.length?(Object(s["q"])(),Object(s["d"])("div",L,[Object(s["h"])("div",V,[Object(s["h"])("div",$,[Object(s["h"])(o,{to:c.generatePath(),class:"font-bold hover:text-blue-800 hover:underline",title:"View detailed service health"},{default:Object(s["D"])((function(){return[Object(s["g"])(Object(s["A"])(n.data.name),1)]})),_:1},8,["to"]),Object(s["h"])("span",_," | "+Object(s["A"])(n.data.results[n.data.results.length-1].hostname),1)]),Object(s["h"])("div",ee,[Object(s["h"])("span",te,Object(s["A"])(i.minResponseTime===i.maxResponseTime?i.minResponseTime:i.minResponseTime+"-"+i.maxResponseTime)+"ms ",1)])]),Object(s["h"])("div",null,[Object(s["h"])("div",ne,[(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.maximumNumberOfResults-n.data.results.length,(function(t){return Object(s["x"])(e.$slots,"default",{key:t},(function(){return[se]}))})),128)),(Object(s["q"])(!0),Object(s["d"])(s["a"],null,Object(s["w"])(n.data.results,(function(n){return Object(s["x"])(e.$slots,"default",{key:n},(function(){return[n.success?(Object(s["q"])(),Object(s["d"])("span",{key:0,class:"status rounded bg-success",onMouseenter:function(e){return c.showTooltip(n,e)},onMouseleave:t[1]||(t[1]=function(e){return c.showTooltip(null,e)})},"✓",40,["onMouseenter"])):(Object(s["q"])(),Object(s["d"])("span",{key:1,class:"status rounded bg-red-600",onMouseenter:function(e){return c.showTooltip(n,e)},onMouseleave:t[2]||(t[2]=function(e){return c.showTooltip(null,e)})},"X",40,["onMouseenter"]))]}))})),128))])]),Object(s["h"])("div",re,[Object(s["h"])("div",ie,Object(s["A"])(e.generatePrettyTimeAgo(n.data.results[0].timestamp)),1),Object(s["h"])("div",ce,Object(s["A"])(e.generatePrettyTimeAgo(n.data.results[n.data.results.length-1].timestamp)),1)])])):Object(s["e"])("",!0)}n("a9e3");var ae={methods:{generatePrettyTimeAgo:function(e){var t=(new Date).getTime()-new Date(e).getTime();if(t>36e5){var n=(t/36e5).toFixed(0);return n+" hour"+("1"!==n?"s":"")+" ago"}if(t>6e4){var s=(t/6e4).toFixed(0);return s+" minute"+("1"!==s?"s":"")+" ago"}return(t/1e3).toFixed(0)+" seconds ago"}}},le={name:"Service",props:{maximumNumberOfResults:Number,data:Object},emits:["showTooltip"],mixins:[ae],methods:{updateMinAndMaxResponseTimes:function(){var e=null,t=null;for(var n in this.data.results){var s=parseInt(this.data.results[n].duration/1e6);(null==e||e>s)&&(e=s),(null==t||t=0;s--){var r=t.events[s];if(s===t.events.length-1)"UNHEALTHY"===r.type?r.fancyText="Service is unhealthy":"HEALTHY"===r.type?r.fancyText="Service is healthy":"START"===r.type&&(r.fancyText="Monitoring started");else{var i=t.events[s+1];"HEALTHY"===r.type?r.fancyText="Service became healthy":"UNHEALTHY"===r.type?r.fancyText=i?"Service was unhealthy for "+e.prettifyTimeDifference(i.timestamp,r.timestamp):"Service became unhealthy":"START"===r.type&&(r.fancyText="Monitoring started")}r.fancyTimeAgo=e.generatePrettyTimeAgo(r.timestamp),n.push(r)}e.events=n}}))},generateBadgeImageURL:function(e){return"".concat(this.serverUrl,"/api/v1/badges/uptime/").concat(e,"/").concat(this.serviceStatus.key)},prettifyUptime:function(e){return e?(100*e).toFixed(2)+"%":"0%"},prettifyTimeDifference:function(e,t){var n=Math.ceil((new Date(e)-new Date(t))/1e3/60);return n+(1===n?" minute":" minutes")},showTooltip:function(e,t){this.$emit("showTooltip",e,t)}},data:function(){return{serviceStatus:{},events:[],uptime:{"7d":0,"24h":0,"1h":0},serverUrl:"."===it?"..":it}},created:function(){this.fetchData()}});n("9f40");et.render=_e,et.__scopeId="data-v-484ca9f8";var tt=et,nt=[{path:"/",name:"Home",component:pe},{path:"/services/:key",name:"Details",component:tt}],st=Object(H["a"])({history:Object(H["b"])("/"),routes:nt}),rt=st,it=".";Object(s["c"])(D).use(rt).mount("#app")},"66ed":function(e,t){e.exports="data:image/png;base64,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"},"6da3":function(e,t,n){"use strict";n("b73a")},"72e5":function(e,t){e.exports="data:image/png;base64,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"},"733c":function(e,t){e.exports="data:image/png;base64,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"},"9f40":function(e,t,n){"use strict";n("d796")},a766:function(e,t,n){},ae5b:function(e,t,n){},afea:function(e,t,n){},b73a:function(e,t,n){},bca1:function(e,t,n){},cf05:function(e,t,n){e.exports=n.p+"img/logo.png"},d796:function(e,t,n){},e007:function(e,t,n){},ef45:function(e,t,n){"use strict";n("3f93")}}); \ No newline at end of file