From 25913d645810645e8094d90b650b6790f9571521 Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Wed, 4 Mar 2020 02:29:00 -0800 Subject: [PATCH] refactor --- Makefile | 21 +- cmd/assets.go | 30 + cmd/cli.go | 29 +- cmd/database.go | 23 + cmd/init.go | 1 + cmd/main.go | 121 ++- cmd/notifiers.go | 3 + core/checkin.go | 154 ---- core/configs.go | 216 ------ core/core.go | 175 ----- core/core_test.go | 151 ---- core/database.go | 410 ---------- core/doc.go | 6 - core/failures.go | 29 - core/groups.go | 58 -- core/incidents.go | 18 - core/integrations.go | 12 - core/messages.go | 29 - core/notifier/audit.go | 45 -- core/notifiers.go | 21 - core/sample.go | 726 ------------------ core/services.go | 163 ---- core/sparklines.go | 1 - core/users.go | 115 --- database/checkin_hits.go | 8 - database/checkins.go | 131 ---- database/crud.go | 80 -- database/database.go | 88 +-- database/database_test.go | 44 ++ database/failures.go | 57 -- database/group.go | 47 -- database/grouping.go | 28 +- database/hits.go | 52 -- database/incident.go | 30 - database/integration.go | 18 - database/interface.go | 27 + database/message.go | 24 - database/sample.go | 173 +++++ database/service.go | 212 ----- database/setup.go | 15 + database/time.go | 6 +- database/user.go | 31 - frontend/yarn.lock | 405 +++++----- go.mod | 1 + go.sum | 4 + handlers/api.go | 65 +- handlers/api_test.go | 397 ++-------- handlers/checkin.go | 41 +- handlers/checkins_test.go | 41 + handlers/dashboard.go | 7 +- {core => handlers}/export.go | 41 +- handlers/function.go | 12 +- handlers/groups.go | 92 +-- handlers/groups_test.go | 47 ++ handlers/handlers.go | 21 +- handlers/incident.go | 29 +- handlers/index.go | 11 +- handlers/integrations.go | 41 +- handlers/messages.go | 51 +- handlers/messages_test.go | 72 ++ handlers/middleware.go | 6 +- handlers/notifications.go | 65 +- handlers/notifiers_test.go | 39 + handlers/prometheus.go | 23 +- handlers/query.go | 25 + handlers/routes.go | 4 +- handlers/services.go | 189 ++--- handlers/services_test.go | 147 ++++ handlers/setup.go | 77 +- handlers/users.go | 38 +- handlers/users_test.go | 57 ++ .../integrations => integrators}/csv_file.go | 36 +- .../csv_file_test.go | 2 +- {core/integrations => integrators}/docker.go | 23 +- .../docker_test.go | 2 +- .../integrations.go | 50 +- {core/integrations => integrators}/traefik.go | 27 +- .../traefik_test.go | 2 +- notifiers/command.go | 17 +- notifiers/discord.go | 17 +- notifiers/email.go | 22 +- notifiers/line_notify.go | 17 +- notifiers/mobile.go | 19 +- notifiers/notifiers.go | 75 ++ notifiers/notifiers_test.go | 2 +- notifiers/slack.go | 19 +- notifiers/telegram.go | 17 +- notifiers/twilio.go | 17 +- notifiers/webhook.go | 21 +- source/wiki.go | 2 +- types/checkin.go | 88 --- types/checkins/database.go | 53 ++ types/checkins/database_hits.go | 29 + types/checkins/failures.go | 23 + types/checkins/methods.go | 59 ++ types/checkins/routine.go | 60 ++ types/checkins/samples.go | 47 ++ types/checkins/struct.go | 55 ++ types/configs/configs_env.go | 76 ++ types/configs/configs_file.go | 26 + types/configs/configs_test.go | 50 ++ types/configs/connection.go | 110 +++ types/configs/database.go | 113 +++ types/configs/file.go | 77 ++ types/configs/methods.go | 34 + types/configs/migration.go | 76 ++ types/configs/struct.go | 26 + types/core.go | 60 -- types/core/core_test.go | 24 + types/core/database.go | 77 ++ types/core/init.go | 36 + types/core/samples.go | 28 + types/core/struct.go | 44 ++ types/failures/database.go | 34 + types/failures/interface.go | 54 ++ types/failures/samples.go | 31 + types/{failure.go => failures/struct.go} | 22 +- types/group.go | 13 - types/groups/database.go | 56 ++ types/groups/methods.go | 20 + types/groups/samples.go | 28 + types/groups/struct.go | 16 + types/hits/database.go | 34 + types/hits/interface.go | 63 ++ types/hits/samples.go | 30 + types/hits/struct.go | 20 + types/incidents/database.go | 30 + types/incidents/database_updates.go | 25 + types/incidents/samples.go | 38 + types/{incident.go => incidents/struct.go} | 4 +- types/integrations/database.go | 41 + .../struct.go} | 6 +- types/interface.go | 1 + types/message.go | 45 -- types/messages/database.go | 30 + types/messages/samples.go | 26 + types/messages/struct.go | 31 + core/hits.go => types/notifications/audit.go | 10 +- types/notifications/database.go | 53 ++ {core/notifier => types/notifications}/doc.go | 6 +- .../notifications}/events.go | 58 +- .../notifications}/example_test.go | 44 +- .../notifications}/interfaces.go | 32 +- types/notifications/methods.go | 148 ++++ types/notifications/notifications_test.go | 14 + .../notifications/struct.go | 276 +------ .../notifications/struct_test.go | 35 +- types/null.go | 121 --- types/null/marshal.go | 35 + types/null/types.go | 45 ++ types/null/unmarshal.go | 31 + types/plugin.go | 78 -- types/safe.go | 6 - types/service.go | 128 --- types/services/checkins.go | 20 + types/services/database.go | 79 ++ types/services/env.go | 59 ++ types/services/failures.go | 95 +++ types/services/hits.go | 18 + types/services/methods.go | 207 +++++ core/checker.go => types/services/routine.go | 99 ++- types/services/samples.go | 82 ++ .../services}/services_checkin_test.go | 2 +- {core => types/services}/services_test.go | 8 +- types/services/struct.go | 94 +++ types/types.go | 59 -- types/user.go | 50 -- types/users/auth.go | 35 + types/users/database.go | 47 ++ types/users/sample.go | 31 + types/users/struct.go | 28 + {core => types/users}/users_test.go | 2 +- utils/log.go | 12 +- utils/utils.go | 62 -- 174 files changed, 4775 insertions(+), 5466 deletions(-) create mode 100644 cmd/assets.go create mode 100644 cmd/database.go create mode 100644 cmd/init.go create mode 100644 cmd/notifiers.go delete mode 100644 core/checkin.go delete mode 100644 core/configs.go delete mode 100644 core/core.go delete mode 100644 core/core_test.go delete mode 100644 core/database.go delete mode 100644 core/doc.go delete mode 100644 core/failures.go delete mode 100644 core/groups.go delete mode 100644 core/incidents.go delete mode 100644 core/integrations.go delete mode 100644 core/messages.go delete mode 100644 core/notifier/audit.go delete mode 100644 core/notifiers.go delete mode 100644 core/sample.go delete mode 100644 core/services.go delete mode 100644 core/sparklines.go delete mode 100644 core/users.go delete mode 100644 database/checkin_hits.go delete mode 100644 database/checkins.go delete mode 100644 database/crud.go create mode 100644 database/database_test.go delete mode 100644 database/failures.go delete mode 100644 database/group.go create mode 100644 database/interface.go create mode 100644 database/sample.go create mode 100644 database/setup.go create mode 100644 handlers/checkins_test.go rename {core => handlers}/export.go (56%) create mode 100644 handlers/groups_test.go create mode 100644 handlers/messages_test.go create mode 100644 handlers/notifiers_test.go create mode 100644 handlers/services_test.go create mode 100644 handlers/users_test.go rename {core/integrations => integrators}/csv_file.go (77%) rename {core/integrations => integrators}/csv_file_test.go (98%) rename {core/integrations => integrators}/docker.go (83%) rename {core/integrations => integrators}/docker_test.go (97%) rename {core/integrations => integrators}/integrations.go (70%) rename {core/integrations => integrators}/traefik.go (81%) rename {core/integrations => integrators}/traefik_test.go (96%) create mode 100644 notifiers/notifiers.go delete mode 100644 types/checkin.go create mode 100644 types/checkins/database.go create mode 100644 types/checkins/database_hits.go create mode 100644 types/checkins/failures.go create mode 100644 types/checkins/methods.go create mode 100644 types/checkins/routine.go create mode 100644 types/checkins/samples.go create mode 100644 types/checkins/struct.go create mode 100644 types/configs/configs_env.go create mode 100644 types/configs/configs_file.go create mode 100644 types/configs/configs_test.go create mode 100644 types/configs/connection.go create mode 100644 types/configs/database.go create mode 100644 types/configs/file.go create mode 100644 types/configs/methods.go create mode 100644 types/configs/migration.go create mode 100644 types/configs/struct.go delete mode 100644 types/core.go create mode 100644 types/core/core_test.go create mode 100644 types/core/database.go create mode 100644 types/core/init.go create mode 100644 types/core/samples.go create mode 100644 types/core/struct.go create mode 100644 types/failures/database.go create mode 100644 types/failures/interface.go create mode 100644 types/failures/samples.go rename types/{failure.go => failures/struct.go} (61%) delete mode 100644 types/group.go create mode 100644 types/groups/database.go create mode 100644 types/groups/methods.go create mode 100644 types/groups/samples.go create mode 100644 types/groups/struct.go create mode 100644 types/hits/database.go create mode 100644 types/hits/interface.go create mode 100644 types/hits/samples.go create mode 100644 types/hits/struct.go create mode 100644 types/incidents/database.go create mode 100644 types/incidents/database_updates.go create mode 100644 types/incidents/samples.go rename types/{incident.go => incidents/struct.go} (93%) create mode 100644 types/integrations/database.go rename types/{integrations.go => integrations/struct.go} (87%) create mode 100644 types/interface.go delete mode 100644 types/message.go create mode 100644 types/messages/database.go create mode 100644 types/messages/samples.go create mode 100644 types/messages/struct.go rename core/hits.go => types/notifications/audit.go (87%) create mode 100644 types/notifications/database.go rename {core/notifier => types/notifications}/doc.go (95%) rename {core/notifier => types/notifications}/events.go (82%) rename {core/notifier => types/notifications}/example_test.go (89%) rename {core/notifier => types/notifications}/interfaces.go (72%) create mode 100644 types/notifications/methods.go create mode 100644 types/notifications/notifications_test.go rename core/notifier/notifiers.go => types/notifications/struct.go (55%) rename core/notifier/notifiers_test.go => types/notifications/struct_test.go (89%) delete mode 100644 types/null.go create mode 100644 types/null/marshal.go create mode 100644 types/null/types.go create mode 100644 types/null/unmarshal.go delete mode 100644 types/plugin.go delete mode 100644 types/safe.go delete mode 100644 types/service.go create mode 100644 types/services/checkins.go create mode 100644 types/services/database.go create mode 100644 types/services/env.go create mode 100644 types/services/failures.go create mode 100644 types/services/hits.go create mode 100644 types/services/methods.go rename core/checker.go => types/services/routine.go (80%) create mode 100644 types/services/samples.go rename {core => types/services}/services_checkin_test.go (99%) rename {core => types/services}/services_test.go (98%) create mode 100644 types/services/struct.go delete mode 100644 types/types.go delete mode 100644 types/user.go create mode 100644 types/users/auth.go create mode 100644 types/users/database.go create mode 100644 types/users/sample.go create mode 100644 types/users/struct.go rename {core => types/users}/users_test.go (99%) diff --git a/Makefile b/Makefile index f8636506..77ec5773 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master TEST_DIR=$(GOPATH)/src/github.com/hunterlong/statping PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH) +all: clean yarn-install compile docker-base docker-vue build-all compress + up: docker-compose -f docker-compose.yml -f dev/docker-compose.full.yml up -d --remove-orphans make print_details @@ -28,6 +30,9 @@ reup: down clean compose-build-full up yarn-serve: cd frontend && yarn serve +yarn-install: + cd frontend && rm -rf node_modules && yarn + go-run: go run ./cmd @@ -122,21 +127,27 @@ print_details: @echo \==== Monitoring and IDE ==== @echo \Grafana: http://localhost:3000 \(username: admin, password: admin\) +build-all: xgo-install build-mac build-linux build-linux build-alpine + +download-key: + wget -O statping.gpg https://s3-us-west-2.amazonaws.com/assets.statping.com/UIDHJI2I292HDH20FJOIJOUIF29UHF827HHF9H2FHH27FGHRIEHFISUHFISHF.gpg + gpg --import statping.gpg + # build Statping for Mac, 64 and 32 bit -build-mac: compile +build-mac: mkdir build $(XGO) $(BUILDVERSION) --targets=darwin/amd64,darwin/386 ./cmd # build Statping for Linux 64, 32 bit, arm6/arm7 -build-linux: compile +build-linux: $(XGO) $(BUILDVERSION) --targets=linux/amd64,linux/386,linux/arm-7,linux/arm-6,linux/arm64 ./cmd # build for windows 64 bit only -build-windows: compile +build-windows: $(XGO) $(BUILDVERSION) --targets=windows-6.0/amd64 ./cmd # build Alpine linux binary (used in docker images) -build-alpine: compile +build-alpine: $(XGO) --targets=linux/amd64 -ldflags="-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT) -linkmode external -extldflags -static" -out alpine ./cmd # build :latest docker tag @@ -145,7 +156,7 @@ docker-build-latest: docker tag hunterlong/statping:latest hunterlong/statping:v${VERSION} # compress built binaries into tar.gz and zip formats -compress: +compress: download-key cd build && mv alpine-linux-amd64 $(BINARY_NAME) cd build && gpg --default-key $(SIGN_KEY) --batch --detach-sign --output statping.asc --armor $(BINARY_NAME) cd build && tar -czvf $(BINARY_NAME)-linux-alpine.tar.gz $(BINARY_NAME) statping.asc && rm -f $(BINARY_NAME) statping.asc diff --git a/cmd/assets.go b/cmd/assets.go new file mode 100644 index 00000000..4590559d --- /dev/null +++ b/cmd/assets.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "github.com/hunterlong/statping/source" + "github.com/hunterlong/statping/utils" + "net" +) + +// UsingAssets will return true if /assets folder is present +func UsingAssets() bool { + return source.UsingAssets(utils.Directory) +} + +// GetLocalIP returns the non loopback local IP of the host +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "http://localhost" + } + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return fmt.Sprintf("http://%v", ipnet.IP.String()) + } + } + } + return "http://localhost" +} diff --git a/cmd/cli.go b/cmd/cli.go index 0ad52aba..1a5f743b 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -18,8 +18,11 @@ package main import ( "encoding/json" "fmt" - "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/handlers" "github.com/hunterlong/statping/source" + "github.com/hunterlong/statping/types/configs" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "github.com/joho/godotenv" "github.com/pkg/errors" @@ -32,7 +35,6 @@ func catchCLI(args []string) error { dir := utils.Directory runLogs := utils.InitLogs runAssets := source.Assets - loadDotEnvs() switch args[0] { case "version": @@ -100,14 +102,14 @@ func catchCLI(args []string) error { if err = runAssets(); err != nil { return err } - configs, err := core.LoadConfigFile(dir) + config, err := configs.LoadConfigs() if err != nil { return err } - if err = core.CoreApp.Connect(configs, false, dir); err != nil { + if err = config.Connect(); err != nil { return err } - if data, err = core.ExportSettings(); err != nil { + if data, err = handlers.ExportSettings(); err != nil { return fmt.Errorf("could not export settings: %v", err.Error()) } //core.CloseDB() @@ -125,7 +127,7 @@ func catchCLI(args []string) error { if data, err = ioutil.ReadFile(filename); err != nil { return err } - var exportData core.ExportData + var exportData handlers.ExportData if err = json.Unmarshal(data, &exportData); err != nil { return err } @@ -200,24 +202,27 @@ func updateDisplay() error { // runOnce will initialize the Statping application and check each service 1 time, will not run HTTP server func runOnce() error { - configs, err := core.LoadConfigFile(utils.Directory) + config, err := configs.LoadConfigs() if err != nil { return errors.Wrap(err, "config.yml file not found") } - err = core.CoreApp.Connect(configs, false, utils.Directory) + err = config.Connect() if err != nil { return errors.Wrap(err, "issue connecting to database") } - core.CoreApp, err = core.SelectCore() + c, err := core.Select() if err != nil { return errors.Wrap(err, "core database was not found or setup") } - _, err = core.SelectAllServices(true) + + core.App = c + + _, err = services.SelectAllServices(true) if err != nil { return errors.Wrap(err, "could not select all services") } - for _, srv := range core.Services() { - core.CheckService(srv, true) + for _, srv := range services.Services() { + services.CheckService(srv, true) } return nil } diff --git a/cmd/database.go b/cmd/database.go new file mode 100644 index 00000000..1037af39 --- /dev/null +++ b/cmd/database.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/groups" + "github.com/hunterlong/statping/types/hits" + "github.com/hunterlong/statping/types/incidents" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/messages" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" +) + +var ( + // DbSession stores the Statping database session + DbModels []interface{} +) + +func init() { + DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, ¬ifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}, &integrations.Integration{}} +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/cmd/init.go @@ -0,0 +1 @@ +package main diff --git a/cmd/main.go b/cmd/main.go index 194c03fc..96e7f92c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,15 +16,17 @@ package main import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/configs" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "github.com/pkg/errors" "flag" "fmt" - "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/handlers" "github.com/hunterlong/statping/source" - "github.com/joho/godotenv" "os" "os/signal" "syscall" @@ -40,10 +42,11 @@ var ( verboseMode int port int log = utils.Log.WithField("type", "cmd") + httpServer = make(chan bool) ) func init() { - core.VERSION = VERSION + } // parseFlags will parse the application flags @@ -63,7 +66,6 @@ func parseFlags() { } func exit(err error) { - fmt.Printf("%+v", core.Configs()) panic(err) //log.Fatalln(err) //os.Exit(2) @@ -73,13 +75,19 @@ func exit(err error) { func main() { var err error go sigterm() + parseFlags() - loadDotEnvs() - source.Assets() + + if err := source.Assets(); err != nil { + exit(err) + } + utils.VerboseMode = verboseMode + if err := utils.InitLogs(); err != nil { log.Errorf("Statping Log Error: %v\n", err) } + args := flag.Args() if len(args) >= 1 { @@ -98,41 +106,21 @@ func main() { log.Warnln(err) } - // check if DB_CONN was set, and load config from that - autoConfigDb := utils.Getenv("DB_CONN", "").(string) - if autoConfigDb != "" { - log.Infof("Environment variable 'DB_CONN' was set to %s, loading configs from ENV.", autoConfigDb) - if _, err := core.LoadUsingEnv(); err != nil { - exit(err) - return - } else { - afterConfigLoaded() - } - } - - // attempt to load config.yml file from current directory, if no file, then start in setup mode. - _, err = core.LoadConfigFile(utils.Directory) + c, err := configs.LoadConfigs() if err != nil { - log.Errorln(err) - core.CoreApp.Setup = false - writeAble, err := utils.DirWritable(utils.Directory) - if err != nil { + if err := SetupMode(); err != nil { exit(err) - return } - if !writeAble { - log.Fatalf("Statping does not have write permissions at: %v\nYou can change this directory by setting the STATPING_DIR environment variable to a dedicated path before starting.", utils.Directory) - return - } - if err := handlers.RunHTTPServer(ipAddress, port); err != nil { - log.Fatalln(err) - } - } else { - afterConfigLoaded() } -} -func afterConfigLoaded() { + if err = c.Connect(); err != nil { + exit(err) + } + + if err := configs.MigrateDatabase(); err != nil { + exit(err) + } + if err := mainProcess(); err != nil { exit(err) } @@ -141,7 +129,11 @@ func afterConfigLoaded() { // Close will gracefully stop the database connection, and log file func Close() { utils.CloseLogs() - core.CloseDB() + database.Close() +} + +func SetupMode() error { + return handlers.RunHTTPServer(ipAddress, port) } // sigterm will attempt to close the database connections gracefully @@ -153,30 +145,9 @@ func sigterm() { os.Exit(1) } -// loadDotEnvs attempts to load database configs from a '.env' file in root directory -func loadDotEnvs() error { - err := godotenv.Load(envFile) - if err == nil { - log.Infoln("Environment file '.env' Loaded") - } - return err -} - // mainProcess will initialize the Statping application and run the HTTP server func mainProcess() error { - dir := utils.Directory - var err error - err = core.CoreApp.Connect(core.Configs(), false, dir) - if err != nil { - log.Errorln(fmt.Sprintf("could not connect to database: %v", err)) - return err - } - - if err := core.CoreApp.MigrateDatabase(); err != nil { - return errors.Wrap(err, "database migration") - } - - if err := core.CoreApp.ServicesFromEnvFile(); err != nil { + if err := services.ServicesFromEnvFile(); err != nil { errStr := "error 'SERVICE' environment variable" log.Errorln(errStr) return errors.Wrap(err, errStr) @@ -185,11 +156,33 @@ func mainProcess() error { if err := core.InitApp(); err != nil { return err } - if core.CoreApp.Setup { - if err := handlers.RunHTTPServer(ipAddress, port); err != nil { - log.Fatalln(err) - return errors.Wrap(err, "http server") - } + + if err := handlers.RunHTTPServer(ipAddress, port); err != nil { + log.Fatalln(err) + return errors.Wrap(err, "http server") } return nil } + +func StartHTTPServer() { + httpServer = make(chan bool) + go httpServerProcess(httpServer) +} + +func StopHTTPServer() { + +} + +func httpServerProcess(process <-chan bool) { + for { + select { + case <-process: + fmt.Println("HTTP Server has stopped") + return + default: + if err := handlers.RunHTTPServer(ipAddress, port); err != nil { + log.Errorln(err) + } + } + } +} diff --git a/cmd/notifiers.go b/cmd/notifiers.go new file mode 100644 index 00000000..02fa3246 --- /dev/null +++ b/cmd/notifiers.go @@ -0,0 +1,3 @@ +package main + +// AttachNotifiers will attach all the notifier's into the system diff --git a/core/checkin.go b/core/checkin.go deleted file mode 100644 index a45393fa..00000000 --- a/core/checkin.go +++ /dev/null @@ -1,154 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "fmt" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "time" -) - -type Checkin struct { - *database.CheckinObj -} - -type CheckinHit struct { - *types.CheckinHit -} - -// Select returns a *types.Checkin -func (c *Checkin) Select() *types.Checkin { - return c.Checkin -} - -// Routine for checking if the last Checkin was within its interval -func CheckinRoutine(checkin database.Checkiner) { - c := checkin.Model() - if checkin.Hits().Last() == nil { - return - } - reCheck := c.Period() -CheckinLoop: - for { - select { - case <-c.Running: - log.Infoln(fmt.Sprintf("Stopping checkin routine: %v", c.Name)) - c.Failing = false - break CheckinLoop - case <-time.After(reCheck): - log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period()))) - if c.Expected() <= 0 { - issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, checkin.Hits().Last().CreatedAt) - log.Errorln(issue) - - CreateCheckinFailure(checkin) - } - reCheck = c.Period() - } - continue - } -} - -// String will return a Checkin API string -func (c *Checkin) String() string { - return c.ApiKey -} - -func CreateCheckinFailure(checkin database.Checkiner) (int64, error) { - c := checkin.Model() - service := checkin.Service() - c.Failing = true - fail := &types.Failure{ - Issue: fmt.Sprintf("Checkin %v was not reported %v ago, it expects a request every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())), - Method: "checkin", - MethodId: c.Id, - Service: service.Id, - Checkin: c.Id, - PingTime: c.Expected().Seconds(), - } - _, err := database.Create(fail) - if err != nil { - return 0, err - } - //sort.Sort(types.FailSort(c.Failures())) - return fail.Id, err -} - -// AllCheckins returns all checkin in system -func AllCheckins() []*database.CheckinObj { - checkins := database.AllCheckins() - return checkins -} - -// SelectCheckin will find a Checkin based on the API supplied -func SelectCheckin(api string) *Checkin { - for _, s := range Services() { - for _, c := range s.Checkins() { - if c.ApiKey == api { - return &Checkin{c} - } - } - } - return nil -} - -// Create will create a new Checkin -func (c *Checkin) Delete() { - c.Close() - i := c.index() - srv := c.Service() - slice := srv.Service.Checkins - srv.Service.Checkins = append(slice[:i], slice[i+1:]...) -} - -// index returns a checkin index int for updating the *checkin.Service slice -func (c *Checkin) index() int { - for k, checkin := range c.Service().Checkins() { - if c.Id == checkin.Model().Id { - return k - } - } - return 0 -} - -// Create will create a new Checkin -func (c *Checkin) Create() (int64, error) { - c.ApiKey = utils.RandomString(7) - _, err := database.Create(c) - if err != nil { - log.Warnln(err) - return 0, err - } - c.Start() - go CheckinRoutine(c) - return c.Id, err -} - -// RecheckCheckinFailure will check if a Service Checkin has been reported yet -func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) { - between := utils.Now().Sub(utils.Now()).Seconds() - if between > float64(c.Interval) { - fmt.Println("rechecking every 15 seconds!") - time.Sleep(15 * time.Second) - guard <- struct{}{} - c.RecheckCheckinFailure(guard) - } else { - fmt.Println("i recovered!!") - } - <-guard -} diff --git a/core/configs.go b/core/configs.go deleted file mode 100644 index 7e518b00..00000000 --- a/core/configs.go +++ /dev/null @@ -1,216 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "fmt" - "github.com/go-yaml/yaml" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "github.com/pkg/errors" -) - -// ErrorResponse is used for HTTP errors to show to User -type ErrorResponse struct { - Error string -} - -// LoadConfigFile will attempt to load the 'config.yml' file in a specific directory -func LoadConfigFile(directory string) (*types.DbConfig, error) { - var configs *types.DbConfig - - log.Debugln("Attempting to read config file at: " + directory + "/config.yml") - file, err := utils.OpenFile(directory + "/config.yml") - if err != nil { - CoreApp.Setup = false - return nil, errors.Wrapf(err, "config.yml file not found at %s/config.yml - starting in setup mode", directory) - } - err = yaml.Unmarshal([]byte(file), &configs) - if err != nil { - return nil, errors.Wrap(err, "yaml file not formatted correctly") - } - log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml") - - CoreApp.config = configs - - return configs, err -} - -func Configs() *types.DbConfig { - return CoreApp.config -} - -// LoadUsingEnv will attempt to load database configs based on environment variables. If DB_CONN is set if will force this function. -func LoadUsingEnv() (*types.DbConfig, error) { - configs, err := EnvToConfig() - if err != nil { - return configs, err - } - - CoreApp.Name = utils.Getenv("NAME", "").(string) - CoreApp.Domain = utils.Getenv("DOMAIN", "").(string) - CoreApp.UseCdn = types.NewNullBool(utils.Getenv("USE_CDN", false).(bool)) - - err = CoreApp.Connect(configs, true, utils.Directory) - if err != nil { - return nil, errors.Wrap(err, "error connecting to database") - } - if err := SaveConfig(configs); err != nil { - return nil, errors.Wrap(err, "error saving configuration") - } - exists := database.Get().HasTable("core") - if !exists { - return InitialSetup(configs) - } - - CoreApp.config = configs - return configs, nil -} - -func InitialSetup(configs *types.DbConfig) (*types.DbConfig, error) { - log.Infoln(fmt.Sprintf("Core database does not exist, creating now!")) - if err := CoreApp.DropDatabase(); err != nil { - return nil, errors.Wrap(err, "error dropping database") - } - if err := CoreApp.CreateDatabase(); err != nil { - return nil, errors.Wrap(err, "error creating database") - } - CoreApp, err := InsertCore(configs) - if err != nil { - return nil, errors.Wrap(err, "error creating the core database") - } - - username := utils.Getenv("ADMIN_USER", "admin").(string) - password := utils.Getenv("ADMIN_PASSWORD", "admin").(string) - - admin := &types.User{ - Username: username, - Password: utils.HashPassword(password), - Email: "info@admin.com", - Admin: types.NewNullBool(true), - } - if _, err := database.Create(admin); err != nil { - return nil, errors.Wrap(err, "error creating admin") - } - - if err := SampleData(); err != nil { - return nil, errors.Wrap(err, "error connecting sample data") - } - - CoreApp.config = configs - return configs, err -} - -// defaultPort accepts a database type and returns its default port -func defaultPort(db string) int { - switch db { - case "mysql": - return 3306 - case "postgres": - return 5432 - case "mssql": - return 1433 - default: - return 0 - } -} - -// EnvToConfig converts environment variables to a DbConfig -func EnvToConfig() (*types.DbConfig, error) { - var err error - - dbConn := utils.Getenv("DB_CONN", "").(string) - dbHost := utils.Getenv("DB_HOST", "").(string) - dbUser := utils.Getenv("DB_USER", "").(string) - dbPass := utils.Getenv("DB_PASS", "").(string) - dbData := utils.Getenv("DB_DATABASE", "").(string) - dbPort := utils.Getenv("DB_PORT", defaultPort(dbConn)).(int) - name := utils.Getenv("NAME", "Statping").(string) - desc := utils.Getenv("DESCRIPTION", "Statping Monitoring Sample Data").(string) - user := utils.Getenv("ADMIN_USER", "admin").(string) - password := utils.Getenv("ADMIN_PASS", "admin").(string) - domain := utils.Getenv("DOMAIN", "").(string) - sqlFile := utils.Getenv("SQL_FILE", "").(string) - - if dbConn != "" && dbConn != "sqlite" { - if dbHost == "" { - return nil, errors.New("Missing DB_HOST environment variable") - } - if dbPort == 0 { - return nil, errors.New("Missing DB_PORT environment variable") - } - if dbUser == "" { - return nil, errors.New("Missing DB_USER environment variable") - } - if dbPass == "" { - return nil, errors.New("Missing DB_PASS environment variable") - } - if dbData == "" { - return nil, errors.New("Missing DB_DATABASE environment variable") - } - } - - config := &types.DbConfig{ - DbConn: dbConn, - DbHost: dbHost, - DbUser: dbUser, - DbPass: dbPass, - DbData: dbData, - DbPort: dbPort, - Project: name, - Description: desc, - Domain: domain, - Email: "", - Username: user, - Password: password, - Error: nil, - Location: utils.Directory, - SqlFile: sqlFile, - } - return config, err -} - -// SampleData runs all the sample data for a new Statping installation -func SampleData() error { - if err := InsertSampleData(); err != nil { - return errors.Wrap(err, "sample data") - } - if err := InsertSampleHits(); err != nil { - return errors.Wrap(err, "sample service hits") - } - if err := insertSampleCheckins(); err != nil { - return errors.Wrap(err, "sample checkin examples") - } - return nil -} - -// DeleteConfig will delete the 'config.yml' file -func DeleteConfig() error { - log.Debugln("deleting config yaml file", utils.Directory+"/config.yml") - err := utils.DeleteFile(utils.Directory + "/config.yml") - if err != nil { - return errors.Wrap(err, "error deleting config.yml") - } - return nil -} - -func IsSetup() bool { - if CoreApp.config != nil { - return true - } - return false -} diff --git a/core/core.go b/core/core.go deleted file mode 100644 index 8cf2fd1a..00000000 --- a/core/core.go +++ /dev/null @@ -1,175 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "errors" - "fmt" - "github.com/hunterlong/statping/core/integrations" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/source" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "net" - "os" - "time" -) - -type PluginJSON types.PluginJSON -type PluginRepos types.PluginRepos - -type Core struct { - *types.Core - services map[int64]*Service - config *types.DbConfig -} - -var ( - CoreApp *Core // CoreApp is a global variable that contains many elements - VERSION string // VERSION is set on build automatically by setting a -ldflag - log = utils.Log.WithField("type", "core") -) - -func init() { - CoreApp = NewCore() -} - -// NewCore return a new *core.Core struct -func NewCore() *Core { - CoreApp = &Core{Core: &types.Core{ - Started: time.Now().UTC(), - }} - CoreApp.services = make(map[int64]*Service) - return CoreApp -} - -// ToCore will convert *core.Core to *types.Core -func (c *Core) ToCore() *types.Core { - return c.Core -} - -// InitApp will initialize Statping -func InitApp() error { - if _, err := SelectCore(); err != nil { - return err - } - if err := InsertNotifierDB(); err != nil { - return err - } - if err := InsertIntegratorDB(); err != nil { - return err - } - if _, err := SelectAllServices(true); err != nil { - return err - } - if err := AttachNotifiers(); err != nil { - return err - } - CoreApp.Notifications = notifier.AllCommunications - if err := AddIntegrations(); err != nil { - return err - } - CoreApp.Integrations = integrations.Integrations - - go checkServices() - - database.StartMaintenceRoutine() - CoreApp.Setup = true - return nil -} - -// InsertNotifierDB inject the Statping database instance to the Notifier package -func InsertNotifierDB() error { - if !database.Available() { - err := CoreApp.Connect(CoreApp.config, false, utils.Directory) - if err != nil { - return errors.New("database connection has not been created") - } - } - notifier.SetDB(database.Get()) - return nil -} - -// InsertIntegratorDB inject the Statping database instance to the Integrations package -func InsertIntegratorDB() error { - if !database.Available() { - err := CoreApp.Connect(CoreApp.config, false, utils.Directory) - if err != nil { - return errors.New("database connection has not been created") - } - } - integrations.SetDB(database.Get()) - return nil -} - -// UsingAssets will return true if /assets folder is present -func (c Core) UsingAssets() bool { - return source.UsingAssets(utils.Directory) -} - -// SelectCore will return the CoreApp global variable and the settings/configs for Statping -func SelectCore() (*Core, error) { - if !database.Available() { - log.Traceln("database has not been initiated yet.") - return nil, errors.New("database has not been initiated yet.") - } - exists := database.Get().HasTable("core") - if !exists { - log.Errorf("core database has not been setup yet, does not have the 'core' table") - return nil, errors.New("core database has not been setup yet.") - } - db := database.Core().First(&CoreApp) - if db.Error() != nil { - return nil, db.Error() - } - CoreApp.Version = VERSION - CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true") - return CoreApp, db.Error() -} - -// GetLocalIP returns the non loopback local IP of the host -func GetLocalIP() string { - addrs, err := net.InterfaceAddrs() - if err != nil { - return "http://localhost" - } - for _, address := range addrs { - // check the address type and if it is not a loopback the display it - if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return fmt.Sprintf("http://%v", ipnet.IP.String()) - } - } - } - return "http://localhost" -} - -// ServiceOrder will reorder the services based on 'order_id' (Order) -type ServiceOrder map[int64]*Service - -// Sort interface for resroting the Services in order -func (c ServiceOrder) Len() int { return len(c) } -func (c ServiceOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] } -func (c ServiceOrder) Less(i, j int) bool { - if c[int64(i)] == nil { - return false - } - if c[int64(j)] == nil { - return false - } - return c[int64(i)].Order < c[int64(j)].Order -} diff --git a/core/core_test.go b/core/core_test.go deleted file mode 100644 index d75a4cc8..00000000 --- a/core/core_test.go +++ /dev/null @@ -1,151 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "github.com/hunterlong/statping/source" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "os" - "testing" -) - -var ( - dir string - skipNewDb bool -) - -var configs *types.DbConfig - -func init() { - dir = utils.Directory - utils.InitLogs() - source.Assets() - skipNewDb = false - SampleHits = 480 -} - -func TestNewCore(t *testing.T) { - err := TmpRecords("core.db") - t.Log(err) - require.Nil(t, err) - require.NotNil(t, CoreApp) -} - -func TestDbConfig_Save(t *testing.T) { - if skipNewDb { - t.SkipNow() - } - - config := &types.DbConfig{ - DbConn: "sqlite", - Project: "Tester", - Location: dir, - } - - err := SaveConfig(config) - require.Nil(t, err) - assert.Equal(t, "sqlite", config.DbConn) - assert.NotEmpty(t, config.ApiKey) - assert.NotEmpty(t, config.ApiSecret) -} - -func TestLoadDbConfig(t *testing.T) { - Configs, err := LoadConfigFile(dir) - assert.Nil(t, err) - assert.Equal(t, "sqlite", Configs.DbConn) - - configs = Configs -} - -func TestDbConnection(t *testing.T) { - err := CoreApp.Connect(configs, false, dir) - assert.Nil(t, err) -} - -func TestDropDatabase(t *testing.T) { - if skipNewDb { - t.SkipNow() - } - err := CoreApp.DropDatabase() - assert.Nil(t, err) -} - -func TestSeedSchemaDatabase(t *testing.T) { - if skipNewDb { - t.SkipNow() - } - err := CoreApp.CreateDatabase() - assert.Nil(t, err) -} - -func TestMigrateDatabase(t *testing.T) { - t.SkipNow() - err := CoreApp.MigrateDatabase() - assert.Nil(t, err) -} - -func TestSeedDatabase(t *testing.T) { - err := InsertLargeSampleData() - assert.Nil(t, err) -} - -func TestReLoadDbConfig(t *testing.T) { - err := CoreApp.Connect(configs, false, dir) - assert.Nil(t, err) - assert.Equal(t, "sqlite", CoreApp.config.DbConn) -} - -func TestSelectCore(t *testing.T) { - core, err := SelectCore() - assert.Nil(t, err) - assert.Equal(t, "Statping Sample Data", core.Name) -} - -func TestInsertNotifierDB(t *testing.T) { - if skipNewDb { - t.SkipNow() - } - err := InsertNotifierDB() - assert.Nil(t, err) -} - -func TestEnvToConfig(t *testing.T) { - os.Setenv("DB_CONN", "sqlite") - os.Setenv("DB_USER", "") - os.Setenv("DB_PASS", "") - os.Setenv("DB_DATABASE", "") - os.Setenv("NAME", "Testing") - os.Setenv("DOMAIN", "http://localhost:8080") - os.Setenv("DESCRIPTION", "Testing Statping") - os.Setenv("ADMIN_USER", "admin") - os.Setenv("ADMIN_PASS", "admin123") - os.Setenv("VERBOSE", "1") - config, err := EnvToConfig() - assert.Nil(t, err) - assert.Equal(t, config.DbConn, "sqlite") - assert.Equal(t, config.Domain, "http://localhost:8080") - assert.Equal(t, config.Description, "Testing Statping") - assert.Equal(t, config.Username, "admin") - assert.Equal(t, config.Password, "admin123") -} - -func TestGetLocalIP(t *testing.T) { - ip := GetLocalIP() - assert.Contains(t, ip, "http://") -} diff --git a/core/database.go b/core/database.go deleted file mode 100644 index 8a323b55..00000000 --- a/core/database.go +++ /dev/null @@ -1,410 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "bufio" - "fmt" - "github.com/pkg/errors" - "os" - "path/filepath" - "time" - - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/jinzhu/gorm/dialects/sqlite" - - "github.com/go-yaml/yaml" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "github.com/jinzhu/gorm" -) - -var ( - // DbSession stores the Statping database session - DbModels []interface{} -) - -func init() { - DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, ¬ifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}, &types.Integration{}} -} - -// CloseDB will close the database connection if available -func CloseDB() { - database.Close() -} - -//// AfterFind for Core will set the timezone -//func (c *Core) AfterFind() (err error) { -// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) -// c.UpdatedAt = utils.Timezoner(c.UpdatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for Service will set the timezone -//func (s *Service) AfterFind() (err error) { -// s.CreatedAt = utils.Timezoner(s.CreatedAt, CoreApp.Timezone) -// s.UpdatedAt = utils.Timezoner(s.UpdatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for Hit will set the timezone -//func (h *Hit) AfterFind() (err error) { -// h.CreatedAt = utils.Timezoner(h.CreatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for Failure will set the timezone -//func (f *Failure) AfterFind() (err error) { -// f.CreatedAt = utils.Timezoner(f.CreatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for USer will set the timezone -//func (u *User) AfterFind() (err error) { -// u.CreatedAt = utils.Timezoner(u.CreatedAt, CoreApp.Timezone) -// u.UpdatedAt = utils.Timezoner(u.UpdatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for Checkin will set the timezone -//func (c *Checkin) AfterFind() (err error) { -// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) -// c.UpdatedAt = utils.Timezoner(c.UpdatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for checkinHit will set the timezone -//func (c *CheckinHit) AfterFind() (err error) { -// c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) -// return -//} -// -//// AfterFind for Message will set the timezone -//func (u *Message) AfterFind() (err error) { -// u.CreatedAt = utils.Timezoner(u.CreatedAt, CoreApp.Timezone) -// u.UpdatedAt = utils.Timezoner(u.UpdatedAt, CoreApp.Timezone) -// u.StartOn = utils.Timezoner(u.StartOn.UTC(), CoreApp.Timezone) -// u.EndOn = utils.Timezoner(u.EndOn.UTC(), CoreApp.Timezone) -// return -//} - -// InsertCore create the single row for the Core settings in Statping -func InsertCore(d *types.DbConfig) (*Core, error) { - apiKey := utils.Getenv("API_KEY", utils.NewSHA1Hash(40)) - apiSecret := utils.Getenv("API_SECRET", utils.NewSHA1Hash(40)) - - core := &types.Core{ - Name: d.Project, - Description: d.Description, - ConfigFile: "config.yml", - ApiKey: apiKey.(string), - ApiSecret: apiSecret.(string), - Domain: d.Domain, - MigrationId: time.Now().Unix(), - } - - CoreApp = &Core{Core: core} - - CoreApp.config = d - - _, err := database.Create(CoreApp.Core) - return CoreApp, err -} - -func findDbFile() string { - if CoreApp.config == nil { - return findSQLin(utils.Directory) - } - if CoreApp.config.SqlFile != "" { - return CoreApp.config.SqlFile - } - return utils.Directory + "/" + types.SqliteFilename -} - -func findSQLin(path string) string { - filename := types.SqliteFilename - err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - if filepath.Ext(path) == ".db" { - fmt.Println("DB file is now: ", info.Name()) - filename = info.Name() - } - return nil - }) - if err != nil { - log.Error(err) - } - return filename -} - -// Connect will attempt to connect to the sqlite, postgres, or mysql database -func (c *Core) Connect(configs *types.DbConfig, retry bool, location string) error { - postgresSSL := os.Getenv("POSTGRES_SSLMODE") - if database.Available() { - return nil - } - var conn string - var err error - - switch configs.DbConn { - case "sqlite", "sqlite3": - conn = findDbFile() - configs.SqlFile = fmt.Sprintf("%s/%s", utils.Directory, conn) - log.Infof("SQL database file at: %s", configs.SqlFile) - configs.DbConn = "sqlite3" - case "mysql": - host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort) - conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", configs.DbUser, configs.DbPass, host, configs.DbData) - case "postgres": - sslMode := "disable" - if postgresSSL != "" { - sslMode = postgresSSL - } - conn = fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=%v", configs.DbHost, configs.DbPort, configs.DbUser, configs.DbData, configs.DbPass, sslMode) - case "mssql": - host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort) - conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", configs.DbUser, configs.DbPass, host, configs.DbData) - } - log.WithFields(utils.ToFields(c, conn)).Debugln("attempting to connect to database") - dbSession, err := database.Openw(configs.DbConn, conn) - if err != nil { - log.Debugln(fmt.Sprintf("Database connection error %s", err)) - if retry { - log.Errorln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost)) - return c.waitForDb(configs) - } else { - return err - } - } - log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database") - - maxOpenConn := utils.Getenv("MAX_OPEN_CONN", 5) - maxIdleConn := utils.Getenv("MAX_IDLE_CONN", 5) - maxLifeConn := utils.Getenv("MAX_LIFE_CONN", 2*time.Minute) - - dbSession.DB().SetMaxOpenConns(maxOpenConn.(int)) - dbSession.DB().SetMaxIdleConns(maxIdleConn.(int)) - dbSession.DB().SetConnMaxLifetime(maxLifeConn.(time.Duration)) - - if dbSession.DB().Ping() == nil { - if utils.VerboseMode >= 4 { - database.LogMode(true).Debug().SetLogger(gorm.Logger{log}) - } - log.Infoln(fmt.Sprintf("Database %v connection was successful.", configs.DbConn)) - } - - CoreApp.config = configs - - return err -} - -// waitForDb will sleep for 5 seconds and try to connect to the database again -func (c *Core) waitForDb(configs *types.DbConfig) error { - time.Sleep(5 * time.Second) - return c.Connect(configs, true, utils.Directory) -} - -// Update will save the config.yml file -func (c *Core) UpdateConfig() error { - var err error - config, err := os.Create(utils.Directory + "/config.yml") - if err != nil { - log.Errorln(err) - return err - } - defer config.Close() - - data, err := yaml.Marshal(c.config) - if err != nil { - log.Errorln(err) - return err - } - config.WriteString(string(data)) - - return err -} - -// Save will initially create the config.yml file -func SaveConfig(d *types.DbConfig) error { - config, err := os.Create(utils.Directory + "/config.yml") - if err != nil { - log.Errorln(err) - return err - } - defer config.Close() - log.WithFields(utils.ToFields(d)).Debugln("saving config file at: " + utils.Directory + "/config.yml") - - if d.ApiKey == "" || d.ApiSecret == "" { - apiKey := utils.Getenv("API_KEY", utils.NewSHA1Hash(16)) - apiSecret := utils.Getenv("API_SECRET", utils.NewSHA1Hash(16)) - d.ApiKey = apiKey.(string) - d.ApiSecret = apiSecret.(string) - } - if d.DbConn == "sqlite3" { - d.DbConn = "sqlite" - } - - data, err := yaml.Marshal(d) - if err != nil { - log.Errorln(err) - return err - } - if _, err := config.WriteString(string(data)); err != nil { - return errors.Wrap(err, "error writing to config.yml") - } - log.WithFields(utils.ToFields(d)).Infoln("Saved config file at: " + utils.Directory + "/config.yml") - - CoreApp.config = d - return err -} - -// CreateCore will initialize the global variable 'CoreApp". This global variable contains most of Statping app. -func (c *Core) CreateCore() *Core { - newCore := &types.Core{ - Name: c.Name, - Description: c.Description, - ConfigFile: utils.Directory + "/config.yml", - ApiKey: c.ApiKey, - ApiSecret: c.ApiSecret, - Domain: c.Domain, - MigrationId: time.Now().Unix(), - } - _, err := database.Create(&newCore) - if err == nil { - CoreApp = &Core{Core: newCore} - } - CoreApp, err := SelectCore() - if err != nil { - log.Errorln(err) - } - return CoreApp -} - -// DropDatabase will DROP each table Statping created -func (c *Core) DropDatabase() error { - log.Infoln("Dropping Database Tables...") - for _, t := range DbModels { - if err := database.Get().DropTableIfExists(t); err != nil { - return err.Error() - } - log.Infof("Dropped table: %T\n", t) - } - return nil -} - -// CreateDatabase will CREATE TABLES for each of the Statping elements -func (c *Core) CreateDatabase() error { - var err error - log.Infoln("Creating Database Tables...") - for _, table := range DbModels { - if err := database.Get().CreateTable(table); err.Error() != nil { - return err.Error() - } - } - if err := database.Get().Table("core").CreateTable(&types.Core{}); err.Error() != nil { - return err.Error() - } - log.Infoln("Statping Database Created") - return err -} - -// findServiceByHas will return a service that matches the SHA256 hash of a service -// Service hash example: sha256(name:EXAMPLEdomain:HTTP://DOMAIN.COMport:8080type:HTTPmethod:GET) -func findServiceByHash(hash string) *Service { - for _, service := range Services() { - if service.String() == hash { - return service - } - } - return nil -} - -func (c *Core) ServicesFromEnvFile() error { - servicesEnv := utils.Getenv("SERVICES_FILE", "").(string) - if servicesEnv == "" { - return nil - } - - file, err := os.Open(servicesEnv) - if err != nil { - return errors.Wrapf(err, "error opening 'SERVICES_FILE' at: %s", servicesEnv) - } - defer file.Close() - - var serviceLines []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - serviceLines = append(serviceLines, scanner.Text()) - } - - if len(serviceLines) == 0 { - return nil - } - - for k, service := range serviceLines { - - svr, err := utils.ValidateService(service) - if err != nil { - return errors.Wrapf(err, "invalid service at index %d in SERVICES_FILE environment variable", k) - } - if findServiceByHash(svr.String()) == nil { - if _, err := database.Create(svr); err != nil { - return errors.Wrapf(err, "could not create service %s", svr.Name) - } - log.Infof("Created new service '%s'", svr.Name) - } - - } - - return nil -} - -// MigrateDatabase will migrate the database structure to current version. -// This function will NOT remove previous records, tables or columns from the database. -// If this function has an issue, it will ROLLBACK to the previous state. -func (c *Core) MigrateDatabase() error { - log.Infoln("Migrating Database Tables...") - tx := database.Begin("migration") - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - if tx.Error() != nil { - log.Errorln(tx.Error()) - return tx.Error() - } - for _, table := range DbModels { - tx = tx.AutoMigrate(table) - } - if err := tx.Table("core").AutoMigrate(&types.Core{}); err.Error() != nil { - tx.Rollback() - log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error())) - return tx.Error() - } - - if err := tx.Commit().Error(); err != nil { - return err - } - log.Infoln("Statping Database Migrated") - - return nil -} diff --git a/core/doc.go b/core/doc.go deleted file mode 100644 index 316bba5b..00000000 --- a/core/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package core contains the main functionality of Statping. This includes everything for -// Services, Hits, Failures, Users, service checking mechanisms, databases, and notifiers -// in the notifier package -// -// More info on: https://github.com/hunterlong/statping -package core diff --git a/core/failures.go b/core/failures.go deleted file mode 100644 index c3693228..00000000 --- a/core/failures.go +++ /dev/null @@ -1,29 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "github.com/hunterlong/statping/types" -) - -type Failure struct { - *types.Failure -} - -const ( - limitedFailures = 32 - limitedHits = 32 -) diff --git a/core/groups.go b/core/groups.go deleted file mode 100644 index 38a0d19c..00000000 --- a/core/groups.go +++ /dev/null @@ -1,58 +0,0 @@ -package core - -import ( - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "sort" -) - -type Group struct { - *types.Group -} - -// SelectGroups returns all groups -func SelectGroups(includeAll bool, auth bool) map[int64]*Group { - validGroups := make(map[int64]*Group) - - groups := database.AllGroups() - - for _, g := range groups { - if !g.Public.Bool { - if auth { - validGroups[g.Id] = &Group{g.Group} - } - } else { - validGroups[g.Id] = &Group{g.Group} - } - } - sort.Sort(GroupOrder(validGroups)) - //if includeAll { - // validGroups = append(validGroups, &Group{}) - //} - return validGroups -} - -// SelectGroup returns a *core.Group -func SelectGroup(id int64) *Group { - groups := SelectGroups(true, true) - if groups[id] != nil { - return groups[id] - } - return nil -} - -// GroupOrder will reorder the groups based on 'order_id' (Order) -type GroupOrder map[int64]*Group - -// Sort interface for resorting the Groups in order -func (c GroupOrder) Len() int { return len(c) } -func (c GroupOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] } -func (c GroupOrder) Less(i, j int) bool { - if c[int64(i)] == nil { - return false - } - if c[int64(j)] == nil { - return false - } - return c[int64(i)].Order < c[int64(j)].Order -} diff --git a/core/incidents.go b/core/incidents.go deleted file mode 100644 index 7f319baa..00000000 --- a/core/incidents.go +++ /dev/null @@ -1,18 +0,0 @@ -package core - -import ( - "github.com/hunterlong/statping/types" -) - -type Incident struct { - *types.Incident -} - -type IncidentUpdate struct { - *types.IncidentUpdate -} - -// ReturnIncident returns *core.Incident based off a *types.Incident -func ReturnIncident(u *types.Incident) *Incident { - return &Incident{u} -} diff --git a/core/integrations.go b/core/integrations.go deleted file mode 100644 index ef13d6a1..00000000 --- a/core/integrations.go +++ /dev/null @@ -1,12 +0,0 @@ -package core - -import "github.com/hunterlong/statping/core/integrations" - -// AddIntegrations will attach all the integrations into the system -func AddIntegrations() error { - return integrations.AddIntegrations( - integrations.CsvIntegrator, - integrations.TraefikIntegrator, - integrations.DockerIntegrator, - ) -} diff --git a/core/messages.go b/core/messages.go deleted file mode 100644 index 4e9ba66c..00000000 --- a/core/messages.go +++ /dev/null @@ -1,29 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "github.com/hunterlong/statping/types" -) - -type Message struct { - *types.Message -} - -// ReturnMessage will convert *types.Message to *core.Message -func ReturnMessage(m *types.Message) *Message { - return &Message{m} -} diff --git a/core/notifier/audit.go b/core/notifier/audit.go deleted file mode 100644 index f1255cc4..00000000 --- a/core/notifier/audit.go +++ /dev/null @@ -1,45 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package notifier - -import ( - "fmt" - "strings" -) - -var ( - allowedVars = []string{"host", "username", "password", "port", "api_key", "api_secret", "var1", "var2"} -) - -func checkNotifierForm(n Notifier) error { - notifier := n.Select() - for _, f := range notifier.Form { - contains := contains(f.DbField, allowedVars) - if !contains { - return fmt.Errorf("the DbField '%v' is not allowed, allowed vars: %v", f.DbField, allowedVars) - } - } - return nil -} - -func contains(s string, arr []string) bool { - for _, v := range arr { - if strings.ToLower(s) == v { - return true - } - } - return false -} diff --git a/core/notifiers.go b/core/notifiers.go deleted file mode 100644 index 6296dfa3..00000000 --- a/core/notifiers.go +++ /dev/null @@ -1,21 +0,0 @@ -package core - -import ( - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/notifiers" -) - -// AttachNotifiers will attach all the notifier's into the system -func AttachNotifiers() error { - return notifier.AddNotifiers( - notifiers.Command, - notifiers.Discorder, - notifiers.Emailer, - notifiers.LineNotify, - notifiers.Mobile, - notifiers.Slacker, - notifiers.Telegram, - notifiers.Twilio, - notifiers.Webhook, - ) -} diff --git a/core/sample.go b/core/sample.go deleted file mode 100644 index c4305b07..00000000 --- a/core/sample.go +++ /dev/null @@ -1,726 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "sync" - "time" - - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/jinzhu/gorm/dialects/sqlite" -) - -var ( - sampleStart = time.Now().Add((-24 * 7) * time.Hour).UTC() - SampleHits = 9900. -) - -// InsertSampleData will create the example/dummy services for a brand new Statping installation -func InsertSampleData() error { - log.Infoln("Inserting Sample Data...") - - if err := insertSampleGroups(); err != nil { - return err - } - - createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC() - s1 := &types.Service{ - Name: "Google", - Domain: "https://google.com", - ExpectedStatus: 200, - Interval: 10, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 1, - GroupId: 1, - Permalink: types.NewNullString("google"), - VerifySSL: types.NewNullBool(true), - CreatedAt: createdOn, - } - s2 := &types.Service{ - Name: "Statping Github", - Domain: "https://github.com/hunterlong/statping", - ExpectedStatus: 200, - Interval: 30, - Type: "http", - Method: "GET", - Timeout: 20, - Order: 2, - Permalink: types.NewNullString("statping_github"), - VerifySSL: types.NewNullBool(true), - CreatedAt: createdOn, - } - s3 := &types.Service{ - Name: "JSON Users Test", - Domain: "https://jsonplaceholder.typicode.com/users", - ExpectedStatus: 200, - Interval: 60, - Type: "http", - Method: "GET", - Timeout: 30, - Order: 3, - Public: types.NewNullBool(true), - VerifySSL: types.NewNullBool(true), - GroupId: 2, - CreatedAt: createdOn, - } - s4 := &types.Service{ - Name: "JSON API Tester", - Domain: "https://jsonplaceholder.typicode.com/posts", - ExpectedStatus: 201, - Expected: types.NewNullString(`(title)": "((\\"|[statping])*)"`), - Interval: 30, - Type: "http", - Method: "POST", - PostData: types.NewNullString(`{ "title": "statping", "body": "bar", "userId": 19999 }`), - Timeout: 30, - Order: 4, - Public: types.NewNullBool(true), - VerifySSL: types.NewNullBool(true), - GroupId: 2, - CreatedAt: createdOn, - } - s5 := &types.Service{ - Name: "Google DNS", - Domain: "8.8.8.8", - Interval: 20, - Type: "tcp", - Port: 53, - Timeout: 120, - Order: 5, - Public: types.NewNullBool(true), - GroupId: 1, - CreatedAt: createdOn, - } - - if _, err := database.Create(s1); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - log.Infof("Created Service '%s'", s1.Name) - - if _, err := database.Create(s2); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - log.Infof("Created Service '%s'", s2.Name) - - if _, err := database.Create(s3); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - log.Infof("Created Service '%s'", s3.Name) - - if _, err := database.Create(s4); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - log.Infof("Created Service '%s'", s4.Name) - - if _, err := database.Create(s5); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - log.Infof("Created Service '%s'", s5.Name) - - if _, err := SelectAllServices(false); err != nil { - return types.ErrWrap(err, types.ErrorServiceSelection) - } - - if err := insertMessages(); err != nil { - return types.ErrWrap(err, types.ErrorCreateMessage) - } - - if err := insertSampleIncidents(); err != nil { - return types.ErrWrap(err, types.ErrorCreateIncident) - } - - log.Infoln("Sample data has finished importing") - - return nil -} - -func insertSampleIncidents() error { - incident1 := &types.Incident{ - Title: "Github Downtime", - Description: "This is an example of a incident for a service.", - ServiceId: 2, - } - if _, err := database.Create(incident1); err != nil { - return types.ErrWrap(err, types.ErrorCreateIncidentUp) - } - - incidentUpdate1 := &types.IncidentUpdate{ - IncidentId: incident1.Id, - Message: "Github's page for Statping seems to be sending a 501 error.", - Type: "Investigating", - } - if _, err := database.Create(incidentUpdate1); err != nil { - return types.ErrWrap(err, types.ErrorCreateIncidentUp) - } - - incidentUpdate2 := &types.IncidentUpdate{ - IncidentId: incident1.Id, - Message: "Problem is continuing and we are looking at the issues.", - Type: "Update", - } - if _, err := database.Create(incidentUpdate2); err != nil { - return types.ErrWrap(err, types.ErrorCreateIncidentUp) - } - - incidentUpdate3 := &types.IncidentUpdate{ - IncidentId: incident1.Id, - Message: "Github is now back online and everything is working.", - Type: "Resolved", - } - if _, err := database.Create(incidentUpdate3); err != nil { - return types.ErrWrap(err, types.ErrorCreateIncidentUp) - } - - return nil -} - -func insertSampleGroups() error { - group1 := &types.Group{ - Name: "Main Services", - Public: types.NewNullBool(true), - Order: 2, - } - if _, err := database.Create(group1); err != nil { - return types.ErrWrap(err, types.ErrorCreateGroup) - } - - group2 := &types.Group{ - Name: "Linked Services", - Public: types.NewNullBool(false), - Order: 1, - } - if _, err := database.Create(group2); err != nil { - return types.ErrWrap(err, types.ErrorCreateGroup) - } - - group3 := &types.Group{ - Name: "Empty Group", - Public: types.NewNullBool(false), - Order: 3, - } - if _, err := database.Create(group3); err != nil { - return types.ErrWrap(err, types.ErrorCreateGroup) - } - return nil -} - -// insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin -func insertSampleCheckins() error { - s1 := SelectService(1) - checkin1 := &types.Checkin{ - Name: "Example Checkin 1", - ServiceId: s1.Id, - Interval: 300, - GracePeriod: 300, - ApiKey: utils.RandomString(7), - } - - if _, err := database.Create(checkin1); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s2 := SelectService(1) - checkin2 := &types.Checkin{ - Name: "Example Checkin 2", - ServiceId: s2.Id, - Interval: 900, - GracePeriod: 300, - ApiKey: utils.RandomString(7), - } - - if _, err := database.Create(checkin2); err != nil { - return types.ErrWrap(err, types.ErrorCreateCheckinHit) - } - - checkTime := time.Now().UTC().Add(-24 * time.Hour) - for i := 0; i <= 60; i++ { - checkHit := &types.CheckinHit{ - Checkin: checkin1.Id, - From: "192.168.0.1", - CreatedAt: checkTime.UTC(), - } - - if _, err := database.Create(checkHit); err != nil { - return types.ErrWrap(err, types.ErrorCreateCheckinHit) - } - - checkTime = checkTime.Add(10 * time.Minute) - } - return nil -} - -// InsertSampleHits will create a couple new hits for the sample services -func InsertSampleHits() error { - tx := database.Begin(&types.Hit{}) - sg := new(sync.WaitGroup) - for i := int64(1); i <= 5; i++ { - sg.Add(1) - service := SelectService(i) - seed := time.Now().UnixNano() - log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name)) - createdAt := sampleStart - p := utils.NewPerlin(2., 2., 10, seed) - go func(sg *sync.WaitGroup) { - defer sg.Done() - for hi := 0.; hi <= float64(SampleHits); hi++ { - latency := p.Noise1D(hi / 500) - createdAt = createdAt.Add(60 * time.Second) - hit := &types.Hit{ - Service: service.Id, - CreatedAt: createdAt, - Latency: latency, - } - tx = tx.Create(&hit) - } - }(sg) - } - sg.Wait() - if err := tx.Commit().Error(); err != nil { - log.Errorln(err) - return types.ErrWrap(err, types.ErrorCreateSampleHits) - } - return nil -} - -// insertSampleCore will create a new Core for the seed -func insertSampleCore() error { - - apiKey := utils.Getenv("API_KEY", "samplekey") - apiSecret := utils.Getenv("API_SECRET", "samplesecret") - - core := &types.Core{ - Name: "Statping Sample Data", - Description: "This data is only used to testing", - ApiKey: apiKey.(string), - ApiSecret: apiSecret.(string), - Domain: "http://localhost:8080", - Version: "test", - CreatedAt: time.Now().UTC(), - UseCdn: types.NewNullBool(false), - Footer: types.NewNullString(""), - } - - if _, err := database.Create(core); err != nil { - return types.ErrWrap(err, types.ErrorCreateCore) - } - - return nil -} - -// insertSampleUsers will create 2 admin users for a seed database -func insertSampleUsers() error { - u2 := &types.User{ - Username: "testadmin", - Password: "password123", - Email: "info@betatude.com", - Admin: types.NewNullBool(true), - } - - if _, err := database.Create(u2); err != nil { - return types.ErrWrap(err, types.ErrorCreateUser) - } - - u3 := &types.User{ - Username: "testadmin2", - Password: "password123", - Email: "info@adminhere.com", - Admin: types.NewNullBool(true), - } - - if _, err := database.Create(u3); err != nil { - return types.ErrWrap(err, types.ErrorCreateUser) - } - - return nil -} - -func insertMessages() error { - m1 := &types.Message{ - Title: "Routine Downtime", - Description: "This is an example a upcoming message for a service!", - ServiceId: 1, - StartOn: time.Now().UTC().Add(15 * time.Minute), - EndOn: time.Now().UTC().Add(2 * time.Hour), - } - - if _, err := database.Create(m1); err != nil { - return types.ErrWrap(err, types.ErrorCreateMessage) - } - - m2 := &types.Message{ - Title: "Server Reboot", - Description: "This is another example a upcoming message for a service!", - ServiceId: 3, - StartOn: time.Now().UTC().Add(15 * time.Minute), - EndOn: time.Now().UTC().Add(2 * time.Hour), - } - - if _, err := database.Create(m2); err != nil { - return types.ErrWrap(err, types.ErrorCreateMessage) - } - return nil -} - -// InsertLargeSampleData will create the example/dummy services for testing the Statping server -func InsertLargeSampleData() error { - if err := insertSampleCore(); err != nil { - return err - } - if err := InsertSampleData(); err != nil { - return err - } - if err := insertSampleUsers(); err != nil { - return err - } - if err := insertSampleCheckins(); err != nil { - return err - } - if err := insertMessages(); err != nil { - return err - } - createdOn := time.Now().UTC().Add((-24 * 90) * time.Hour) - s6 := &types.Service{ - Name: "JSON Lint", - Domain: "https://jsonlint.com", - ExpectedStatus: 200, - Interval: 15, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 6, - CreatedAt: createdOn, - } - - if _, err := database.Create(s6); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s7 := &types.Service{ - Name: "Demo Page", - Domain: "https://demo.statping.com", - ExpectedStatus: 200, - Interval: 30, - Type: "http", - Method: "GET", - Timeout: 15, - Order: 7, - CreatedAt: createdOn, - } - - if _, err := database.Create(s7); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s8 := &types.Service{ - Name: "Golang", - Domain: "https://golang.org", - ExpectedStatus: 200, - Interval: 15, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 8, - } - - if _, err := database.Create(s8); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s9 := &types.Service{ - Name: "Santa Monica", - Domain: "https://www.santamonica.com", - ExpectedStatus: 200, - Interval: 15, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 9, - CreatedAt: createdOn, - } - - if _, err := database.Create(s9); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s10 := &types.Service{ - Name: "Oeschs Die Dritten", - Domain: "https://www.oeschs-die-dritten.ch/en/", - ExpectedStatus: 200, - Interval: 15, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 10, - CreatedAt: createdOn, - } - - if _, err := database.Create(s10); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s11 := &types.Service{ - Name: "XS Project - Bochka, Bass, Kolbaser", - Domain: "https://www.youtube.com/watch?v=VLW1ieY4Izw", - ExpectedStatus: 200, - Interval: 60, - Type: "http", - Method: "GET", - Timeout: 20, - Order: 11, - CreatedAt: createdOn, - } - - if _, err := database.Create(s11); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s12 := &types.Service{ - Name: "Github", - Domain: "https://github.com/hunterlong", - ExpectedStatus: 200, - Interval: 60, - Type: "http", - Method: "GET", - Timeout: 20, - Order: 12, - CreatedAt: createdOn, - } - - if _, err := database.Create(s12); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s13 := &types.Service{ - Name: "Failing URL", - Domain: "http://thisdomainisfakeanditsgoingtofail.com", - ExpectedStatus: 200, - Interval: 45, - Type: "http", - Method: "GET", - Timeout: 10, - Order: 13, - CreatedAt: createdOn, - } - - if _, err := database.Create(s13); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s14 := &types.Service{ - Name: "Oesch's die Dritten - Die Jodelsprache", - Domain: "https://www.youtube.com/watch?v=k3GTxRt4iao", - ExpectedStatus: 200, - Interval: 60, - Type: "http", - Method: "GET", - Timeout: 12, - Order: 14, - CreatedAt: createdOn, - } - - if _, err := database.Create(s14); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - s15 := &types.Service{ - Name: "Gorm", - Domain: "http://gorm.io/", - ExpectedStatus: 200, - Interval: 30, - Type: "http", - Method: "GET", - Timeout: 12, - Order: 15, - CreatedAt: createdOn, - } - - if _, err := database.Create(s15); err != nil { - return types.ErrWrap(err, types.ErrorCreateService) - } - - var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour) - - CoreApp.services, _ = SelectAllServices(false) - - insertHitRecords(dayAgo, 5450) - - insertFailureRecords(dayAgo, 730) - - return nil -} - -// insertFailureRecords will create failures for 15 services from seed -func insertFailureRecords(since time.Time, amount int) { - for i := int64(14); i <= 15; i++ { - service := SelectService(i) - log.Infoln(fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Name)) - createdAt := since - - for fi := 1; fi <= amount; fi++ { - createdAt = createdAt.Add(2 * time.Minute) - - failure := &types.Failure{ - Service: service.Id, - Issue: "testing right here", - CreatedAt: createdAt, - } - - database.Create(failure) - } - } -} - -// insertHitRecords will create successful Hit records for 15 services -func insertHitRecords(since time.Time, amount int) error { - for i := int64(1); i <= 15; i++ { - service := SelectService(i) - log.Infoln(fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name)) - createdAt := since - p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano()) - for hi := 1; hi <= amount; hi++ { - latency := p.Noise1D(float64(hi / 10)) - createdAt = createdAt.Add(1 * time.Minute) - hit := &types.Hit{ - Service: service.Id, - CreatedAt: createdAt.UTC(), - Latency: latency, - } - if _, err := database.Create(hit); err != nil { - return types.ErrWrap(err, types.ErrorCreateHit, service.Id) - } - } - - } - return nil -} - -// TmpRecords is used for testing Statping. It will create a SQLite database file -// with sample data and store it in the /tmp folder to be used by the tests. -func TmpRecords(dbFile string) error { - var sqlFile = utils.Directory + "/" + dbFile - if err := utils.CreateDirectory(utils.Directory + "/tmp"); err != nil { - log.Error(err) - } - var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename - SampleHits = 480 - - var err error - CoreApp = NewCore() - CoreApp.Name = "Tester" - CoreApp.Setup = true - configs := &types.DbConfig{ - DbConn: "sqlite", - Project: "Tester", - Location: utils.Directory, - SqlFile: sqlFile, - } - log.Infoln("saving config.yml in: " + utils.Directory) - if err := SaveConfig(configs); err != nil { - log.Error(err) - } - log.Infoln("loading config.yml from: " + utils.Directory) - if configs, err = LoadConfigFile(utils.Directory); err != nil { - log.Error(err) - } - log.Infoln("connecting to database") - - exists := utils.FileExists(tmpSqlFile) - if exists { - log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile) - if err := utils.DeleteFile(sqlFile); err != nil { - log.Error(err) - } - if err := utils.CopyFile(tmpSqlFile, sqlFile); err != nil { - log.Error(err) - } - log.Infoln("loading config.yml from: " + utils.Directory) - - if err := CoreApp.Connect(configs, false, utils.Directory); err != nil { - log.Error(err) - } - log.Infoln("selecting the Core variable") - if _, err := SelectCore(); err != nil { - log.Error(err) - } - log.Infoln("inserting notifiers into database") - if err := InsertNotifierDB(); err != nil { - log.Error(err) - } - log.Infoln("inserting integrations into database") - if err := InsertIntegratorDB(); err != nil { - log.Error(err) - } - log.Infoln("loading all services") - if _, err := SelectAllServices(false); err != nil { - return err - } - if err := AttachNotifiers(); err != nil { - log.Error(err) - } - if err := AddIntegrations(); err != nil { - log.Error(err) - } - CoreApp.Notifications = notifier.AllCommunications - return nil - } - - log.Infoln(tmpSqlFile + " not found, creating a new database...") - - if err := CoreApp.Connect(configs, false, utils.Directory); err != nil { - return err - } - log.Infoln("creating database") - if err := CoreApp.CreateDatabase(); err != nil { - return err - } - log.Infoln("migrating database") - if err := CoreApp.MigrateDatabase(); err != nil { - return err - } - log.Infoln("insert large sample data into database") - if err := InsertLargeSampleData(); err != nil { - return err - } - log.Infoln("selecting the Core variable") - if CoreApp, err = SelectCore(); err != nil { - return err - } - log.Infoln("inserting notifiers into database") - if err := InsertNotifierDB(); err != nil { - return err - } - log.Infoln("inserting integrations into database") - if err := InsertIntegratorDB(); err != nil { - return err - } - log.Infoln("loading all services") - if _, err := SelectAllServices(false); err != nil { - return err - } - log.Infoln("copying sql database file to: " + tmpSqlFile) - if err := utils.CopyFile(sqlFile, tmpSqlFile); err != nil { - return err - } - return err -} diff --git a/core/services.go b/core/services.go deleted file mode 100644 index 3d797abf..00000000 --- a/core/services.go +++ /dev/null @@ -1,163 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "sort" - "time" -) - -type Service struct { - *database.ServiceObj -} - -func Services() map[int64]*Service { - return CoreApp.services -} - -// SelectService returns a *core.Service from in memory -func SelectService(id int64) *Service { - service := CoreApp.services[id] - if service != nil { - return service - } - return nil -} - -func (s *Service) AfterCreate(obj interface{}, err error) { - -} - -// CheckinProcess runs the checkin routine for each checkin attached to service -func CheckinProcess(s database.Servicer) { - for _, c := range s.Checkins() { - c.Start() - go CheckinRoutine(c) - } -} - -// SelectAllServices returns a slice of *core.Service to be store on []*core.Services -// should only be called once on startup. -func SelectAllServices(start bool) (map[int64]*Service, error) { - services := make(map[int64]*Service) - if len(CoreApp.services) > 0 { - return CoreApp.services, nil - } - - for _, s := range database.Services() { - if start { - s.Start() - CheckinProcess(s) - } - - fails := s.Failures().Last(limitedFailures) - s.Service.Failures = fails - - for _, c := range s.Checkins() { - s.Service.Checkins = append(s.Service.Checkins, c.Checkin) - } - - // collect initial service stats - s.UpdateStats() - - services[s.Id] = &Service{s} - } - - CoreApp.services = services - reorderServices() - - return services, nil -} - -func wrapFailures(f []*types.Failure) []*Failure { - var fails []*Failure - for _, v := range f { - fails = append(fails, &Failure{v}) - } - return fails -} - -// reorderServices will sort the services based on 'order_id' -func reorderServices() { - sort.Sort(ServiceOrder(CoreApp.services)) -} - -// updateService will update a service in the []*core.Services slice -func updateService(s *Service) { - CoreApp.services[s.Id] = s -} - -// Delete will remove a service from the database, it will also end the service checking go routine -func (s *Service) Delete() error { - err := database.Delete(s) - if err != nil { - log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err)) - return err - } - s.Close() - CoreApp.services[s.Id] = nil - reorderServices() - notifier.OnDeletedService(s.Service) - return err -} - -// Update will update a service in the database, the service's checking routine can be restarted by passing true -func Update(s *Service, restart bool) error { - err := database.Update(s) - if err != nil { - log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err)) - return err - } - // clear the notification queue for a service - if !s.AllowNotifications.Bool { - for _, n := range CoreApp.Notifications { - notif := n.(notifier.Notifier).Select() - notif.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) - } - } - if restart { - s.Close() - s.Start() - s.SleepDuration = s.Duration() - go ServiceCheckQueue(s, true) - } - reorderServices() - updateService(s) - notifier.OnUpdatedService(s.Service) - return err -} - -// Create will create a service and insert it into the database -func Create(srv database.Servicer, check bool) (int64, error) { - s := srv.Model() - s.CreatedAt = time.Now().UTC() - _, err := database.Create(s) - if err != nil { - log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err)) - return 0, err - } - service := &Service{s} - s.Start() - CoreApp.services[service.Id] = service - go ServiceCheckQueue(service, check) - reorderServices() - notifier.OnNewService(s.Service) - return s.Id, nil -} diff --git a/core/sparklines.go b/core/sparklines.go deleted file mode 100644 index 9a8bc959..00000000 --- a/core/sparklines.go +++ /dev/null @@ -1 +0,0 @@ -package core diff --git a/core/users.go b/core/users.go deleted file mode 100644 index a37ddefa..00000000 --- a/core/users.go +++ /dev/null @@ -1,115 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core - -import ( - "fmt" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "golang.org/x/crypto/bcrypt" - "time" -) - -type User struct { - *database.UserObj -} - -// ReturnUser returns *core.User based off a *types.User -func uwrap(u *database.UserObj) *User { - return &User{u} -} - -// SelectUser returns the User based on the User's ID. -func SelectUser(id int64) (*User, error) { - user, err := database.User(id) - if err != nil { - return nil, err - } - return uwrap(user), err -} - -// SelectUsername returns the User based on the User's username -func SelectUsername(username string) (*User, error) { - user, err := database.UserByUsername(username) - if err != nil { - return nil, err - } - return uwrap(user), err -} - -// Delete will remove the User record from the database -func (u *User) Delete() error { - return database.Delete(u) -} - -// Update will update the User's record in database -func (u *User) Update() error { - u.ApiKey = utils.NewSHA1Hash(5) - u.ApiSecret = utils.NewSHA1Hash(10) - return database.Update(u) -} - -// Create will insert a new User into the database -func (u *User) Create() (int64, error) { - u.CreatedAt = time.Now().UTC() - u.Password = utils.HashPassword(u.Password) - u.ApiKey = utils.NewSHA1Hash(5) - u.ApiSecret = utils.NewSHA1Hash(10) - - user, err := database.Create(u) - if err != nil { - return 0, err - } - if user.Id == 0 { - log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, err)) - return 0, err - } - return u.Id, err -} - -// SelectAllUsers returns all users -func SelectAllUsers() []*types.User { - users := database.AllUsers() - return users -} - -// AuthUser will return the User and a boolean if authentication was correct. -// AuthUser accepts username, and password as a string -func AuthUser(username, password string) (*types.User, bool) { - user, err := database.UserByUsername(username) - if err != nil { - log.Warnln(fmt.Errorf("user %v not found", username)) - return nil, false - } - - fmt.Println(username, password) - - fmt.Println(username, user.Password) - - if CheckHash(password, user.Password) { - user.UpdatedAt = time.Now().UTC() - database.Update(user) - return user.User, true - } - return nil, false -} - -// CheckHash returns true if the password matches with a hashed bcrypt password -func CheckHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} diff --git a/database/checkin_hits.go b/database/checkin_hits.go deleted file mode 100644 index 00fe2eec..00000000 --- a/database/checkin_hits.go +++ /dev/null @@ -1,8 +0,0 @@ -package database - -import "github.com/hunterlong/statping/types" - -type CheckinHitObj struct { - hits []*types.CheckinHit - o *Object -} diff --git a/database/checkins.go b/database/checkins.go deleted file mode 100644 index 2177b220..00000000 --- a/database/checkins.go +++ /dev/null @@ -1,131 +0,0 @@ -package database - -import ( - "fmt" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "time" -) - -type CheckinObj struct { - *types.Checkin - o *Object - - Checkiner -} - -type Checkiner interface { - Hits() *CheckinHitObj - Failures() *FailureObj - Model() *types.Checkin - Service() *ServiceObj -} - -func (c *CheckinObj) BeforeCreate() (err error) { - c.ApiKey = utils.RandomString(7) - if c.CreatedAt.IsZero() { - c.CreatedAt = time.Now().UTC() - c.UpdatedAt = time.Now().UTC() - } - return -} - -func (c *CheckinObj) BeforeDelete(tx Database) (err error) { - q := tx.Services().Where("id = ?", c.ServiceId). - Update("group_id", 0) - return q.Error() -} - -func Checkin(id int64) (*CheckinObj, error) { - var checkin types.Checkin - query := database.Checkins().Where("id = ?", id) - finder := query.Find(&checkin) - return &CheckinObj{Checkin: &checkin, o: wrapObject(id, &checkin, query)}, finder.Error() -} - -func CheckinByKey(api string) (*CheckinObj, error) { - var checkin types.Checkin - query := database.Checkins().Where("api = ?", api) - finder := query.Find(&checkin) - return &CheckinObj{Checkin: &checkin, o: wrapObject(checkin.Id, &checkin, query)}, finder.Error() -} - -func wrapCheckins(all []*types.Checkin, db Database) []*CheckinObj { - var arr []*CheckinObj - for _, v := range all { - arr = append(arr, &CheckinObj{Checkin: v, o: wrapObject(v.Id, v, db)}) - } - return arr -} - -func AllCheckins() []*CheckinObj { - var checkins []*types.Checkin - query := database.Checkins() - query.Find(&checkins) - return wrapCheckins(checkins, query) -} - -func (s *CheckinObj) Service() *ServiceObj { - var srv *types.Service - q := database.Services().Where("id = ?", s.ServiceId) - q.Find(&srv) - return &ServiceObj{ - Service: srv, - o: wrapObject(s.ServiceId, srv, q), - } -} - -func (s *CheckinObj) Failures() *FailureObj { - q := database.Failures(). - Where("method = 'checkin' AND id = ?", s.Id). - Where("method = 'checkin'") - return &FailureObj{wrapObject(s.Id, nil, q)} -} - -func (s *CheckinObj) object() *Object { - return s.o -} - -func (c *CheckinObj) Model() *types.Checkin { - return c.Checkin -} - -// Period will return the duration of the Checkin interval -func (c *CheckinObj) Period() time.Duration { - duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval)) - return duration -} - -// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response) -func (c *CheckinObj) Grace() time.Duration { - duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod)) - return duration -} - -// Expected returns the duration of when the serviec should receive a Checkin -func (c *CheckinObj) Expected() time.Duration { - last := c.Hits().Last() - now := time.Now().UTC() - lastDir := now.Sub(last.CreatedAt) - sub := time.Duration(c.Period() - lastDir) - return sub -} - -// Last returns the last checkinHit for a Checkin -func (c *CheckinObj) Hits() *CheckinHitObj { - var checkinHits []*types.CheckinHit - query := database.CheckinHits().Where("checkin = ?", c.Id) - query.Find(&checkinHits) - return &CheckinHitObj{checkinHits, wrapObject(c.Id, checkinHits, query)} -} - -// Last returns the last checkinHit for a Checkin -func (c *CheckinHitObj) Last() *types.CheckinHit { - var last types.CheckinHit - c.o.db.Last(&last) - return &last -} - -func (c *CheckinObj) Link() string { - return fmt.Sprintf("%v/checkin/%v", "DOMAINHERE", c.ApiKey) -} diff --git a/database/crud.go b/database/crud.go deleted file mode 100644 index 0574b6b1..00000000 --- a/database/crud.go +++ /dev/null @@ -1,80 +0,0 @@ -package database - -import ( - "github.com/hunterlong/statping/types" - "reflect" - - _ "github.com/jinzhu/gorm/dialects/mysql" - _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/jinzhu/gorm/dialects/sqlite" -) - -type CrudObject interface { - Create() -} - -type Object struct { - Id int64 - model interface{} - db Database -} - -type isObject interface { - object() *Object -} - -func wrapObject(id int64, model interface{}, db Database) *Object { - return &Object{ - Id: id, - model: model, - db: db, - } -} - -func modelId(model interface{}) int64 { - switch model.(type) { - case *types.Core: - return 0 - default: - iface := reflect.ValueOf(model) - field := iface.Elem().FieldByName("Id") - return field.Int() - } -} - -type CreateCallback func(interface{}, error) - -func runCallbacks(data interface{}, err error, fns ...AfterCreate) { - for _, fn := range fns { - fn.AfterCreate(data, err) - } -} - -type AfterCreate interface { - AfterCreate(interface{}, error) -} - -func Create(data interface{}, fns ...AfterCreate) (*Object, error) { - model := database.Model(&data) - if err := model.Create(data).Error(); err != nil { - runCallbacks(data, err, fns...) - return nil, err - } - obj := &Object{ - Id: modelId(data), - model: data, - db: model, - } - runCallbacks(data, nil, fns...) - return obj, nil -} - -func Update(data interface{}) error { - model := database.Model(&data) - return model.Update(&data).Error() -} - -func Delete(data interface{}) error { - model := database.Model(&data) - return model.Delete(data).Error() -} diff --git a/database/database.go b/database/database.go index 0b2201b5..faaabfc1 100644 --- a/database/database.go +++ b/database/database.go @@ -2,9 +2,7 @@ package database import ( "database/sql" - "github.com/hunterlong/statping/types" "github.com/jinzhu/gorm" - "net/http" "strings" "time" @@ -111,25 +109,10 @@ type Database interface { FormatTime(t time.Time) string ParseTime(t string) (time.Time, error) - - Requests(*http.Request, isObject) Database - - Objects } -type Objects interface { - Core() Database - Services() Database - Users() Database - Groups() Database - Incidents() Database - IncidentUpdates() Database - Hits() Database - Failures() Database - Checkins() Database - CheckinHits() Database - Messages() Database - Integrations() Database +func DB() Database { + return database } func Close() error { @@ -152,17 +135,6 @@ func Begin(model interface{}) Database { return database.Model(model).Begin() } -func Core() Database { - return database.Core() -} - -func Get() Database { - if Available() { - return database - } - return nil -} - func Available() bool { if database == nil { return false @@ -173,59 +145,6 @@ func Available() bool { return true } -func (d *Db) Core() Database { - return d.Table("core").Model(&types.Service{}) -} - -func (d *Db) Services() Database { - return d.Model(&types.Service{}) -} - -func (d *Db) Users() Database { - return d.Model(&types.User{}) -} - -func (d *Db) Groups() Database { - return d.Model(&types.Group{}) -} - -func (d *Db) Incidents() Database { - return d.Model(&types.Incident{}) -} - -func (d *Db) IncidentUpdates() Database { - return d.Model(&types.IncidentUpdate{}) -} - -func (d *Db) Hits() Database { - return d.Model(&types.Hit{}) -} - -func (d *Db) Integrations() Database { - return d.Model(&types.Integration{}) -} - -func (d *Db) Failures() Database { - return d.Model(&types.Failure{}) -} - -func (d *Db) Checkins() Database { - return d.Model(&types.Checkin{}) -} - -func (d *Db) CheckinHits() Database { - return d.Model(&types.CheckinHit{}) -} - -func (d *Db) Messages() Database { - return d.Model(&types.Message{}) -} - -func (it *Db) Requests(r *http.Request, o isObject) Database { - g := ParseQueries(r, o) - return g.db -} - func (it *Db) MultipleSelects(args ...string) Database { joined := strings.Join(args, ", ") return it.Select(joined) @@ -245,7 +164,8 @@ func Openw(dialect string, args ...interface{}) (db Database, err error) { if err != nil { return nil, err } - database = Wrap(gormdb) + db = Wrap(gormdb) + database = db return database, err } diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 00000000..80083a55 --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,44 @@ +package database + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDbConnection(t *testing.T) { + err := CoreApp.Connect(configs, false, dir) + assert.Nil(t, err) +} + +func TestDropDatabase(t *testing.T) { + if skipNewDb { + t.SkipNow() + } + err := CoreApp.DropDatabase() + assert.Nil(t, err) +} + +func TestSeedSchemaDatabase(t *testing.T) { + if skipNewDb { + t.SkipNow() + } + err := CoreApp.CreateDatabase() + assert.Nil(t, err) +} + +func TestMigrateDatabase(t *testing.T) { + t.SkipNow() + err := CoreApp.MigrateDatabase() + assert.Nil(t, err) +} + +func TestSeedDatabase(t *testing.T) { + err := InsertLargeSampleData() + assert.Nil(t, err) +} + +func TestReLoadDbConfig(t *testing.T) { + err := CoreApp.Connect(configs, false, dir) + assert.Nil(t, err) + assert.Equal(t, "sqlite", CoreApp.config.DbConn) +} diff --git a/database/failures.go b/database/failures.go deleted file mode 100644 index 8a151aa0..00000000 --- a/database/failures.go +++ /dev/null @@ -1,57 +0,0 @@ -package database - -import ( - "github.com/hunterlong/statping/types" - "time" -) - -type FailureObj struct { - o *Object -} - -type Failurer interface { - Model() []*types.Failure -} - -func (f *FailureObj) Model() []*types.Failure { - return f.All() -} - -func (f *FailureObj) All() []*types.Failure { - var fails []*types.Failure - f.o.db.Find(&fails) - return fails -} - -func AllFailures() int { - var amount int - database.Failures().Count(&amount) - return amount -} - -func (f *FailureObj) DeleteAll() error { - query := database.Exec(`DELETE FROM failures WHERE service = ?`, f.o.Id) - return query.Error() -} - -func (f *FailureObj) Last(amount int) []*types.Failure { - var fail []*types.Failure - f.o.db.Limit(amount).Find(&fail) - return fail -} - -func (f *FailureObj) Count() int { - var amount int - f.o.db.Count(&amount) - return amount -} - -func (f *FailureObj) Since(t time.Time) []*types.Failure { - var fails []*types.Failure - f.o.db.Since(t).Find(&fails) - return fails -} - -func (f *FailureObj) object() *Object { - return f.o -} diff --git a/database/group.go b/database/group.go deleted file mode 100644 index a7bf8f17..00000000 --- a/database/group.go +++ /dev/null @@ -1,47 +0,0 @@ -package database - -import "github.com/hunterlong/statping/types" - -type GroupObj struct { - *types.Group - o *Object - - Grouper -} - -type Grouper interface { - Services() []*types.Service - Model() *types.Group -} - -func AllGroups() []*GroupObj { - var groups []*types.Group - query := database.Groups() - query.Find(&groups) - return wrapGroups(groups, query) -} - -func (g *Db) GetGroup(id int64) (*GroupObj, error) { - var group types.Group - query := database.Groups().Where("id = ?", id) - finder := query.Find(&group) - return &GroupObj{Group: &group, o: wrapObject(id, &group, query)}, finder.Error() -} - -func (g *GroupObj) Services() []*types.Service { - var services []*types.Service - database.Services().Where("group = ?", g.Id).Find(&services) - return services -} - -func (g *GroupObj) Model() *types.Group { - return g.Group -} - -func wrapGroups(all []*types.Group, db Database) []*GroupObj { - var arr []*GroupObj - for _, v := range all { - arr = append(arr, &GroupObj{Group: v, o: wrapObject(v.Id, v, db)}) - } - return arr -} diff --git a/database/grouping.go b/database/grouping.go index cb9bc4ac..2a16332c 100644 --- a/database/grouping.go +++ b/database/grouping.go @@ -38,7 +38,7 @@ type GroupQuery struct { } func (b GroupQuery) Find(data interface{}) error { - return b.db.Find(&data).Error() + return b.db.Find(data).Error() } func (b GroupQuery) Database() Database { @@ -176,6 +176,10 @@ func (g *GroupQuery) duration() time.Duration { } } +type isObject interface { + Db() Database +} + func ParseQueries(r *http.Request, o isObject) *GroupQuery { fields := parseGet(r) grouping := fields.Get("group") @@ -192,7 +196,7 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery { limit = 10000 } - db := o.object().db + db := o.Db() query := &GroupQuery{ Start: time.Unix(startField, 0).UTC(), @@ -205,22 +209,22 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery { db: db, } + if startField == 0 { + query.Start = time.Now().Add(-3 * types.Month).UTC() + } + if endField == 0 { + query.End = time.Now().UTC() + } + if query.Limit != 0 { db = db.Limit(query.Limit) } if query.Offset > 0 { db = db.Offset(query.Offset) } - if !query.Start.IsZero() && !query.End.IsZero() { - db = db.Where("created_at BETWEEN ? AND ?", db.FormatTime(query.Start), db.FormatTime(query.End)) - } else { - if !query.Start.IsZero() { - db = db.Where("created_at > ?", db.FormatTime(query.Start)) - } - if !query.End.IsZero() { - db = db.Where("created_at < ?", db.FormatTime(query.End)) - } - } + + db = db.Where("created_at BETWEEN ? AND ?", db.FormatTime(query.Start), db.FormatTime(query.End)) + if query.Order != "" { db = db.Order(query.Order) } diff --git a/database/hits.go b/database/hits.go index 41c4577c..636bab89 100644 --- a/database/hits.go +++ b/database/hits.go @@ -1,53 +1 @@ package database - -import ( - "github.com/hunterlong/statping/types" - "time" -) - -type HitObj struct { - o *Object -} - -func (h *HitObj) All() []*types.Hit { - var fails []*types.Hit - h.o.db.Find(&fails) - return fails -} - -func (s *ServiceObj) CreateHit(hit *types.Hit) *HitObj { - hit.Service = s.Id - database.Create(hit) - return &HitObj{wrapObject(hit.Id, hit, database.Hits().Where("id = ?", hit.Id))} -} - -func (h *HitObj) Sum() float64 { - result := struct { - amount float64 - }{0} - - h.o.db.Select("AVG(latency) as amount").Scan(&result).Debug() - return result.amount -} - -func (h *HitObj) Last(amount int) []*types.Hit { - var hits []*types.Hit - h.o.db.Limit(amount).Find(&hits) - return hits -} - -func (h *HitObj) Since(t time.Time) []*types.Hit { - var hits []*types.Hit - h.o.db.Since(t).Find(&hits) - return hits -} - -func (h *HitObj) Count() int { - var amount int - h.o.db.Count(&amount) - return amount -} - -func (h *HitObj) object() *Object { - return h.o -} diff --git a/database/incident.go b/database/incident.go index 80fc91fc..636bab89 100644 --- a/database/incident.go +++ b/database/incident.go @@ -1,31 +1 @@ package database - -import "github.com/hunterlong/statping/types" - -type IncidentObj struct { - *types.Incident - o *Object -} - -func Incident(id int64) (*IncidentObj, error) { - var incident types.Incident - query := database.Incidents().Where("id = ?", id) - finder := query.Find(&incident) - return &IncidentObj{Incident: &incident, o: wrapObject(id, &incident, query)}, finder.Error() -} - -func AllIncidents() []*types.Incident { - var incidents []*types.Incident - database.Incidents().Find(&incidents) - return incidents -} - -func (i *IncidentObj) Updates() []*types.IncidentUpdate { - var incidents []*types.IncidentUpdate - database.IncidentUpdates().Where("incident = ?", i.Id).Find(&incidents) - return incidents -} - -func (i *IncidentObj) object() *Object { - return i.o -} diff --git a/database/integration.go b/database/integration.go index ba987624..636bab89 100644 --- a/database/integration.go +++ b/database/integration.go @@ -1,19 +1 @@ package database - -import "github.com/hunterlong/statping/types" - -type IntegrationObj struct { - *types.Integration - o *Object -} - -func Integration(id int64) (*IntegrationObj, error) { - var integration types.Integration - query := database.Model(&types.Integration{}).Where("id = ?", id) - finder := query.Find(&integration) - return &IntegrationObj{Integration: &integration, o: wrapObject(id, &integration, query)}, finder.Error() -} - -func (i *IntegrationObj) object() *Object { - return i.o -} diff --git a/database/interface.go b/database/interface.go new file mode 100644 index 00000000..60c4e9c8 --- /dev/null +++ b/database/interface.go @@ -0,0 +1,27 @@ +package database + +type DbObject interface { + Create() error + Update() error + Delete() error +} + +type Sampler interface { + Sample() DbObject +} + +func MigrateTable(table interface{}) error { + tx := database.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + tx = tx.AutoMigrate(table) + + if err := tx.Commit().Error(); err != nil { + return err + } + return nil +} diff --git a/database/message.go b/database/message.go index 28805381..636bab89 100644 --- a/database/message.go +++ b/database/message.go @@ -1,25 +1 @@ package database - -import "github.com/hunterlong/statping/types" - -type MessageObj struct { - *types.Message - o *Object -} - -func Message(id int64) (*MessageObj, error) { - var message types.Message - query := database.Messages().Where("id = ?", id) - finder := query.Find(&message) - return &MessageObj{Message: &message, o: wrapObject(id, &message, query)}, finder.Error() -} - -func AllMessages() []*types.Message { - var messages []*types.Message - database.Messages().Find(&messages) - return messages -} - -func (m *MessageObj) object() *Object { - return m.o -} diff --git a/database/sample.go b/database/sample.go new file mode 100644 index 00000000..8dfd9fdd --- /dev/null +++ b/database/sample.go @@ -0,0 +1,173 @@ +// Statping +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long and the project contributors +// +// https://github.com/hunterlong/statping +// +// The licenses for most software and other practical works are designed +// to take away your freedom to share and change the works. By contrast, +// the GNU General Public License is intended to guarantee your freedom to +// share and change all versions of a program--to make sure it remains free +// software for all its users. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package database + +import ( + "time" + + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/postgres" + _ "github.com/jinzhu/gorm/dialects/sqlite" +) + +var ( + sampleStart = time.Now().Add((-24 * 7) * time.Hour).UTC() + SampleHits = 9900. +) + +// InsertSampleHits will create a couple new hits for the sample services +func InsertSampleHits() error { + //tx := Begin(&hits.Hit{}) + //sg := new(sync.WaitGroup) + //for i := int64(1); i <= 5; i++ { + // sg.Add(1) + // service := SelectService(i) + // seed := time.Now().UnixNano() + // log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name)) + // createdAt := sampleStart + // p := utils.NewPerlin(2., 2., 10, seed) + // go func(sg *sync.WaitGroup) { + // defer sg.Done() + // for hi := 0.; hi <= float64(SampleHits); hi++ { + // latency := p.Noise1D(hi / 500) + // createdAt = createdAt.Add(60 * time.Second) + // hit := &hits.Hit{ + // Service: service.Id, + // CreatedAt: createdAt, + // Latency: latency, + // } + // tx = tx.Create(&hit) + // } + // }(sg) + //} + //sg.Wait() + //if err := tx.Commit().Error(); err != nil { + // log.Errorln(err) + // return types.ErrWrap(err, types.ErrorCreateSampleHits) + //} + return nil +} + +// TmpRecords is used for testing Statping. It will create a SQLite database file +// with sample data and store it in the /tmp folder to be used by the tests. +//func TmpRecords(dbFile string) error { +// var sqlFile = utils.Directory + "/" + dbFile +// if err := utils.CreateDirectory(utils.Directory + "/tmp"); err != nil { +// log.Error(err) +// } +// var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename +// SampleHits = 480 +// +// var err error +// CoreApp = NewCore() +// CoreApp.Name = "Tester" +// CoreApp.Setup = true +// configs := &types.DbConfig{ +// DbConn: "sqlite", +// Project: "Tester", +// Location: utils.Directory, +// SqlFile: sqlFile, +// } +// log.Infoln("saving config.yml in: " + utils.Directory) +// if err := configs.Save(utils.Directory); err != nil { +// log.Error(err) +// } +// +// log.Infoln("loading config.yml from: " + utils.Directory) +// if configs, err = LoadConfigs(); err != nil { +// log.Error(err) +// } +// log.Infoln("connecting to database") +// +// exists := utils.FileExists(tmpSqlFile) +// if exists { +// log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile) +// if err := utils.DeleteFile(sqlFile); err != nil { +// log.Error(err) +// } +// if err := utils.CopyFile(tmpSqlFile, sqlFile); err != nil { +// log.Error(err) +// } +// log.Infoln("loading config.yml from: " + utils.Directory) +// +// if err := CoreApp.Connect(configs, false, utils.Directory); err != nil { +// log.Error(err) +// } +// log.Infoln("selecting the Core variable") +// if _, err := SelectCore(); err != nil { +// log.Error(err) +// } +// log.Infoln("inserting notifiers into database") +// if err := InsertNotifierDB(); err != nil { +// log.Error(err) +// } +// log.Infoln("inserting integrations into database") +// if err := InsertIntegratorDB(); err != nil { +// log.Error(err) +// } +// log.Infoln("loading all services") +// if _, err := SelectAllServices(false); err != nil { +// return err +// } +// if err := AttachNotifiers(); err != nil { +// log.Error(err) +// } +// if err := AddIntegrations(); err != nil { +// log.Error(err) +// } +// CoreApp.Notifications = notifier.AllCommunications +// return nil +// } +// +// log.Infoln(tmpSqlFile + " not found, creating a new database...") +// +// if err := CoreApp.Connect(configs, false, utils.Directory); err != nil { +// return err +// } +// log.Infoln("creating database") +// if err := CoreApp.CreateDatabase(); err != nil { +// return err +// } +// log.Infoln("migrating database") +// if err := MigrateDatabase(); err != nil { +// return err +// } +// log.Infoln("insert large sample data into database") +// if err := InsertLargeSampleData(); err != nil { +// return err +// } +// log.Infoln("selecting the Core variable") +// if CoreApp, err = SelectCore(); err != nil { +// return err +// } +// log.Infoln("inserting notifiers into database") +// if err := InsertNotifierDB(); err != nil { +// return err +// } +// log.Infoln("inserting integrations into database") +// if err := InsertIntegratorDB(); err != nil { +// return err +// } +// log.Infoln("loading all services") +// if _, err := SelectAllServices(false); err != nil { +// return err +// } +// log.Infoln("copying sql database file to: " + tmpSqlFile) +// if err := utils.CopyFile(sqlFile, tmpSqlFile); err != nil { +// return err +// } +// return err +//} diff --git a/database/service.go b/database/service.go index 54cfb24a..636bab89 100644 --- a/database/service.go +++ b/database/service.go @@ -1,213 +1 @@ package database - -import ( - "fmt" - "github.com/hunterlong/statping/types" - "strconv" - "strings" - "time" -) - -type ServiceObj struct { - *types.Service - o *Object - - Servicer -} - -type Servicer interface { - Failures() *FailureObj - Checkins() []*CheckinObj - DowntimeText() string - UpdateStats() - Model() *ServiceObj - - Hittable -} - -type Hittable interface { - Hits() *HitObj - CreateHit(hit *types.Hit) *HitObj -} - -func (s *ServiceObj) Model() *ServiceObj { - return s -} - -func Service(id int64) (*ServiceObj, error) { - var service types.Service - query := database.Services().Where("id = ?", id) - finer := query.Find(&service) - return &ServiceObj{Service: &service, o: wrapObject(id, &service, query)}, finer.Error() -} - -func wrapServices(all []*types.Service, db Database) []*ServiceObj { - var arr []*ServiceObj - for _, v := range all { - arr = append(arr, &ServiceObj{Service: v, o: wrapObject(v.Id, v, db)}) - } - return arr -} - -func Services() []*ServiceObj { - var services []*types.Service - db := database.Services().Order("order_id desc") - db.Find(&services) - return wrapServices(services, db) -} - -func (s *ServiceObj) Checkins() []*CheckinObj { - var checkins []*types.Checkin - query := database.Checkins().Where("service = ?", s.Id) - query.Find(&checkins) - return wrapCheckins(checkins, query) -} - -func (s *ServiceObj) DowntimeText() string { - last := s.Failures().Last(1) - if len(last) == 0 { - return "" - } - return parseError(last[0]) -} - -// ParseError returns a human readable error for a Failure -func parseError(f *types.Failure) string { - if f.Method == "checkin" { - return fmt.Sprintf("Checkin is Offline") - } - err := strings.Contains(f.Issue, "connection reset by peer") - if err { - return fmt.Sprintf("Connection Reset") - } - err = strings.Contains(f.Issue, "operation timed out") - if err { - return fmt.Sprintf("HTTP Request Timed Out") - } - err = strings.Contains(f.Issue, "x509: certificate is valid") - if err { - return fmt.Sprintf("SSL Certificate invalid") - } - err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers") - if err { - return fmt.Sprintf("Connection Timed Out") - } - err = strings.Contains(f.Issue, "no such host") - if err { - return fmt.Sprintf("Domain is offline or not found") - } - err = strings.Contains(f.Issue, "HTTP Status Code") - if err { - return fmt.Sprintf("Incorrect HTTP Status Code") - } - err = strings.Contains(f.Issue, "connection refused") - if err { - return fmt.Sprintf("Connection Failed") - } - err = strings.Contains(f.Issue, "can't assign requested address") - if err { - return fmt.Sprintf("Unable to Request Address") - } - err = strings.Contains(f.Issue, "no route to host") - if err { - return fmt.Sprintf("Domain is offline or not found") - } - err = strings.Contains(f.Issue, "i/o timeout") - if err { - return fmt.Sprintf("Connection Timed Out") - } - err = strings.Contains(f.Issue, "Client.Timeout exceeded while reading body") - if err { - return fmt.Sprintf("Timed Out on Response Body") - } - return f.Issue -} - -func (s *ServiceObj) Hits() *HitObj { - query := database.Hits().Where("service = ?", s.Id) - return &HitObj{wrapObject(s.Id, nil, query)} -} - -func (s *ServiceObj) Failures() *FailureObj { - q := database.Failures().Where("method != 'checkin' AND service = ?", s.Id) - return &FailureObj{wrapObject(s.Id, nil, q)} -} - -func (s *ServiceObj) Group() *GroupObj { - var group types.Group - q := database.Groups().Where("id = ?", s.GroupId) - finder := q.Find(&group) - if finder.Error() != nil { - return nil - } - return &GroupObj{Group: &group, o: wrapObject(group.Id, &group, q)} -} - -func (s *ServiceObj) object() *Object { - return s.o -} - -func (s *ServiceObj) UpdateStats() { - s.Online24Hours = s.OnlineDaysPercent(1) - s.Online7Days = s.OnlineDaysPercent(7) - s.AvgResponse = s.AvgTime() - s.FailuresLast24Hours = len(s.Failures().Since(time.Now().UTC().Add(-time.Hour * 24))) - s.Stats = &types.Stats{ - Failures: s.Failures().Count(), - Hits: s.Hits().Count(), - } -} - -// AvgTime will return the average amount of time for a service to response back successfully -func (s *ServiceObj) AvgTime() float64 { - sum := s.Hits().Sum() - return sum -} - -// AvgUptime will return the average amount of time for a service to response back successfully -func (s *ServiceObj) AvgUptime(since time.Time) float64 { - sum := s.Hits().Sum() - return sum -} - -// OnlineDaysPercent returns the service's uptime percent within last 24 hours -func (s *ServiceObj) OnlineDaysPercent(days int) float32 { - ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour) - return s.OnlineSince(ago) -} - -// OnlineSince accepts a time since parameter to return the percent of a service's uptime. -func (s *ServiceObj) OnlineSince(ago time.Time) float32 { - failed := s.Failures().Since(ago) - if len(failed) == 0 { - s.Online24Hours = 100.00 - return s.Online24Hours - } - total := s.Hits().Since(ago) - if len(total) == 0 { - s.Online24Hours = 0 - return s.Online24Hours - } - avg := float64(len(failed)) / float64(len(total)) * 100 - avg = 100 - avg - if avg < 0 { - avg = 0 - } - amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10) - s.Online24Hours = float32(amount) - return s.Online24Hours -} - -// Downtime returns the amount of time of a offline service -func (s *ServiceObj) Downtime() time.Duration { - hits := s.Hits().Last(1) - fail := s.Failures().Last(1) - if len(fail) == 0 { - return time.Duration(0) - } - if len(fail) == 0 { - return time.Now().UTC().Sub(fail[0].CreatedAt.UTC()) - } - since := fail[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) - return since -} diff --git a/database/setup.go b/database/setup.go new file mode 100644 index 00000000..e64cffd3 --- /dev/null +++ b/database/setup.go @@ -0,0 +1,15 @@ +package database + +//// SampleData runs all the sample data for a new Statping installation +//func SampleData() error { +// if err := InsertSampleData(); err != nil { +// return errors.Wrap(err, "sample data") +// } +// if err := InsertSampleHits(); err != nil { +// return errors.Wrap(err, "sample service hits") +// } +// if err := insertSampleCheckins(); err != nil { +// return errors.Wrap(err, "sample checkin examples") +// } +// return nil +//} diff --git a/database/time.go b/database/time.go index 9ea1977f..6c7bfacc 100644 --- a/database/time.go +++ b/database/time.go @@ -22,11 +22,11 @@ func (it *Db) ParseTime(t string) (time.Time, error) { func (it *Db) FormatTime(t time.Time) string { switch it.Type { case "mysql": - return t.UTC().Format("2006-01-02 15:04:05") + return t.Format("2006-01-02 15:04:05") case "postgres": - return t.UTC().Format("2006-01-02 15:04:05.999999999") + return t.Format("2006-01-02 15:04:05.999999999") default: - return t.UTC().Format("2006-01-02 15:04:05") + return t.Format("2006-01-02 15:04:05") } } diff --git a/database/user.go b/database/user.go index 59d85ff0..636bab89 100644 --- a/database/user.go +++ b/database/user.go @@ -1,32 +1 @@ package database - -import "github.com/hunterlong/statping/types" - -type UserObj struct { - *types.User - o *Object -} - -func User(id int64) (*UserObj, error) { - var user types.User - query := database.Users().Where("id = ?", id) - finder := query.First(&user) - return &UserObj{User: &user, o: wrapObject(id, &user, query)}, finder.Error() -} - -func UserByUsername(username string) (*UserObj, error) { - var user types.User - query := database.Users().Where("username = ?", username) - finder := query.First(&user) - return &UserObj{User: &user, o: wrapObject(user.Id, &user, query)}, finder.Error() -} - -func AllUsers() []*types.User { - var users []*types.User - database.Users().Find(&users) - return users -} - -func (u *UserObj) object() *Object { - return u.o -} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f69960bd..efc32f35 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -9,27 +9,27 @@ dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.8.4": - version "7.8.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.8.5.tgz#d28ce872778c23551cbb9432fc68d28495b613b9" - integrity sha512-jWYUqQX/ObOhG1UiEkbH5SANsE/8oKXiQWjj7p7xgj9Zmnt//aUvyz4dBkK0HNsS8/cbyC5NmmH87VekW+mXFg== +"@babel/compat-data@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.8.6.tgz#7eeaa0dfa17e50c7d9c0832515eee09b56f04e35" + integrity sha512-CurCIKPTkS25Mb8mz267vU95vy+TyUpnctEX2lV33xWNmHAfjruztgiPBbXZRh3xZZy1CYvGx6XfxyTVS+sk7Q== dependencies: browserslist "^4.8.5" invariant "^2.2.4" semver "^5.5.0" "@babel/core@^7.1.0", "@babel/core@^7.7.5", "@babel/core@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e" - integrity sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA== + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.6.tgz#27d7df9258a45c2e686b6f18b6c659e563aa4636" + integrity sha512-Sheg7yEJD51YHAvLEV/7Uvw95AeWqYPL3Vk3zGujJKIhJ+8oLw2ALaf3hbucILhKsgSoADOvtKRJuNVdcJkOrg== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.4" + "@babel/generator" "^7.8.6" "@babel/helpers" "^7.8.4" - "@babel/parser" "^7.8.4" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.4" - "@babel/types" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/template" "^7.8.6" + "@babel/traverse" "^7.8.6" + "@babel/types" "^7.8.6" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -59,12 +59,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.2.2", "@babel/generator@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e" - integrity sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA== +"@babel/generator@^7.2.2", "@babel/generator@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.6.tgz#57adf96d370c9a63c241cd719f9111468578537a" + integrity sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.8.6" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -93,34 +93,35 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-compilation-targets@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz#03d7ecd454b7ebe19a254f76617e61770aed2c88" - integrity sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg== +"@babel/helper-compilation-targets@^7.8.4", "@babel/helper-compilation-targets@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.6.tgz#015b85db69e3a34240d5c2b761fc53eb9695f09c" + integrity sha512-UrJdk27hKVJSnibFcUWYLkCL0ZywTUoot8yii1lsHJcvwrypagmYKjHLMWivQPm4s6GdyygCL8fiH5EYLxhQwQ== dependencies: - "@babel/compat-data" "^7.8.4" + "@babel/compat-data" "^7.8.6" browserslist "^4.8.5" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" "@babel/helper-create-class-features-plugin@^7.3.0", "@babel/helper-create-class-features-plugin@^7.3.4", "@babel/helper-create-class-features-plugin@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz#5b94be88c255f140fd2c10dd151e7f98f4bff397" - integrity sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA== + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" + integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== dependencies: "@babel/helper-function-name" "^7.8.3" "@babel/helper-member-expression-to-functions" "^7.8.3" "@babel/helper-optimise-call-expression" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" "@babel/helper-split-export-declaration" "^7.8.3" "@babel/helper-create-regexp-features-plugin@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz#c774268c95ec07ee92476a3862b75cc2839beb79" - integrity sha512-Gcsm1OHCUr9o9TcJln57xhWHtdXbA2pgQ58S0Lxlks0WMGNXuki4+GLfX0p+L2ZkINUGZvfkz8rzoqJQSthI+Q== + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.6.tgz#7fa040c97fb8aebe1247a5c645330c32d083066b" + integrity sha512-bPyujWfsHhV/ztUkwGHz/RPV1T1TDEsSZDsN42JPehndA+p1KKTh3npvTadux0ZhCrytx9tvjpWNowKby3tM6A== dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/helper-regex" "^7.8.3" regexpu-core "^4.6.0" @@ -179,15 +180,16 @@ "@babel/types" "^7.8.3" "@babel/helper-module-transforms@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.3.tgz#d305e35d02bee720fbc2c3c3623aa0c316c01590" - integrity sha512-C7NG6B7vfBa/pwCOshpMbOYUmrYQDfCpVL/JCRu0ek8B5p8kue1+BCXpg2vOYs7w5ACB9GTOBYQ5U6NwrMg+3Q== + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.8.6.tgz#6a13b5eecadc35692047073a64e42977b97654a4" + integrity sha512-RDnGJSR5EFBJjG3deY0NiL0K9TO8SXxS9n/MPsbPK/s9LbQymuLNtlzvDiNS7IpecuL45cMeLVkA+HfmlrnkRg== dependencies: "@babel/helper-module-imports" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" "@babel/helper-simple-access" "^7.8.3" "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/template" "^7.8.6" + "@babel/types" "^7.8.6" lodash "^4.17.13" "@babel/helper-optimise-call-expression@^7.8.3": @@ -220,15 +222,15 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-replace-supers@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz#91192d25f6abbcd41da8a989d4492574fb1530bc" - integrity sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA== +"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" + integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== dependencies: "@babel/helper-member-expression-to-functions" "^7.8.3" "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/traverse" "^7.8.6" + "@babel/types" "^7.8.6" "@babel/helper-simple-access@^7.8.3": version "7.8.3" @@ -273,10 +275,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.7.5", "@babel/parser@^7.8.3", "@babel/parser@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8" - integrity sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw== +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" + integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" @@ -500,17 +502,17 @@ "@babel/helper-plugin-utils" "^7.8.3" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.3.tgz#46fd7a9d2bb9ea89ce88720477979fe0d71b21b8" - integrity sha512-SjT0cwFJ+7Rbr1vQsvphAHwUHvSUPmMjMU/0P59G8U2HLFqSa082JO7zkbDNWs9kH/IUqpHI6xWNesGf8haF1w== +"@babel/plugin-transform-classes@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.6.tgz#77534447a477cbe5995ae4aee3e39fbc8090c46d" + integrity sha512-k9r8qRay/R6v5aWZkrEclEhKO6mc1CCQr2dLsVHBmOQiMpN6I2bpjX3vgnldUWeEI1GHVNByULVxZ4BdP4Hmdg== dependencies: "@babel/helper-annotate-as-pure" "^7.8.3" "@babel/helper-define-map" "^7.8.3" "@babel/helper-function-name" "^7.8.3" "@babel/helper-optimise-call-expression" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" + "@babel/helper-replace-supers" "^7.8.6" "@babel/helper-split-export-declaration" "^7.8.3" globals "^11.1.0" @@ -551,10 +553,10 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-for-of@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz#6fe8eae5d6875086ee185dd0b098a8513783b47d" - integrity sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A== +"@babel/plugin-transform-for-of@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.6.tgz#a051bd1b402c61af97a27ff51b468321c7c2a085" + integrity sha512-M0pw4/1/KI5WAxPsdcUL/w2LJ7o89YHN3yLkzNjg7Yl15GlVGgzHyCU+FMeAxevHGsLVmUqbirlUIKTafPmzdw== dependencies: "@babel/helper-plugin-utils" "^7.8.3" @@ -733,12 +735,12 @@ regenerator-runtime "^0.12.0" "@babel/preset-env@^7.8.4", "@babel/preset-env@~7.8.3": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.4.tgz#9dac6df5f423015d3d49b6e9e5fa3413e4a72c4e" - integrity sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w== + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.8.6.tgz#2a0773b08589ecba4995fc71b1965e4f531af40b" + integrity sha512-M5u8llV9DIVXBFB/ArIpqJuvXpO+ymxcJ6e8ZAmzeK3sQeBNOD1y+rHvHCGG4TlEmsNpIrdecsHGHT8ZCoOSJg== dependencies: - "@babel/compat-data" "^7.8.4" - "@babel/helper-compilation-targets" "^7.8.4" + "@babel/compat-data" "^7.8.6" + "@babel/helper-compilation-targets" "^7.8.6" "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-proposal-async-generator-functions" "^7.8.3" @@ -761,13 +763,13 @@ "@babel/plugin-transform-async-to-generator" "^7.8.3" "@babel/plugin-transform-block-scoped-functions" "^7.8.3" "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.8.3" + "@babel/plugin-transform-classes" "^7.8.6" "@babel/plugin-transform-computed-properties" "^7.8.3" "@babel/plugin-transform-destructuring" "^7.8.3" "@babel/plugin-transform-dotall-regex" "^7.8.3" "@babel/plugin-transform-duplicate-keys" "^7.8.3" "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.8.4" + "@babel/plugin-transform-for-of" "^7.8.6" "@babel/plugin-transform-function-name" "^7.8.3" "@babel/plugin-transform-literals" "^7.8.3" "@babel/plugin-transform-member-expression-literals" "^7.8.3" @@ -788,7 +790,7 @@ "@babel/plugin-transform-template-literals" "^7.8.3" "@babel/plugin-transform-typeof-symbol" "^7.8.4" "@babel/plugin-transform-unicode-regex" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/types" "^7.8.6" browserslist "^4.8.5" core-js-compat "^3.6.2" invariant "^2.2.2" @@ -802,34 +804,34 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.2.2", "@babel/template@^7.7.4", "@babel/template@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" - integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ== +"@babel/template@^7.2.2", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.2.2", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.4.tgz#f0845822365f9d5b0e312ed3959d3f827f869e3c" - integrity sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.2.2", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.4", "@babel/traverse@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" + integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.8.4" + "@babel/generator" "^7.8.6" "@babel/helper-function-name" "^7.8.3" "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.8.4" - "@babel/types" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" - integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg== +"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.8.3", "@babel/types@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" + integrity sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -1097,9 +1099,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "13.7.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.4.tgz#76c3cb3a12909510f52e5dc04a6298cdf9504ffd" - integrity sha512-oVeL12C6gQS/GAExndigSaLxTrKpQPxewx9bOcwfvJiJge4rr7wNaph4J+ns5hrmIV2as5qxqN8YKthn9qh0jw== + version "13.7.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.7.tgz#1628e6461ba8cc9b53196dfeaeec7b07fa6eea99" + integrity sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1143,9 +1145,9 @@ source-map "^0.6.1" "@types/webpack@^4.4.31": - version "4.41.6" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.6.tgz#c76afbdef59159d12e3e1332dc264b75574722a2" - integrity sha512-iWRpV5Ej+8uKrgxp6jXz3v7ZTjgtuMXY+rsxQjFNU0hYCnHkpA7vtiNffgxjuxX4feFHBbz0IF76OzX2OqDYPw== + version "4.41.7" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.7.tgz#22be27dbd4362b01c3954ca9b021dbc9328d9511" + integrity sha512-OQG9viYwO0V1NaNV7d0n79V+n6mjOV30CwgFPIfTzwmk8DHbt+C4f2aBGdCYbo3yFyYD6sjXfqqOjwkl1j+ulA== dependencies: "@types/anymatch" "*" "@types/node" "*" @@ -1160,9 +1162,9 @@ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^15.0.0": - version "15.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.3.tgz#41453a0bc7ab393e995d1f5451455638edbd2baf" - integrity sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ== + version "15.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299" + integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg== dependencies: "@types/yargs-parser" "*" @@ -1183,10 +1185,10 @@ lodash.kebabcase "^4.1.1" svg-tags "^1.0.0" -"@vue/babel-preset-app@^4.1.2", "@vue/babel-preset-app@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.2.2.tgz#c7a0a685a5eb92e1b1538f8d1fc4f5ac00dccec1" - integrity sha512-QGgL+iR+ZdNO9xcFJqYjg938bwjArgIyNOFfM0m+dNSOt7wWVrlFA2v0C6aVN1sJ+IEjdurEolBTZ7hXp6Fbsg== +"@vue/babel-preset-app@^4.1.2", "@vue/babel-preset-app@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.2.3.tgz#608b2c9f7ca677e793833662fc727ff9137a9a35" + integrity sha512-Xlc8d9Ebgu9pNZMUxKZWVP2CctVZzfX3LAxjBDWAAIiVpdXX4IkQQCevDhgiANFzlmE3KXtiSgPGs57Sso2g7Q== dependencies: "@babel/core" "^7.8.4" "@babel/helper-compilation-targets" "^7.8.4" @@ -1256,13 +1258,13 @@ integrity sha512-V51eS7NIsK/rv19oK0+B5Yl/VNWCJTTkjibreIXDknOLSH3MKTOJamUI1BEYo5FOXBWw+7DLmaNF3XKemQ5Y/w== "@vue/cli-plugin-babel@^4.1.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.2.2.tgz#c65cad9921713b9233bab49306559c553a78ee1d" - integrity sha512-uCXDlgUp4ehHoYosr6kbyJYeQ+aQ4lR9Zn0Bf58MFbZbmjBCi8dBKzQf7ve4bo8L8CTGjWirnzgA7pStRmWx0g== + version "4.2.3" + resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-4.2.3.tgz#8633795126f4d78c517bff9a2539229c9e0c8db4" + integrity sha512-vbK6f7dN4gj+6xyhTZkvjjbz1vsTwX+ObRD0ElaaipXo2oVSBAAPPGHkLjnH8C2brDLPeLHdUCzERzx2kc2lmQ== dependencies: "@babel/core" "^7.8.4" - "@vue/babel-preset-app" "^4.2.2" - "@vue/cli-shared-utils" "^4.2.2" + "@vue/babel-preset-app" "^4.2.3" + "@vue/cli-shared-utils" "^4.2.3" babel-loader "^8.0.6" cache-loader "^4.1.0" thread-loader "^2.1.3" @@ -1338,25 +1340,6 @@ webpack-dev-server "^3.10.2" webpack-merge "^4.2.2" -"@vue/cli-shared-utils@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.2.2.tgz#953fec34115cb12d0820012a9d7400f8c27d6660" - integrity sha512-EK5wcxgjadqUpSzfh6Bnxd46Zx+SAaHusygqV11UZKHr4EObc/SjCpq7c7drmFkBjRqmVvrHs4jRnJJo5VgCgQ== - dependencies: - "@hapi/joi" "^15.0.1" - chalk "^2.4.2" - execa "^1.0.0" - launch-editor "^2.2.1" - lru-cache "^5.1.1" - node-ipc "^9.1.1" - open "^6.3.0" - ora "^3.4.0" - read-pkg "^5.1.1" - request "^2.87.0" - request-promise-native "^1.0.8" - semver "^6.1.0" - strip-ansi "^6.0.0" - "@vue/cli-shared-utils@^4.2.3": version "4.2.3" resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-4.2.3.tgz#13646452cc25b0ab68a57cb52cac27983cee39a4" @@ -1613,9 +1596,9 @@ acorn-globals@^4.3.4: acorn-walk "^6.0.1" acorn-jsx@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" - integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== acorn-walk@^6.0.1, acorn-walk@^6.1.1: version "6.2.0" @@ -1633,9 +1616,9 @@ acorn@^6.0.1, acorn@^6.0.2, acorn@^6.0.5, acorn@^6.0.7, acorn@^6.2.1: integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== address@^1.1.2: version "1.1.2" @@ -1661,9 +1644,9 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: - version "6.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" - integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + version "6.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" + integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1762,9 +1745,9 @@ anymatch@^3.0.3, anymatch@~3.1.1: picomatch "^2.0.4" apexcharts@^3.15.0: - version "3.15.6" - resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.15.6.tgz#1716f8badd447b06796dbcd95b5e405ec676a3bf" - integrity sha512-8mZqg7eTZGU2zvjYUUOf+sTqgfmutipHU9lNgkqzZPtwIVGwR5PwXTBNKRJSI3AeSoQ8VZGYfzTJWoUDfGAeBw== + version "3.16.0" + resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.16.0.tgz#7fec05f85773c2b89a7b757cfe551a9c055807ed" + integrity sha512-tD7d/VJ9G2giVIOTowi89RKu4Ptp/jJdvrmkBJiaaOROCT3Wa4qyJ6QKp4dgUmpN5z0xMFxtDPaikItW2BEucw== dependencies: svg.draggable.js "^2.2.2" svg.easing.js "^2.0.0" @@ -2226,16 +2209,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.0.0, browserslist@^4.8.3, browserslist@^4.8.5: - version "4.8.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.7.tgz#ec8301ff415e6a42c949d0e66b405eb539c532d0" - integrity sha512-gFOnZNYBHrEyUML0xr5NJ6edFaaKbTFX9S9kQHlYfCP0Rit/boRIz4G+Avq6/4haEKJXdGGUnoolx+5MWW2BoA== - dependencies: - caniuse-lite "^1.0.30001027" - electron-to-chromium "^1.3.349" - node-releases "^1.1.49" - -browserslist@^4.8.6: +browserslist@^4.0.0, browserslist@^4.8.3, browserslist@^4.8.5, browserslist@^4.8.6: version "4.9.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.9.1.tgz#01ffb9ca31a1aef7678128fc6a2253316aa7287c" integrity sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw== @@ -2489,15 +2463,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001027: - version "1.0.30001028" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001028.tgz#f2241242ac70e0fa9cda55c2776d32a0867971c2" - integrity sha512-Vnrq+XMSHpT7E+LWoIYhs3Sne8h9lx9YJV3acH3THNCwU/9zV93/ta4xVfzTtnqd3rvnuVpVjE3DFqf56tr3aQ== - -caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001030: - version "1.0.30001030" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001030.tgz#78076c4c6d67d3e41d6eb9399853fb27fe6e44ee" - integrity sha512-QGK0W4Ft/Ac+zTjEiRJfwDNATvS3fodDczBXrH42784kcfqcDKpEPfN08N0HQjrAp8He/Jw8QiSS9QRn7XAbUw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001030: + version "1.0.30001031" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001031.tgz#76f1bdd39e19567b855302f65102d9a8aaad5930" + integrity sha512-DpAP5a1NGRLgYfaNCaXIRyGARi+3tJA2quZXNNA1Du26VyVkqvy2tznNu5ANyN1Y5aX44QDotZSVSUSi2uMGjg== capture-exit@^2.0.0: version "2.0.0" @@ -3358,9 +3327,9 @@ data-urls@^2.0.0: whatwg-url "^8.0.0" date-fns@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.9.0.tgz#d0b175a5c37ed5f17b97e2272bbc1fa5aec677d2" - integrity sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA== + version "2.10.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.10.0.tgz#abd10604d8bafb0bcbd2ba2e9b0563b922ae4b6b" + integrity sha512-EhfEKevYGWhWlZbNeplfhIU/+N+x0iCIx7VzKlXma2EdQyznVlZhCptXUY+BegNpPW2kjdx15Rvq503YcXXrcA== de-indent@^1.0.2: version "1.0.2" @@ -3772,15 +3741,10 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.3.349: - version "1.3.356" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.356.tgz#fb985ee0f3023e6e11b97547ff3f738bdd8643d2" - integrity sha512-qW4YHMfOFjvx0jkSK2vjaHoLjk1+uJIV5tqtLDo7P5y3/kM8KQP23YBU0Y5fCSW4jIbDvEzeHDaY4+4vEaqqOw== - electron-to-chromium@^1.3.363: - version "1.3.364" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.364.tgz#524bd0cf9c45ba49c508fd3b731a07efbf310b1c" - integrity sha512-V6hyxQ9jzt6Jy6w8tAv4HHKhIaVS6psG/gmwtQ+2+itdkWMHJLHJ4m1sFep/fWkdKvfJcPXuywfnECRzfNa7gw== + version "1.3.367" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.367.tgz#48abffcaa6591051b612ae70ddc657763ede2662" + integrity sha512-GCHQreWs4zhKA48FNXCjvpV4kTnKoLu2PSAfKX394g34NPvTs2pPh1+jzWitNwhmOYI8zIqt36ulRVRZUgqlfA== elliptic@^6.0.0: version "6.5.2" @@ -4339,9 +4303,9 @@ fast-glob@^2.2.6: micromatch "^3.1.10" fast-glob@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.1.tgz#c5aaea632f92543b744bdcb19f11efd49e56c7b3" - integrity sha512-XObtOQLTl4EptWcBbO9O6wd17VlVf9YXYY/zuzuu7nZfTsv4BL3KupMAMUVzH88CUwWkI3uNHBfxtfU8PveVTQ== + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4361,11 +4325,11 @@ fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2" - integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA== + version "1.6.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" + integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== dependencies: - reusify "^1.0.0" + reusify "^1.0.4" faye-websocket@^0.10.0: version "0.10.0" @@ -4490,16 +4454,7 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874" - integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.0" - pkg-dir "^4.1.0" - -find-cache-dir@^3.2.0: +find-cache-dir@^3.0.0, find-cache-dir@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.0.tgz#4d74ed1fe9ef1731467ca24378e8f8f5c8b6ed11" integrity sha512-PtXtQb7IrD8O+h6Cq1dbpJH5NzD8+9keN1zZ0YlpDzl1PwXEJEBj6u1Xa92t1Hwluoozd9TNKul5Hi2iqpsWwg== @@ -5078,9 +5033,9 @@ hoopy@^0.1.4: integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hosted-git-info@^2.1.4: - version "2.8.5" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" - integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== hpack.js@^2.1.6: version "2.1.6" @@ -5108,9 +5063,9 @@ html-comment-regex@^1.1.0: integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== html-encoding-sniffer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.0.tgz#70b3b69bb5999f35d0d4495d79079f35630e71ae" - integrity sha512-Y9prnPKkM7FXxQevZ5UH8Z6aVTY0ede1tHquck5UxGmKWDshxXh95gSa2xXYjS8AsGO5iOvrCI5+GttRKnLdNA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== dependencies: whatwg-encoding "^1.0.5" @@ -5486,12 +5441,7 @@ ip@^1.1.0, ip@^1.1.5: resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= -ipaddr.js@1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" - integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== - -ipaddr.js@^1.5.2, ipaddr.js@^1.9.0: +ipaddr.js@1.9.1, ipaddr.js@^1.5.2, ipaddr.js@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== @@ -6322,7 +6272,14 @@ lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.1 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -log-symbols@2.2.0, log-symbols@^2.2.0: +log-symbols@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + +log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== @@ -6391,7 +6348,7 @@ make-dir@^2.0.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0, make-dir@^3.0.2: +make-dir@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== @@ -6710,9 +6667,9 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd minimist "0.0.8" mocha@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.0.1.tgz#276186d35a4852f6249808c6dd4a1376cbf6c6ce" - integrity sha512-9eWmWTdHLXh72rGrdZjNbG3aa1/3NRPpul1z0D979QpEnFdCG0Q5tv834N+94QEN2cysfV72YocQ3fn87s70fg== + version "7.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.0.tgz#c784f579ad0904d29229ad6cb1e2514e4db7d249" + integrity sha512-MymHK8UkU0K15Q/zX7uflZgVoRWiTjy0fXE/QjKts6mowUvGxOdPhZ2qj3b0iZdUrNZlW9LAIMFHB4IW+2b3EQ== dependencies: ansi-colors "3.2.3" browser-stdout "1.3.1" @@ -6725,7 +6682,7 @@ mocha@^7.0.1: growl "1.10.5" he "1.2.0" js-yaml "3.13.1" - log-symbols "2.2.0" + log-symbols "3.0.0" minimatch "3.0.4" mkdirp "0.5.1" ms "2.1.1" @@ -6957,13 +6914,6 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-releases@^1.1.49: - version "1.1.49" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.49.tgz#67ba5a3fac2319262675ef864ed56798bb33b93e" - integrity sha512-xH8t0LS0disN0mtRCh+eByxFPie+msJUBL/lJDBuap53QGiYPa9joh83K4pCZgWJ+2L4b9h88vCVdXQ60NO2bg== - dependencies: - semver "^6.3.0" - node-releases@^1.1.50: version "1.1.50" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.50.tgz#803c40d2c45db172d0410e4efec83aa8c6ad0592" @@ -8143,12 +8093,12 @@ proto-list@~1.2.1: integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= proxy-addr@~2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" - integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== dependencies: forwarded "~0.1.2" - ipaddr.js "1.9.0" + ipaddr.js "1.9.1" prr@~1.0.1: version "1.0.1" @@ -8286,9 +8236,9 @@ raw-body@2.4.0: unpipe "1.0.0" react-is@^16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" - integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" + integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== read-pkg-up@^1.0.1: version "1.0.1" @@ -8626,7 +8576,7 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -reusify@^1.0.0: +reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== @@ -8669,9 +8619,9 @@ rsvp@^4.8.4: integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + version "2.4.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" + integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== dependencies: is-promise "^2.1.0" @@ -9591,19 +9541,10 @@ terser-webpack-plugin@^2.3.4: terser "^4.4.3" webpack-sources "^1.4.3" -terser@^4.1.2, terser@^4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.3.tgz#e33aa42461ced5238d352d2df2a67f21921f8d87" - integrity sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^4.4.3: - version "4.6.4" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.4.tgz#40a0b37afbe5b57e494536815efa68326840fc00" - integrity sha512-5fqgBPLgVHZ/fVvqRhhUp9YUiGXhFJ9ZkrZWD9vQtFBR4QIGTnbsb+/kKqSqfgp3WnBwGWAFnedGTtmX1YTn0w== +terser@^4.1.2, terser@^4.4.3, terser@^4.6.3: + version "4.6.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.6.tgz#da2382e6cafbdf86205e82fb9a115bd664d54863" + integrity sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -9792,9 +9733,9 @@ ts-pnp@^1.1.6: integrity sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ== tslib@^1.10.0, tslib@^1.9.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.0.tgz#f1f3528301621a53220d58373ae510ff747a66bc" - integrity sha512-BmndXUtiTn/VDDrJzQE7Mm22Ix3PxgLltW9bSNLoeCY31gnG2OPx0QqJnuc9oMIKioYrz487i6K9o4Pdn0j+Kg== + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== tty-browserify@0.0.0: version "0.0.0" @@ -10452,9 +10393,9 @@ webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.3.0, webpack- source-map "~0.6.1" webpack@^4.0.0: - version "4.41.6" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.6.tgz#12f2f804bf6542ef166755050d4afbc8f66ba7e1" - integrity sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA== + version "4.42.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.0.tgz#b901635dd6179391d90740a63c93f76f39883eb8" + integrity sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -10620,9 +10561,9 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.1.tgz#558328352e673b5bb192cf86500d60b230667d4b" - integrity sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: imurmurhash "^0.1.4" is-typedarray "^1.0.0" diff --git a/go.mod b/go.mod index 1d80b28c..7f236601 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/lib/pq v1.2.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/pkg/errors v0.8.1 + github.com/prometheus/common v0.2.0 github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f github.com/russross/blackfriday/v2 v2.0.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8730b642..47e8b5eb 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,9 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d h1:ZX0t+GA3MWiP7LWt5xWOphWRQd5JwL4VW5uLW83KM8g= @@ -123,6 +125,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -205,6 +208,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/handlers/api.go b/handlers/api.go index 37d901a8..b67eb203 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -19,10 +19,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/groups" + "github.com/hunterlong/statping/types/incidents" + "github.com/hunterlong/statping/types/messages" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" "net/http" "time" @@ -38,21 +43,21 @@ type apiResponse struct { } func apiIndexHandler(r *http.Request) interface{} { - coreClone := *core.CoreApp + coreClone := core.App var loggedIn bool _, err := getJwtToken(r) if err == nil { loggedIn = true } coreClone.LoggedIn = loggedIn - return *coreClone.ToCore() + return coreClone } func apiRenewHandler(w http.ResponseWriter, r *http.Request) { var err error - core.CoreApp.ApiKey = utils.NewSHA1Hash(40) - core.CoreApp.ApiSecret = utils.NewSHA1Hash(40) - err = database.Update(core.CoreApp) + core.App.ApiKey = utils.NewSHA1Hash(40) + core.App.ApiSecret = utils.NewSHA1Hash(40) + err = core.App.Update() if err != nil { sendErrorJson(err, w, r) return @@ -71,7 +76,7 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(err, w, r) return } - app := core.CoreApp + app := core.App if c.Name != "" { app.Name = c.Name } @@ -90,9 +95,9 @@ func apiCoreHandler(w http.ResponseWriter, r *http.Request) { if c.Timezone != app.Timezone { app.Timezone = c.Timezone } - app.UseCdn = types.NewNullBool(c.UseCdn.Bool) - err = database.Update(app) - returnJson(core.CoreApp, w, r) + app.UseCdn = null.NewNullBool(c.UseCdn.Bool) + err = app.Update() + returnJson(core.App, w, r) } type cacheJson struct { @@ -135,45 +140,33 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht var objName string var objId int64 switch v := obj.(type) { - case types.Servicer: + case *services.Service: objName = "service" - objId = v.Model().Id - case *notifier.Notification: + objId = v.Id + case *notifications.Notification: objName = "notifier" objId = v.Id - case *core.Core, *types.Core: + case *core.Core: objName = "core" - case *types.User: + case *users.User: objName = "user" objId = v.Id - case *core.User: - objName = "user" - objId = v.Id - case *types.Group: + case *groups.Group: objName = "group" objId = v.Id - case database.Grouper: - objName = "group" - objId = v.Model().Id - case *core.Checkin: + case *checkins.Checkin: objName = "checkin" objId = v.Id - case *core.CheckinHit: + case *checkins.CheckinHit: objName = "checkin_hit" objId = v.Id - case *types.Message: + case *messages.Message: objName = "message" objId = v.Id - case *core.Message: - objName = "message" - objId = v.Id - case *types.Checkin: - objName = "checkin" - objId = v.Id - case *core.Incident: + case *incidents.Incident: objName = "incident" objId = v.Id - case *core.IncidentUpdate: + case *incidents.IncidentUpdate: objName = "incident_update" objId = v.Id default: diff --git a/handlers/api_test.go b/handlers/api_test.go index a957fb28..2757d7d6 100644 --- a/handlers/api_test.go +++ b/handlers/api_test.go @@ -1,12 +1,14 @@ package handlers import ( + "encoding/json" "fmt" "github.com/hunterlong/statping/core" _ "github.com/hunterlong/statping/notifiers" "github.com/hunterlong/statping/source" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "io/ioutil" @@ -31,12 +33,12 @@ func init() { dir = utils.Directory } -func TestResetDatabase(t *testing.T) { - err := core.TmpRecords("handlers.db") - t.Log(err) - require.Nil(t, err) - require.NotNil(t, core.CoreApp) -} +//func TestResetDatabase(t *testing.T) { +// err := core.TmpRecords("handlers.db") +// t.Log(err) +// require.Nil(t, err) +// require.NotNil(t, core.CoreApp) +//} func TestFailedHTTPServer(t *testing.T) { err := RunHTTPServer("missinghost", 0) @@ -65,6 +67,12 @@ func TestSetupRoutes(t *testing.T) { URL: "/api", Method: "GET", ExpectedStatus: 200, + FuncTest: func() error { + if core.CoreApp.Setup { + return errors.New("core has already been setup") + } + return nil + }, }, { Name: "Statping Run Setup", @@ -73,7 +81,13 @@ func TestSetupRoutes(t *testing.T) { Body: form.Encode(), ExpectedStatus: 200, HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"}, - ExpectedFiles: []string{dir + "/config.yml", dir + "/tmp/" + types.SqliteFilename}, + ExpectedFiles: []string{dir + "/config.yml", dir + "/handlers/" + types.SqliteFilename}, + FuncTest: func() error { + if !core.CoreApp.Setup { + return errors.New("core has not been setup") + } + return nil + }, }} for _, v := range tests { @@ -94,7 +108,13 @@ func TestMainApiRoutes(t *testing.T) { URL: "/api", Method: "GET", ExpectedStatus: 200, - ExpectedContains: []string{`"description":"This data is only used to testing"`}, + ExpectedContains: []string{`"description":"This is an awesome test"`}, + FuncTest: func() error { + if !core.CoreApp.Setup { + return errors.New("database is not setup") + } + return nil + }, }, { Name: "Statping Renew API Keys", @@ -107,6 +127,12 @@ func TestMainApiRoutes(t *testing.T) { URL: "/api/clear_cache", Method: "POST", ExpectedStatus: 200, + FuncTest: func() error { + if len(CacheStorage.List()) != 0 { + return errors.New("cache was not reset") + } + return nil + }, }, { Name: "404 Error Page", @@ -123,346 +149,7 @@ func TestMainApiRoutes(t *testing.T) { } } -func TestApiServiceRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping All Services", - URL: "/api/services", - Method: "GET", - ExpectedContains: []string{`"name":"Google"`}, - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1", - URL: "/api/services/1", - Method: "GET", - ExpectedContains: []string{`"name":"Google"`}, - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1 Data", - URL: "/api/services/1/data", - Method: "GET", - Body: "", - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1 Ping Data", - URL: "/api/services/1/ping", - Method: "GET", - Body: "", - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1 Heatmap Data", - URL: "/api/services/1/heatmap", - Method: "GET", - Body: "", - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1 Hits", - URL: "/api/services/1/hits", - Method: "GET", - Body: "", - ExpectedStatus: 200, - }, - { - Name: "Statping Service 1 Failures", - URL: "/api/services/1/failures", - Method: "GET", - Body: "", - ExpectedStatus: 200, - }, - { - Name: "Statping Reorder Services", - URL: "/api/services/reorder", - Method: "POST", - Body: `[{"service":1,"order":1},{"service":5,"order":2},{"service":2,"order":3},{"service":3,"order":4},{"service":4,"order":5}]`, - ExpectedStatus: 200, - HttpHeaders: []string{"Content-Type=application/json"}, - }, - { - Name: "Statping Create Service", - URL: "/api/services", - HttpHeaders: []string{"Content-Type=application/json"}, - Method: "POST", - Body: `{ - "name": "New Service", - "domain": "https://statping.com", - "expected": "", - "expected_status": 200, - "check_interval": 30, - "type": "http", - "method": "GET", - "post_data": "", - "port": 0, - "timeout": 30, - "order_id": 0 -}`, - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"service","method":"create"`}, - }, - { - Name: "Statping Update Service", - URL: "/api/services/1", - HttpHeaders: []string{"Content-Type=application/json"}, - Method: "POST", - Body: `{ - "name": "Updated New Service", - "domain": "https://google.com", - "expected": "", - "expected_status": 200, - "check_interval": 60, - "type": "http", - "method": "GET", - "post_data": "", - "port": 0, - "timeout": 10, - "order_id": 0 -}`, - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"service","method":"update"`}, - }, - { - Name: "Statping Delete Service", - URL: "/api/services/1", - Method: "DELETE", - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"service","method":"delete"`}, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} - -func TestGroupAPIRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping Groups", - URL: "/api/groups", - Method: "GET", - ExpectedStatus: 200, - }, - { - Name: "Statping View Group", - URL: "/api/groups/1", - Method: "GET", - ExpectedStatus: 200, - }, - { - Name: "Statping Create Group", - URL: "/api/groups", - HttpHeaders: []string{"Content-Type=application/json"}, - Body: `{ - "name": "New Group", - "public": true -}`, - Method: "POST", - ExpectedStatus: 200, - }, - { - Name: "Statping Delete Group", - URL: "/api/groups/1", - Method: "DELETE", - ExpectedStatus: 200, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} - -func TestApiUsersRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping All Users", - URL: "/api/users", - Method: "GET", - ExpectedStatus: 200, - }, { - Name: "Statping Create User", - URL: "/api/users", - HttpHeaders: []string{"Content-Type=application/json"}, - Method: "POST", - Body: `{ - "username": "adminuser2", - "email": "info@adminemail.com", - "password": "passsword123", - "admin": true -}`, - ExpectedStatus: 200, - }, { - Name: "Statping View User", - URL: "/api/users/1", - Method: "GET", - ExpectedStatus: 200, - }, { - Name: "Statping Update User", - URL: "/api/users/1", - Method: "POST", - Body: `{ - "username": "adminupdated", - "email": "info@email.com", - "password": "password12345", - "admin": true -}`, - ExpectedStatus: 200, - }, { - Name: "Statping Delete User", - URL: "/api/users/1", - Method: "DELETE", - ExpectedStatus: 200, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} - -func TestApiNotifiersRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping Notifiers", - URL: "/api/notifiers", - Method: "GET", - ExpectedStatus: 200, - }, { - Name: "Statping Mobile Notifier", - URL: "/api/notifier/mobile", - Method: "GET", - ExpectedStatus: 200, - }, { - Name: "Statping Update Notifier", - URL: "/api/notifier/mobile", - Method: "POST", - Body: `{ - "method": "mobile", - "var1": "ExponentPushToken[ToBadIWillError123456]", - "enabled": true, - "limits": 55 -}`, - ExpectedStatus: 200, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} - -func TestMessagesApiRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping Messages", - URL: "/api/messages", - Method: "GET", - ExpectedStatus: 200, - ExpectedContains: []string{`"title":"Routine Downtime"`}, - }, { - Name: "Statping Create Message", - URL: "/api/messages", - Method: "POST", - Body: `{ - "title": "API Message", - "description": "This is an example a upcoming message for a service!", - "start_on": "2022-11-17T03:28:16.323797-08:00", - "end_on": "2022-11-17T05:13:16.323798-08:00", - "service": 1, - "notify_users": true, - "notify_method": "email", - "notify_before": 6, - "notify_before_scale": "hour" -}`, - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"message","method":"create"`}, - }, - { - Name: "Statping View Message", - URL: "/api/messages/1", - Method: "GET", - ExpectedStatus: 200, - ExpectedContains: []string{`"title":"Routine Downtime"`}, - }, { - Name: "Statping Update Message", - URL: "/api/messages/1", - Method: "POST", - Body: `{ - "title": "Updated Message", - "description": "This message was updated", - "start_on": "2022-11-17T03:28:16.323797-08:00", - "end_on": "2022-11-17T05:13:16.323798-08:00", - "service": 1, - "notify_users": true, - "notify_method": "email", - "notify_before": 3, - "notify_before_scale": "hour" -}`, - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"message","method":"update"`}, - }, - { - Name: "Statping Delete Message", - URL: "/api/messages/1", - Method: "DELETE", - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"message","method":"delete"`}, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} - -func TestApiCheckinRoutes(t *testing.T) { - tests := []HTTPTest{ - { - Name: "Statping Checkins", - URL: "/api/checkins", - Method: "GET", - ExpectedStatus: 200, - }, { - Name: "Statping Create Checkin", - URL: "/api/checkin", - Method: "POST", - Body: `{ - "service_id": 2, - "name": "Server Checkin", - "interval": 900, - "grace": 60 -}`, - ExpectedStatus: 200, - ExpectedContains: []string{`"status":"success","type":"checkin","method":"create"`}, - }, - { - Name: "Statping Checkins", - URL: "/api/checkins", - Method: "GET", - ExpectedStatus: 200, - }} - - for _, v := range tests { - t.Run(v.Name, func(t *testing.T) { - _, t, err := RunHTTPTest(v, t) - require.Nil(t, err) - }) - } -} +type HttpFuncTest func() error // HTTPTest contains all the parameters for a HTTP Unit Test type HTTPTest struct { @@ -474,6 +161,8 @@ type HTTPTest struct { ExpectedContains []string HttpHeaders []string ExpectedFiles []string + FuncTest HttpFuncTest + ResponseLen int } // RunHTTPTest accepts a HTTPTest type to execute the HTTP request @@ -497,6 +186,8 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) { assert.Nil(t, err) return "", t, err } + defer rr.Result().Body.Close() + stringBody := string(body) if test.ExpectedStatus != rr.Result().StatusCode { assert.Equal(t, test.ExpectedStatus, rr.Result().StatusCode) @@ -512,5 +203,15 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) { assert.FileExists(t, v) } } + if test.FuncTest != nil { + err := test.FuncTest() + assert.Nil(t, err) + } + if test.ResponseLen != 0 { + var respArray []interface{} + err := json.Unmarshal(body, &respArray) + assert.Nil(t, err) + assert.Equal(t, test.ResponseLen, len(respArray)) + } return stringBody, t, err } diff --git a/handlers/checkin.go b/handlers/checkin.go index 179ff5f2..7a955dd2 100644 --- a/handlers/checkin.go +++ b/handlers/checkin.go @@ -19,44 +19,43 @@ import ( "encoding/json" "fmt" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "net" "net/http" ) func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) { - checkins := database.AllCheckins() - returnJson(checkins, w, r) + chks := checkins.All() + returnJson(chks, w, r) } func apiCheckinHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - checkin, err := database.CheckinByKey(vars["api"]) + checkin, err := checkins.FindByAPI(vars["api"]) if err != nil { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } - out := checkin.Model() - returnJson(out, w, r) + returnJson(checkin, w, r) } func checkinCreateHandler(w http.ResponseWriter, r *http.Request) { - var checkin *core.Checkin + var checkin *checkins.Checkin decoder := json.NewDecoder(r.Body) err := decoder.Decode(&checkin) if err != nil { sendErrorJson(err, w, r) return } - service := core.SelectService(checkin.ServiceId) - if service == nil { + service, err := services.Find(checkin.ServiceId) + if err != nil { sendErrorJson(fmt.Errorf("missing service_id field"), w, r) return } - _, err = checkin.Create() + checkin.ServiceId = service.Id + err = checkin.Create() if err != nil { sendErrorJson(err, w, r) return @@ -66,42 +65,40 @@ func checkinCreateHandler(w http.ResponseWriter, r *http.Request) { func checkinHitHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - checkin, err := database.CheckinByKey(vars["api"]) + checkin, err := checkins.FindByAPI(vars["api"]) if err != nil { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } ip, _, _ := net.SplitHostPort(r.RemoteAddr) - hit := &types.CheckinHit{ + hit := &checkins.CheckinHit{ Checkin: checkin.Id, From: ip, CreatedAt: utils.Now().UTC(), } - newCheck, err := database.Create(hit) + err = hit.Create() if err != nil { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } checkin.Failing = false - checkin.LastHit = utils.Timezoner(utils.Now().UTC(), core.CoreApp.Timezone) - sendJsonAction(newCheck.Id, "update", w, r) + checkin.LastHitTime = utils.Now().UTC() + sendJsonAction(hit.Id, "update", w, r) } func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - checkin := core.SelectCheckin(vars["api"]) - if checkin == nil { + checkin, err := checkins.FindByAPI(vars["api"]) + if err != nil { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } - if err := database.Delete(checkin); err != nil { + if err := checkin.Delete(); err != nil { sendErrorJson(err, w, r) return } - checkin.Delete() - sendJsonAction(checkin, "delete", w, r) } diff --git a/handlers/checkins_test.go b/handlers/checkins_test.go new file mode 100644 index 00000000..8ccf416f --- /dev/null +++ b/handlers/checkins_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestApiCheckinRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping Checkins", + URL: "/api/checkins", + Method: "GET", + ExpectedStatus: 200, + }, { + Name: "Statping Create Checkin", + URL: "/api/checkin", + Method: "POST", + Body: `{ + "service_id": 2, + "name": "Server Checkin", + "interval": 900, + "grace": 60 +}`, + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success","type":"checkin","method":"create"`}, + }, + { + Name: "Statping Checkins", + URL: "/api/checkins", + Method: "GET", + ExpectedStatus: 200, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/handlers/dashboard.go b/handlers/dashboard.go index 4d19ea93..52c473f8 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -19,9 +19,8 @@ import ( "encoding/json" "fmt" "github.com/dgrijalva/jwt-go" - "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/source" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" "net/http" "os" @@ -184,7 +183,7 @@ func removeJwtToken(w http.ResponseWriter) { }) } -func setJwtToken(user *types.User, w http.ResponseWriter) (JwtClaim, string) { +func setJwtToken(user *users.User, w http.ResponseWriter) (JwtClaim, string) { expirationTime := time.Now().Add(72 * time.Hour) jwtClaim := JwtClaim{ Username: user.Username, @@ -209,7 +208,7 @@ func apiLoginHandler(w http.ResponseWriter, r *http.Request) { form := parseForm(r) username := form.Get("username") password := form.Get("password") - user, auth := core.AuthUser(username, password) + user, auth := users.AuthUser(username, password) if auth { utils.Log.Infoln(fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr)) _, token := setJwtToken(user, w) diff --git a/core/export.go b/handlers/export.go similarity index 56% rename from core/export.go rename to handlers/export.go index 6a9f1cd4..b4a9b314 100644 --- a/core/export.go +++ b/handlers/export.go @@ -13,24 +13,28 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package core +package handlers import ( "encoding/json" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/groups" + "github.com/hunterlong/statping/types/messages" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" ) // ExportChartsJs renders the charts for the index page type ExportData struct { - Core *types.Core `json:"core"` - Services []*types.Service `json:"services"` - Messages []*types.Message `json:"messages"` - Checkins []*types.Checkin `json:"checkins"` - Users []*types.User `json:"users"` - Groups []*types.Group `json:"groups"` - Notifiers []types.AllNotifiers `json:"notifiers"` + Core *core.Core `json:"core"` + Services []*services.Service `json:"services"` + Messages []*messages.Message `json:"messages"` + Checkins []*checkins.Checkin `json:"checkins"` + Users []*users.User `json:"users"` + Groups []*groups.Group `json:"groups"` + Notifiers []core.AllNotifiers `json:"notifiers"` } // ExportSettings will export a JSON file containing all of the settings below: @@ -42,17 +46,14 @@ type ExportData struct { // - Groups // - Messages func ExportSettings() ([]byte, error) { - users := database.AllUsers() - messages := database.AllMessages() - data := ExportData{ - Core: CoreApp.Core, - Notifiers: CoreApp.Notifications, - //Checkins: database.AllCheckins(), - Users: users, - //Services: CoreApp.Services, - //Groups: SelectGroups(true, true), - Messages: messages, + Core: core.App, + //Notifiers: notifications.All(), + Checkins: checkins.All(), + Users: users.All(), + Services: services.All(), + Groups: groups.All(), + Messages: messages.All(), } export, err := json.Marshal(data) return export, err diff --git a/handlers/function.go b/handlers/function.go index 04742c16..d1b540a7 100644 --- a/handlers/function.go +++ b/handlers/function.go @@ -2,7 +2,9 @@ package handlers import ( "encoding/json" - "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/source" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/utils" "html/template" "net/http" "net/url" @@ -65,20 +67,20 @@ func serviceFromID(r *http.Request, object interface{}) error { var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap { return template.FuncMap{ "VERSION": func() string { - return core.VERSION + return core.App.Version }, "CoreApp": func() core.Core { - c := *core.CoreApp + c := *core.App if c.Name == "" { c.Name = "Statping" } return c }, "USE_CDN": func() bool { - return core.CoreApp.UseCdn.Bool + return core.App.UseCdn.Bool }, "USING_ASSETS": func() bool { - return core.CoreApp.UsingAssets() + return source.UsingAssets(utils.Directory) }, "BasePath": func() string { return basePath diff --git a/handlers/groups.go b/handlers/groups.go index 0cffddf1..94bfb882 100644 --- a/handlers/groups.go +++ b/handlers/groups.go @@ -16,37 +16,30 @@ package handlers import ( - "encoding/json" - "errors" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/groups" "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" "net/http" ) +func selectGroup(r *http.Request) (*groups.Group, error) { + vars := mux.Vars(r) + id := utils.ToInt(vars["id"]) + return groups.Find(id) +} + // apiAllGroupHandler will show all the groups func apiAllGroupHandler(r *http.Request) interface{} { auth, admin := IsUser(r), IsAdmin(r) - groups := core.SelectGroups(admin, auth) - return flattenGroups(groups) -} - -func flattenGroups(groups map[int64]*core.Group) []*types.Group { - var groupers []*types.Group - for _, group := range groups { - groupers = append(groupers, group.Group) - } - return groupers + return groups.SelectGroups(admin, auth) } // apiGroupHandler will show a single group func apiGroupHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - group := core.SelectGroup(utils.ToInt(vars["id"])) - if group.Id == 0 { - sendErrorJson(errors.New("group not found"), w, r) + group, err := selectGroup(r) + if err != nil { + sendErrorJson(errors.Wrap(err, "group not found"), w, r) return } returnJson(group, w, r) @@ -54,52 +47,54 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request) { // apiGroupUpdateHandler will update a group func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - group := core.SelectGroup(utils.ToInt(vars["id"])) - if group.Id == 0 { - sendErrorJson(errors.New("group not found"), w, r) + group, err := selectGroup(r) + if err != nil { + sendErrorJson(errors.Wrap(err, "group not found"), w, r) return } - decoder := json.NewDecoder(r.Body) - decoder.Decode(&group) - err := database.Update(group) - if err != nil { + + if err := DecodeJSON(r, &group); err != nil { sendErrorJson(err, w, r) return } + + if err := group.Update(); err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(group, "update", w, r) } // apiCreateGroupHandler accepts a POST method to create new groups func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) { - var group *core.Group - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&group) - if err != nil { + var group *groups.Group + if err := DecodeJSON(r, &group); err != nil { sendErrorJson(err, w, r) return } - _, err = database.Create(group) - if err != nil { + + if err := group.Create(); err != nil { sendErrorJson(err, w, r) return } + sendJsonAction(group, "create", w, r) } // apiGroupDeleteHandler accepts a DELETE method to delete groups func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - group := core.SelectGroup(utils.ToInt(vars["id"])) - if group.Id == 0 { - sendErrorJson(errors.New("group not found"), w, r) + group, err := selectGroup(r) + if err != nil { + sendErrorJson(errors.Wrap(err, "group not found"), w, r) return } - err := database.Delete(group) - if err != nil { + + if err := group.Delete(); err != nil { sendErrorJson(err, w, r) return } + sendJsonAction(group, "delete", w, r) } @@ -111,12 +106,23 @@ type groupOrder struct { func apiGroupReorderHandler(w http.ResponseWriter, r *http.Request) { r.ParseForm() var newOrder []*groupOrder - decoder := json.NewDecoder(r.Body) - decoder.Decode(&newOrder) + + if err := DecodeJSON(r, &newOrder); err != nil { + sendErrorJson(err, w, r) + return + } + for _, g := range newOrder { - group := core.SelectGroup(g.Id) + group, err := groups.Find(g.Id) + if err != nil { + sendErrorJson(err, w, r) + return + } group.Order = g.Order - database.Update(group) + if err := group.Update(); err != nil { + sendErrorJson(err, w, r) + return + } } returnJson(newOrder, w, r) } diff --git a/handlers/groups_test.go b/handlers/groups_test.go new file mode 100644 index 00000000..2fef36f3 --- /dev/null +++ b/handlers/groups_test.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestGroupAPIRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping Groups", + URL: "/api/groups", + Method: "GET", + ExpectedStatus: 200, + ResponseLen: 3, + }, + { + Name: "Statping View Group", + URL: "/api/groups/1", + Method: "GET", + ExpectedStatus: 200, + }, + { + Name: "Statping Create Group", + URL: "/api/groups", + HttpHeaders: []string{"Content-Type=application/json"}, + Body: `{ + "name": "New Group", + "public": true +}`, + Method: "POST", + ExpectedStatus: 200, + }, + { + Name: "Statping Delete Group", + URL: "/api/groups/1", + Method: "DELETE", + ExpectedStatus: 200, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/handlers/handlers.go b/handlers/handlers.go index 2745d264..a4fa076b 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "github.com/dgrijalva/jwt-go" + "github.com/hunterlong/statping/types/core" "html/template" "net/http" "os" @@ -29,7 +30,6 @@ import ( "strings" "time" - "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/source" "github.com/hunterlong/statping/utils" ) @@ -98,25 +98,24 @@ func RunHTTPServer(ip string, port int) error { httpServer.SetKeepAlivesEnabled(false) return httpServer.ListenAndServe() } - return nil } // IsReadAuthenticated will allow Read Only authentication for some routes func IsReadAuthenticated(r *http.Request) bool { - if !core.CoreApp.Setup { + if !core.App.Setup { return false } var token string query := r.URL.Query() key := query.Get("api") - if subtle.ConstantTimeCompare([]byte(key), []byte(core.CoreApp.ApiSecret)) == 1 { + if subtle.ConstantTimeCompare([]byte(key), []byte(core.App.ApiSecret)) == 1 { return true } tokens, ok := r.Header["Authorization"] if ok && len(tokens) >= 1 { token = tokens[0] token = strings.TrimPrefix(token, "Bearer ") - if subtle.ConstantTimeCompare([]byte(token), []byte(core.CoreApp.ApiSecret)) == 1 { + if subtle.ConstantTimeCompare([]byte(token), []byte(core.App.ApiSecret)) == 1 { return true } } @@ -129,10 +128,10 @@ func IsFullAuthenticated(r *http.Request) bool { if os.Getenv("GO_ENV") == "test" { return true } - if core.CoreApp == nil { + if core.App == nil { return true } - if !core.CoreApp.Setup { + if !core.App.Setup { return false } var token string @@ -140,7 +139,7 @@ func IsFullAuthenticated(r *http.Request) bool { if ok && len(tokens) >= 1 { token = tokens[0] token = strings.TrimPrefix(token, "Bearer ") - if subtle.ConstantTimeCompare([]byte(token), []byte(core.CoreApp.ApiSecret)) == 1 { + if subtle.ConstantTimeCompare([]byte(token), []byte(core.App.ApiSecret)) == 1 { return true } } @@ -185,7 +184,7 @@ func ScopeName(r *http.Request) string { // IsAdmin returns true if the user session is an administrator func IsAdmin(r *http.Request) bool { - if !core.CoreApp.Setup { + if !core.App.Setup { return false } if os.Getenv("GO_ENV") == "test" { @@ -200,7 +199,7 @@ func IsAdmin(r *http.Request) bool { // IsUser returns true if the user is registered func IsUser(r *http.Request) bool { - if !core.CoreApp.Setup { + if !core.App.Setup { return false } if os.Getenv("GO_ENV") == "test" { @@ -275,7 +274,7 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data // "safe": func(html string) template.HTML { // return template.HTML(html) // }, - // "Services": func() []types.ServiceInterface { + // "Services": func() []services.ServiceInterface { // return core.CoreApp.Services // }, //}) diff --git a/handlers/incident.go b/handlers/incident.go index 1dbe855d..745f220b 100644 --- a/handlers/incident.go +++ b/handlers/incident.go @@ -3,38 +3,35 @@ package handlers import ( "encoding/json" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/incidents" "github.com/hunterlong/statping/utils" "net/http" ) func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) { - incidents := database.AllIncidents() - returnJson(incidents, w, r) + inc := incidents.All() + returnJson(inc, w, r) } func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) { - var incident *types.Incident + var incident *incidents.Incident decoder := json.NewDecoder(r.Body) err := decoder.Decode(&incident) if err != nil { sendErrorJson(err, w, r) return } - newIncident := core.ReturnIncident(incident) - obj, err := database.Create(newIncident) + err = incident.Create() if err != nil { sendErrorJson(err, w, r) return } - sendJsonAction(obj, "create", w, r) + sendJsonAction(incident, "create", w, r) } func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - incident, err := database.Incident(utils.ToInt(vars["id"])) + incident, err := incidents.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(err, w, r) return @@ -47,22 +44,18 @@ func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) { return } - err = database.Update(&incident) - if err != nil { - sendErrorJson(err, w, r) - return - } - sendJsonAction(incident, "update", w, r) + updates := incident.Updates() + sendJsonAction(updates, "update", w, r) } func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - incident, err := database.Incident(utils.ToInt(vars["id"])) + incident, err := incidents.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(err, w, r) return } - err = database.Delete(incident) + err = incident.Delete() if err != nil { sendErrorJson(err, w, r) return diff --git a/handlers/index.go b/handlers/index.go index 14a76a51..c48d7643 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -16,23 +16,24 @@ package handlers import ( - "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/services" "net/http" ) func indexHandler(w http.ResponseWriter, r *http.Request) { - if !core.CoreApp.Setup { + if !core.App.Setup { http.Redirect(w, r, "/setup", http.StatusSeeOther) return } - ExecuteResponse(w, r, "base.gohtml", core.CoreApp, nil) + ExecuteResponse(w, r, "base.gohtml", core.App, nil) } func healthCheckHandler(w http.ResponseWriter, r *http.Request) { health := map[string]interface{}{ - "services": len(core.Services()), + "services": len(services.All()), "online": true, - "setup": core.IsSetup(), + "setup": core.App.Setup, } returnJson(health, w, r) } diff --git a/handlers/integrations.go b/handlers/integrations.go index b65c07d1..efc163b2 100644 --- a/handlers/integrations.go +++ b/handlers/integrations.go @@ -3,54 +3,51 @@ package handlers import ( "encoding/json" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core/integrations" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/integrations" "net/http" ) +func findIntegration(r *http.Request) (*integrations.Integration, string, error) { + vars := mux.Vars(r) + name := vars["name"] + intgr, err := integrations.Find(name) + if err != nil { + return nil, "", err + } + return intgr, name, nil +} + func apiAllIntegrationsHandler(w http.ResponseWriter, r *http.Request) { - integrations := integrations.Integrations - returnJson(integrations, w, r) + inte := integrations.All() + returnJson(inte, w, r) } func apiIntegrationViewHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - intgr, err := integrations.Find(vars["name"]) + intgr, _, err := findIntegration(r) if err != nil { sendErrorJson(err, w, r) return } - returnJson(intgr.Get(), w, r) + returnJson(intgr, w, r) } func apiIntegrationHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - intgr, err := integrations.Find(vars["name"]) + intgr, _, err := findIntegration(r) if err != nil { sendErrorJson(err, w, r) return } - var intJson *types.Integration decoder := json.NewDecoder(r.Body) - if err := decoder.Decode(&intJson); err != nil { + if err := decoder.Decode(&intgr); err != nil { sendErrorJson(err, w, r) return } - integration := intgr.Get() - integration.Enabled = intJson.Enabled - integration.Fields = intJson.Fields - - if err := integrations.Update(integration); err != nil { + if err := intgr.Update(); err != nil { sendErrorJson(err, w, r) return } - list, err := intgr.List() - if err != nil { - sendErrorJson(err, w, r) - return - } - returnJson(list, w, r) + returnJson(intgr, w, r) } diff --git a/handlers/messages.go b/handlers/messages.go index 532fc22f..8fa0dda0 100644 --- a/handlers/messages.go +++ b/handlers/messages.go @@ -16,30 +16,35 @@ package handlers import ( - "encoding/json" "fmt" "github.com/gorilla/mux" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/messages" "github.com/hunterlong/statping/utils" "net/http" ) +func getMessageByID(r *http.Request) (*messages.Message, int64, error) { + vars := mux.Vars(r) + num := utils.ToInt(vars["id"]) + message, err := messages.Find(num) + if err != nil { + return nil, num, err + } + return message, num, nil +} + func apiAllMessagesHandler(r *http.Request) interface{} { - messages := database.AllMessages() - return messages + msgs := messages.All() + return msgs } func apiMessageCreateHandler(w http.ResponseWriter, r *http.Request) { - var message *types.Message - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&message) - if err != nil { + var message *messages.Message + if err := DecodeJSON(r, &message); err != nil { sendErrorJson(err, w, r) return } - _, err = database.Create(message) - if err != nil { + if err := message.Create(); err != nil { sendErrorJson(err, w, r) return } @@ -47,23 +52,21 @@ func apiMessageCreateHandler(w http.ResponseWriter, r *http.Request) { } func apiMessageGetHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - message, err := database.Message(utils.ToInt(vars["id"])) + message, id, err := getMessageByID(r) if err != nil { - sendErrorJson(err, w, r) + sendErrorJson(fmt.Errorf("message #%d was not found", id), w, r) return } returnJson(message, w, r) } func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - message, err := database.Message(utils.ToInt(vars["id"])) + message, id, err := getMessageByID(r) if err != nil { - sendErrorJson(err, w, r) + sendErrorJson(fmt.Errorf("message #%d was not found", id), w, r) return } - err = database.Delete(message) + err = message.Delete() if err != nil { sendErrorJson(err, w, r) return @@ -72,20 +75,16 @@ func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) { } func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - message, err := database.Message(utils.ToInt(vars["id"])) + message, id, err := getMessageByID(r) if err != nil { - sendErrorJson(fmt.Errorf("message #%v was not found", vars["id"]), w, r) + sendErrorJson(fmt.Errorf("message #%d was not found", id), w, r) return } - decoder := json.NewDecoder(r.Body) - err = decoder.Decode(&message) - if err != nil { + if err := DecodeJSON(r, &message); err != nil { sendErrorJson(err, w, r) return } - err = database.Update(message) - if err != nil { + if err := message.Update(); err != nil { sendErrorJson(err, w, r) return } diff --git a/handlers/messages_test.go b/handlers/messages_test.go new file mode 100644 index 00000000..b68b48c2 --- /dev/null +++ b/handlers/messages_test.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestMessagesApiRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping Messages", + URL: "/api/messages", + Method: "GET", + ExpectedStatus: 200, + ExpectedContains: []string{`"title":"Routine Downtime"`}, + }, { + Name: "Statping Create Message", + URL: "/api/messages", + Method: "POST", + Body: `{ + "title": "API Message", + "description": "This is an example a upcoming message for a service!", + "start_on": "2022-11-17T03:28:16.323797-08:00", + "end_on": "2022-11-17T05:13:16.323798-08:00", + "service": 1, + "notify_users": true, + "notify_method": "email", + "notify_before": 6, + "notify_before_scale": "hour" +}`, + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success"`, `"type":"message"`, `"method":"create"`, `"title": "API Message"`}, + }, + { + Name: "Statping View Message", + URL: "/api/messages/1", + Method: "GET", + ExpectedStatus: 200, + ExpectedContains: []string{`"title":"Routine Downtime"`}, + }, { + Name: "Statping Update Message", + URL: "/api/messages/1", + Method: "POST", + Body: `{ + "title": "Updated Message", + "description": "This message was updated", + "start_on": "2022-11-17T03:28:16.323797-08:00", + "end_on": "2022-11-17T05:13:16.323798-08:00", + "service": 1, + "notify_users": true, + "notify_method": "email", + "notify_before": 3, + "notify_before_scale": "hour" +}`, + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success"`, `"type":"message"`, `"method":"update"`}, + }, + { + Name: "Statping Delete Message", + URL: "/api/messages/1", + Method: "DELETE", + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success"`, `"method":"delete"`}, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/handlers/middleware.go b/handlers/middleware.go index 699f9700..ed07d662 100644 --- a/handlers/middleware.go +++ b/handlers/middleware.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/types/core" "github.com/hunterlong/statping/utils" "io" "net/http" @@ -64,7 +64,7 @@ func basicAuthHandler(next http.Handler) http.Handler { // apiMiddleware will confirm if Core has been setup func apiMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !core.CoreApp.Setup { + if !core.App.Setup { sendErrorJson(errors.New("statping has not been setup"), w, r) return } @@ -135,7 +135,7 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r content := CacheStorage.Get(r.RequestURI) w.Header().Set("Content-Type", contentType) w.Header().Set("Access-Control-Allow-Origin", "*") - if !core.IsSetup() { + if !core.App.Setup { handler(w, r) return } diff --git a/handlers/notifications.go b/handlers/notifications.go index 954bd9f2..e2ca1522 100644 --- a/handlers/notifications.go +++ b/handlers/notifications.go @@ -19,17 +19,17 @@ import ( "encoding/json" "fmt" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/null" "github.com/hunterlong/statping/utils" "net/http" ) func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) { - var notifiers []*notifier.Notification - for _, n := range core.CoreApp.Notifications { - notif := n.(notifier.Notifier) + var notifiers []*notifications.Notification + for _, n := range core.App.Notifications { + notif := n.(notifications.Notifier) notifiers = append(notifiers, notif.Select()) } returnJson(notifiers, w, r) @@ -37,33 +37,36 @@ func apiNotifiersHandler(w http.ResponseWriter, r *http.Request) { func apiNotifierGetHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - _, notifierObj, err := notifier.SelectNotifier(vars["notifier"]) + notifier, err := notifications.Find(vars["notifier"]) if err != nil { sendErrorJson(err, w, r) return } - returnJson(notifierObj, w, r) + returnJson(notifier, w, r) } func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - notifer, not, err := notifier.SelectNotifier(vars["notifier"]) + notifer, err := notifications.Find(vars["notifier"]) if err != nil { sendErrorJson(err, w, r) return } + + n := notifer.Select() + decoder := json.NewDecoder(r.Body) - err = decoder.Decode(¬ifer) + err = decoder.Decode(&n) if err != nil { sendErrorJson(err, w, r) return } - _, err = notifier.Update(not, notifer) + err = n.Update() if err != nil { sendErrorJson(err, w, r) return } - notifier.OnSave(notifer.Method) + notifications.OnSave(n.Method) sendJsonAction(notifer, "update", w, r) } @@ -83,48 +86,48 @@ func testNotificationHandler(w http.ResponseWriter, r *http.Request) { apiSecret := form.Get("api_secret") limits := int(utils.ToInt(form.Get("limits"))) - fakeNotifer, notif, err := notifier.SelectNotifier(method) + notifier, err := notifications.Find(method) if err != nil { log.Errorln(fmt.Sprintf("issue saving notifier %v: %v", method, err)) - ExecuteResponse(w, r, "settings.gohtml", core.CoreApp, "settings") + ExecuteResponse(w, r, "settings.gohtml", core.App, "settings") return } - notifer := *fakeNotifer + n := notifier.Select() if host != "" { - notifer.Host = host + n.Host = host } if port != 0 { - notifer.Port = port + n.Port = port } if username != "" { - notifer.Username = username + n.Username = username } if password != "" && password != "##########" { - notifer.Password = password + n.Password = password } if var1 != "" { - notifer.Var1 = var1 + n.Var1 = var1 } if var2 != "" { - notifer.Var2 = var2 + n.Var2 = var2 } if apiKey != "" { - notifer.ApiKey = apiKey + n.ApiKey = apiKey } if apiSecret != "" { - notifer.ApiSecret = apiSecret + n.ApiSecret = apiSecret } if limits != 0 { - notifer.Limits = limits + n.Limits = limits } - notifer.Enabled = types.NewNullBool(enabled == "on") + n.Enabled = null.NewNullBool(enabled == "on") - err = notif.(notifier.Tester).OnTest() - if err == nil { - w.Write([]byte("ok")) - } else { - w.Write([]byte(err.Error())) - } + //err = notifications.OnTest(notifier) + //if err == nil { + // w.Write([]byte("ok")) + //} else { + // w.Write([]byte(err.Error())) + //} } diff --git a/handlers/notifiers_test.go b/handlers/notifiers_test.go new file mode 100644 index 00000000..c83c31dc --- /dev/null +++ b/handlers/notifiers_test.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestApiNotifiersRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping Notifiers", + URL: "/api/notifiers", + Method: "GET", + ExpectedStatus: 200, + }, { + Name: "Statping Mobile Notifier", + URL: "/api/notifier/mobile", + Method: "GET", + ExpectedStatus: 200, + }, { + Name: "Statping Update Notifier", + URL: "/api/notifier/mobile", + Method: "POST", + Body: `{ + "method": "mobile", + "var1": "ExponentPushToken[ToBadIWillError123456]", + "enabled": true, + "limits": 55 +}`, + ExpectedStatus: 200, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/handlers/prometheus.go b/handlers/prometheus.go index 63e7f2bf..3e1ce7b2 100644 --- a/handlers/prometheus.go +++ b/handlers/prometheus.go @@ -17,8 +17,8 @@ package handlers import ( "fmt" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/services" "net/http" "strings" ) @@ -35,21 +35,20 @@ import ( func prometheusHandler(w http.ResponseWriter, r *http.Request) { metrics := []string{} - allFails := database.AllFailures() + allFails := failures.All() system := fmt.Sprintf("statping_total_failures %v\n", allFails) - system += fmt.Sprintf("statping_total_services %v", len(core.Services())) + system += fmt.Sprintf("statping_total_services %v", len(services.All())) metrics = append(metrics, system) - for _, ser := range core.Services() { - v := ser.Model() + for _, ser := range services.All() { online := 1 - if !v.Online { + if !ser.Online { online = 0 } - met := fmt.Sprintf("statping_service_failures{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, v.Failures().Count()) - met += fmt.Sprintf("statping_service_latency{id=\"%v\" name=\"%v\"} %0.0f\n", v.Id, v.Name, (v.Latency * 100)) - met += fmt.Sprintf("statping_service_online{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, online) - met += fmt.Sprintf("statping_service_status_code{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, v.LastStatusCode) - met += fmt.Sprintf("statping_service_response_length{id=\"%v\" name=\"%v\"} %v", v.Id, v.Name, len([]byte(v.LastResponse))) + met := fmt.Sprintf("statping_service_failures{id=\"%v\" name=\"%v\"} %v\n", ser.Id, ser.Name, ser.AllFailures().Count()) + met += fmt.Sprintf("statping_service_latency{id=\"%v\" name=\"%v\"} %0.0f\n", ser.Id, ser.Name, (ser.Latency * 100)) + met += fmt.Sprintf("statping_service_online{id=\"%v\" name=\"%v\"} %v\n", ser.Id, ser.Name, online) + met += fmt.Sprintf("statping_service_status_code{id=\"%v\" name=\"%v\"} %v\n", ser.Id, ser.Name, ser.LastStatusCode) + met += fmt.Sprintf("statping_service_response_length{id=\"%v\" name=\"%v\"} %v", ser.Id, ser.Name, len([]byte(ser.LastResponse))) metrics = append(metrics, met) } output := strings.Join(metrics, "\n") diff --git a/handlers/query.go b/handlers/query.go index 5ac8282f..4137bf34 100644 --- a/handlers/query.go +++ b/handlers/query.go @@ -1 +1,26 @@ package handlers + +import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" + "github.com/hunterlong/statping/utils" + "net/http" +) + +func DecodeJSON(r *http.Request, obj interface{}) error { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&obj) + if err != nil { + return err + } + return nil +} + +func GetID(r *http.Request) (int64, error) { + vars := mux.Vars(r) + if vars["id"] == "" { + return 0, errors.New("no id specified in request") + } + return utils.ToInt(vars["id"]), nil +} diff --git a/handlers/routes.go b/handlers/routes.go index ee6f7b40..1449a10e 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -18,8 +18,8 @@ package handlers import ( "fmt" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/source" + "github.com/hunterlong/statping/types/core" "github.com/hunterlong/statping/utils" "net/http" ) @@ -188,5 +188,5 @@ func resetRouter() { } func resetCookies() { - jwtKey = fmt.Sprintf("%v_%v", core.CoreApp.ApiSecret, utils.Now().Nanosecond()) + jwtKey = fmt.Sprintf("%v_%v", core.App.ApiSecret, utils.Now().Nanosecond()) } diff --git a/handlers/services.go b/handlers/services.go index 3f50e795..265dc5b6 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -16,13 +16,13 @@ package handlers import ( - "encoding/json" - "errors" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/hits" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" "net/http" ) @@ -31,85 +31,83 @@ type serviceOrder struct { Order int `json:"order"` } +func serviceByID(r *http.Request) (*services.Service, error) { + vars := mux.Vars(r) + id := utils.ToInt(vars["id"]) + servicer, err := services.Find(id) + if err != nil { + return nil, errors.Errorf("service %d not found", id) + } + return servicer, nil +} + func reorderServiceHandler(w http.ResponseWriter, r *http.Request) { r.ParseForm() var newOrder []*serviceOrder - decoder := json.NewDecoder(r.Body) - decoder.Decode(&newOrder) + if err := DecodeJSON(r, &newOrder); err != nil { + sendErrorJson(err, w, r) + return + } + for _, s := range newOrder { - service := core.SelectService(s.Id).Model() + service, err := services.Find(s.Id) + if err != nil { + sendErrorJson(errors.Errorf("service %d not found", s.Id), w, r) + return + } service.Order = s.Order - database.Update(service) + service.Update() } returnJson(newOrder, w, r) } func apiServiceHandler(r *http.Request) interface{} { - vars := mux.Vars(r) - servicer := core.SelectService(utils.ToInt(vars["id"])) - if servicer == nil { - return errors.New("service not found") + service, err := serviceByID(r) + if err != nil { + return err } - return servicer.Service + return service } func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) { - var service *types.Service - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&service) - if err != nil { - sendErrorJson(err, w, r) - return - } - _, err = database.Create(service) - if err != nil { - sendErrorJson(err, w, r) - return - } - sendJsonAction(service, "create", w, r) -} - -func apiTestServiceHandler(w http.ResponseWriter, r *http.Request) { - var service *types.Service - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&service) - if err != nil { + var service *services.Service + if err := DecodeJSON(r, &service); err != nil { sendErrorJson(err, w, r) return } - _, err = database.Create(service) - if err != nil { + if err := service.Create(); err != nil { sendErrorJson(err, w, r) return } + sendJsonAction(service, "create", w, r) } func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - service := core.SelectService(utils.ToInt(vars["id"])) - if service == nil { - sendErrorJson(errors.New("service not found"), w, r) - return - } - decoder := json.NewDecoder(r.Body) - decoder.Decode(&service) - err := database.Update(service) + service, err := serviceByID(r) if err != nil { sendErrorJson(err, w, r) return } - go core.CheckService(service, true) + if err := DecodeJSON(r, &service); err != nil { + sendErrorJson(err, w, r) + return + } + + err = service.Update() + if err != nil { + sendErrorJson(err, w, r) + return + } + go services.CheckService(service, true) sendJsonAction(service, "update", w, r) } func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - srv := core.SelectService(utils.ToInt(vars["id"])) - service := srv.Model() - if service == nil { - sendErrorJson(errors.New("service not found"), w, r) + service, err := serviceByID(r) + if err != nil { + sendErrorJson(err, w, r) return } if service.IsRunning() { @@ -122,13 +120,13 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) { func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service, err := database.Service(utils.ToInt(vars["id"])) + service, err := services.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := database.ParseQueries(r, service.Hits()) + groupQuery := database.ParseQueries(r, service.AllHits()) objs, err := groupQuery.GraphData(database.ByAverage("latency")) if err != nil { @@ -140,13 +138,13 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service, err := database.Service(utils.ToInt(vars["id"])) + service, err := services.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := database.ParseQueries(r, service.Failures()) + groupQuery := database.ParseQueries(r, service.AllFailures()) objs, err := groupQuery.GraphData(database.ByCount) if err != nil { @@ -158,14 +156,13 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { } func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - service, err := database.Service(utils.ToInt(vars["id"])) + service, err := serviceByID(r) if err != nil { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := database.ParseQueries(r, service.Hits()) + groupQuery := database.ParseQueries(r, service.AllHits()) objs, err := groupQuery.GraphData(database.ByAverage("ping_time")) if err != nil { @@ -176,85 +173,69 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { returnJson(objs, w, r) } -type dataXy struct { - X int `json:"x"` - Y int `json:"y"` -} - -type dataXyMonth struct { - Date string `json:"date"` - Data []*dataXy `json:"data"` -} - func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - service := core.SelectService(utils.ToInt(vars["id"])) - if service == nil { - sendErrorJson(errors.New("service not found"), w, r) - return - } - err := database.Delete(service) + service, err := serviceByID(r) if err != nil { sendErrorJson(err, w, r) return } + err = service.Delete() + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(service, "delete", w, r) } func apiAllServicesHandler(r *http.Request) interface{} { - services := core.Services() - return joinServices(services) -} - -func joinServices(srvs map[int64]*core.Service) []*types.Service { - var services []*types.Service - for _, v := range srvs { - v.UpdateStats() - services = append(services, v.Service) - } + services := services.All() return services } -func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - srv, err := database.Service(utils.ToInt(vars["id"])) - if err != nil { - sendErrorJson(errors.New("service not found"), w, r) - return +func joinServices(srvss map[int64]*services.Service) []*services.Service { + var srvs []*services.Service + for _, v := range srvss { + v.UpdateStats() + srvs = append(srvs, v) } - err = srv.Failures().DeleteAll() + return srvs +} + +func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) { + service, err := serviceByID(r) if err != nil { sendErrorJson(err, w, r) return } - sendJsonAction(srv.Model(), "delete_failures", w, r) + err = service.DeleteFailures() + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(service, "delete_failures", w, r) } func apiServiceFailuresHandler(r *http.Request) interface{} { vars := mux.Vars(r) - - service, err := database.Service(utils.ToInt(vars["id"])) + service, err := services.Find(utils.ToInt(vars["id"])) if err != nil { return errors.New("service not found") } - var fails []*types.Failure - database.ParseQueries(r, service.Failures()).Find(&fails) + var fails []*failures.Failure + database.ParseQueries(r, service.AllFailures()).Find(&fails) return fails } func apiServiceHitsHandler(r *http.Request) interface{} { vars := mux.Vars(r) - service, err := database.Service(utils.ToInt(vars["id"])) + service, err := services.Find(utils.ToInt(vars["id"])) if err != nil { return errors.New("service not found") } - var hits []types.Hit - database.ParseQueries(r, service.Hits()).Find(&hits) - return hits -} - -func createServiceHandler(w http.ResponseWriter, r *http.Request) { - ExecuteResponse(w, r, "service_create.gohtml", core.CoreApp, nil) + var hts []*hits.Hit + database.ParseQueries(r, service.AllHits()).Find(&hts) + return hts } diff --git a/handlers/services_test.go b/handlers/services_test.go new file mode 100644 index 00000000..f7570dd4 --- /dev/null +++ b/handlers/services_test.go @@ -0,0 +1,147 @@ +package handlers + +import ( + "github.com/hunterlong/statping/core" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "testing" +) + +func TestApiServiceRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping All Services", + URL: "/api/services", + Method: "GET", + ExpectedContains: []string{`"name":"Google"`}, + ExpectedStatus: 200, + ResponseLen: 5, + FuncTest: func() error { + count := len(core.Services()) + if count != 5 { + return errors.Errorf("incorrect services count: %d", count) + } + return nil + }, + }, + { + Name: "Statping Service 1", + URL: "/api/services/1", + Method: "GET", + ExpectedContains: []string{`"name":"Google"`}, + ExpectedStatus: 200, + }, + { + Name: "Statping Service 1 Data", + URL: "/api/services/1/hits_data", + Method: "GET", + Body: "", + ExpectedStatus: 200, + }, + { + Name: "Statping Service 1 Ping Data", + URL: "/api/services/1/ping_data", + Method: "GET", + Body: "", + ExpectedStatus: 200, + }, + { + Name: "Statping Service 1 Failure Data", + URL: "/api/services/1/failure_data", + Method: "GET", + Body: "", + ExpectedStatus: 200, + }, + { + Name: "Statping Service 1 Hits", + URL: "/api/services/1/hits_data", + Method: "GET", + Body: "", + ExpectedStatus: 200, + }, + { + Name: "Statping Service 1 Failures", + URL: "/api/services/1/failure_data", + Method: "GET", + Body: "", + ExpectedStatus: 200, + }, + { + Name: "Statping Reorder Services", + URL: "/api/services/reorder", + Method: "POST", + Body: `[{"service":1,"order":1},{"service":5,"order":2},{"service":2,"order":3},{"service":3,"order":4},{"service":4,"order":5}]`, + ExpectedStatus: 200, + HttpHeaders: []string{"Content-Type=application/json"}, + }, + { + Name: "Statping Create Service", + URL: "/api/services", + HttpHeaders: []string{"Content-Type=application/json"}, + Method: "POST", + Body: `{ + "name": "New Service", + "domain": "https://statping.com", + "expected": "", + "expected_status": 200, + "check_interval": 30, + "type": "http", + "method": "GET", + "post_data": "", + "port": 0, + "timeout": 30, + "order_id": 0 +}`, + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success","type":"service","method":"create"`}, + FuncTest: func() error { + count := len(core.Services()) + if count != 6 { + return errors.Errorf("incorrect services count: %d", count) + } + return nil + }, + }, + { + Name: "Statping Update Service", + URL: "/api/services/1", + HttpHeaders: []string{"Content-Type=application/json"}, + Method: "POST", + Body: `{ + "name": "Updated New Service", + "domain": "https://google.com", + "expected": "", + "expected_status": 200, + "check_interval": 60, + "type": "http", + "method": "GET", + "post_data": "", + "port": 0, + "timeout": 10, + "order_id": 0 +}`, + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success"`, `"name":"Updated New Service"`, `"method":"update"`}, + }, + { + Name: "Statping Delete Service", + URL: "/api/services/1", + Method: "DELETE", + ExpectedStatus: 200, + ExpectedContains: []string{`"status":"success"`, `"method":"delete"`}, + FuncTest: func() error { + count := len(core.Services()) + if count != 5 { + return errors.Errorf("incorrect services count: %d", count) + } + return nil + }, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/handlers/setup.go b/handlers/setup.go index 0c49190d..40d7184f 100644 --- a/handlers/setup.go +++ b/handlers/setup.go @@ -17,9 +17,10 @@ package handlers import ( "errors" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/configs" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" "net/http" "strconv" @@ -28,7 +29,7 @@ import ( func processSetupHandler(w http.ResponseWriter, r *http.Request) { var err error - if core.CoreApp.Setup { + if core.App.Setup { sendErrorJson(errors.New("Statping has already been setup"), w, r) return } @@ -50,9 +51,8 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { domain := r.PostForm.Get("domain") email := r.PostForm.Get("email") sample, _ := strconv.ParseBool(r.PostForm.Get("sample_data")) - dir := utils.Directory - configs := &types.DbConfig{ + confg := &configs.DbConfig{ DbConn: dbConn, DbHost: dbHost, DbUser: dbUser, @@ -69,68 +69,83 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { Location: utils.Directory, } - log.WithFields(utils.ToFields(core.CoreApp, configs)).Debugln("new configs posted") + log.WithFields(utils.ToFields(core.App, confg)).Debugln("new configs posted") - if err := core.SaveConfig(configs); err != nil { + if err := confg.Save(utils.Directory); err != nil { log.Errorln(err) sendErrorJson(err, w, r) return } - if _, err = core.LoadConfigFile(dir); err != nil { + if _, err = configs.LoadConfigs(); err != nil { log.Errorln(err) sendErrorJson(err, w, r) return } - if err = core.CoreApp.Connect(configs, false, dir); err != nil { + if err = configs.ConnectConfigs(confg); err != nil { log.Errorln(err) - core.DeleteConfig() + if err := confg.Delete(); err != nil { + log.Errorln(err) + sendErrorJson(err, w, r) + } + } + + if err = configs.MigrateDatabase(); err != nil { sendErrorJson(err, w, r) return } - if err = core.CoreApp.DropDatabase(); err != nil { - sendErrorJson(err, w, r) - return + c := &core.Core{ + Name: "Statping Sample Data", + Description: "This data is only used to testing", + //ApiKey: apiKey.(string), + //ApiSecret: apiSecret.(string), + Domain: "http://localhost:8080", + Version: "test", + CreatedAt: time.Now().UTC(), + UseCdn: null.NewNullBool(false), + Footer: null.NewNullString(""), } - if err = core.CoreApp.CreateDatabase(); err != nil { - sendErrorJson(err, w, r) - return - } - - core.CoreApp, err = core.InsertCore(configs) - if err != nil { + if err := c.Create(); err != nil { log.Errorln(err) sendErrorJson(err, w, r) return } - admin := &types.User{ - Username: configs.Username, - Password: configs.Password, - Email: configs.Email, - Admin: types.NewNullBool(true), + core.App = c + + admin := &users.User{ + Username: confg.Username, + Password: confg.Password, + Email: confg.Email, + Admin: null.NewNullBool(true), + } + + if err := admin.Create(); err != nil { + log.Errorln(err) + sendErrorJson(err, w, r) + return } - database.Create(admin) if sample { - if err = core.SampleData(); err != nil { + if err = configs.TriggerSamples(); err != nil { sendErrorJson(err, w, r) return } } + core.InitApp() CacheStorage.Delete("/") resetCookies() time.Sleep(1 * time.Second) out := struct { - Message string `json:"message"` - Config *types.DbConfig `json:"config"` + Message string `json:"message"` + Config *configs.DbConfig `json:"config"` }{ "success", - configs, + confg, } returnJson(out, w, r) } diff --git a/handlers/users.go b/handlers/users.go index d24276dd..1e7b9012 100644 --- a/handlers/users.go +++ b/handlers/users.go @@ -16,20 +16,17 @@ package handlers import ( - "encoding/json" "errors" "fmt" "github.com/gorilla/mux" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" "net/http" ) func apiUserHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - user, err := core.SelectUser(utils.ToInt(vars["id"])) + user, err := users.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(err, w, r) return @@ -40,16 +37,22 @@ func apiUserHandler(w http.ResponseWriter, r *http.Request) { func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - user, err := core.SelectUser(utils.ToInt(vars["id"])) + user, err := users.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(fmt.Errorf("user #%v was not found", vars["id"]), w, r) return } - decoder := json.NewDecoder(r.Body) - decoder.Decode(&user) + + err = DecodeJSON(r, &user) + if err != nil { + sendErrorJson(fmt.Errorf("user #%v was not found", vars["id"]), w, r) + return + } + if user.Password != "" { user.Password = utils.HashPassword(user.Password) } + err = user.Update() if err != nil { sendErrorJson(fmt.Errorf("issue updating user #%v: %v", user.Id, err), w, r) @@ -60,12 +63,12 @@ func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) { func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - users := database.AllUsers() - if len(users) == 1 { + allUsers := users.All() + if len(allUsers) == 1 { sendErrorJson(errors.New("cannot delete the last user"), w, r) return } - user, err := core.SelectUser(utils.ToInt(vars["id"])) + user, err := users.Find(utils.ToInt(vars["id"])) if err != nil { sendErrorJson(err, w, r) return @@ -79,19 +82,20 @@ func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) { } func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) { - users := core.SelectAllUsers() - returnJson(users, w, r) + allUsers := users.All() + returnJson(allUsers, w, r) } func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) { - var user *types.User - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&user) + var user *users.User + + err := DecodeJSON(r, &user) if err != nil { sendErrorJson(err, w, r) return } - _, err = database.Create(user) + + err = user.Create() if err != nil { sendErrorJson(err, w, r) return diff --git a/handlers/users_test.go b/handlers/users_test.go new file mode 100644 index 00000000..a9e82ed8 --- /dev/null +++ b/handlers/users_test.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestApiUsersRoutes(t *testing.T) { + tests := []HTTPTest{ + { + Name: "Statping All Users", + URL: "/api/users", + Method: "GET", + ExpectedStatus: 200, + ResponseLen: 1, + }, { + Name: "Statping Create User", + URL: "/api/users", + HttpHeaders: []string{"Content-Type=application/json"}, + Method: "POST", + Body: `{ + "username": "adminuser2", + "email": "info@adminemail.com", + "password": "passsword123", + "admin": true +}`, + ExpectedStatus: 200, + }, { + Name: "Statping View User", + URL: "/api/users/1", + Method: "GET", + ExpectedStatus: 200, + }, { + Name: "Statping Update User", + URL: "/api/users/1", + Method: "POST", + Body: `{ + "username": "adminupdated", + "email": "info@email.com", + "password": "password12345", + "admin": true +}`, + ExpectedStatus: 200, + }, { + Name: "Statping Delete User", + URL: "/api/users/1", + Method: "DELETE", + ExpectedStatus: 200, + }} + + for _, v := range tests { + t.Run(v.Name, func(t *testing.T) { + _, t, err := RunHTTPTest(v, t) + require.Nil(t, err) + }) + } +} diff --git a/core/integrations/csv_file.go b/integrators/csv_file.go similarity index 77% rename from core/integrations/csv_file.go rename to integrators/csv_file.go index 89820320..cdbed161 100644 --- a/core/integrations/csv_file.go +++ b/integrators/csv_file.go @@ -13,14 +13,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package integrations +package integrators import ( "bytes" "encoding/csv" "errors" "fmt" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "strconv" "time" @@ -29,15 +31,15 @@ import ( const requiredSize = 17 type csvIntegration struct { - *types.Integration + *integrations.Integration } -var CsvIntegrator = &csvIntegration{&types.Integration{ +var CsvIntegrator = &csvIntegration{&integrations.Integration{ ShortName: "csv", Name: "CSV File", Icon: "", Description: "Import multiple services from a CSV file. Please have your CSV file formatted with the correct amount of columns based on the example file on Github.", - Fields: []*types.IntegrationField{ + Fields: []*integrations.IntegrationField{ { Name: "input", Type: "textarea", @@ -48,11 +50,11 @@ var CsvIntegrator = &csvIntegration{&types.Integration{ var csvData [][]string -func (t *csvIntegration) Get() *types.Integration { +func (t *csvIntegration) Get() *integrations.Integration { return t.Integration } -func (t *csvIntegration) List() ([]*types.Service, error) { +func (t *csvIntegration) List() ([]*services.Service, error) { data := Value(t, "input").(string) buf := bytes.NewReader([]byte(data)) r := csv.NewReader(buf) @@ -61,7 +63,7 @@ func (t *csvIntegration) List() ([]*types.Service, error) { return nil, err } - var services []*types.Service + var services []*services.Service for k, v := range records[1:] { s, err := commaToService(v) if err != nil { @@ -75,7 +77,7 @@ func (t *csvIntegration) List() ([]*types.Service, error) { // commaToService will convert a CSV comma delimited string slice to a Service type // this function is used for the bulk import services feature -func commaToService(s []string) (*types.Service, error) { +func commaToService(s []string) (*services.Service, error) { if len(s) != requiredSize { err := fmt.Errorf("file has %v columns of data, not the expected amount of %v columns for a service", len(s), requiredSize) return nil, err @@ -106,23 +108,23 @@ func commaToService(s []string) (*types.Service, error) { return nil, errors.New("could not parse verifiy SSL boolean: " + s[16]) } - newService := &types.Service{ + newService := &services.Service{ Name: s[0], Domain: s[1], - Expected: types.NewNullString(s[2]), + Expected: null.NewNullString(s[2]), ExpectedStatus: int(utils.ToInt(s[3])), Interval: int(utils.ToInt(interval.Seconds())), Type: s[5], Method: s[6], - PostData: types.NewNullString(s[7]), + PostData: null.NewNullString(s[7]), Port: int(utils.ToInt(s[8])), Timeout: int(utils.ToInt(timeout.Seconds())), - AllowNotifications: types.NewNullBool(allowNotifications), - Public: types.NewNullBool(public), + AllowNotifications: null.NewNullBool(allowNotifications), + Public: null.NewNullBool(public), GroupId: int(utils.ToInt(s[13])), - Headers: types.NewNullString(s[14]), - Permalink: types.NewNullString(s[15]), - VerifySSL: types.NewNullBool(verifySsl), + Headers: null.NewNullString(s[14]), + Permalink: null.NewNullString(s[15]), + VerifySSL: null.NewNullBool(verifySsl), } return newService, nil diff --git a/core/integrations/csv_file_test.go b/integrators/csv_file_test.go similarity index 98% rename from core/integrations/csv_file_test.go rename to integrators/csv_file_test.go index 3ec27e51..992b5a0b 100644 --- a/core/integrations/csv_file_test.go +++ b/integrators/csv_file_test.go @@ -1,4 +1,4 @@ -package integrations +package integrators import ( "github.com/stretchr/testify/assert" diff --git a/core/integrations/docker.go b/integrators/docker.go similarity index 83% rename from core/integrations/docker.go rename to integrators/docker.go index febd5820..121d8f2c 100644 --- a/core/integrations/docker.go +++ b/integrators/docker.go @@ -13,21 +13,22 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package integrations +package integrators import ( "context" dTypes "github.com/docker/docker/api/types" "github.com/docker/docker/client" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/services" "os" ) type dockerIntegration struct { - *types.Integration + *integrations.Integration } -var DockerIntegrator = &dockerIntegration{&types.Integration{ +var DockerIntegrator = &dockerIntegration{&integrations.Integration{ ShortName: "docker", Name: "Docker", Icon: "", @@ -35,7 +36,7 @@ var DockerIntegrator = &dockerIntegration{&types.Integration{ You can also do this in Docker by setting -v /var/run/docker.sock:/var/run/docker.sock in the Statping Docker container. All of the containers with open TCP/UDP ports will be listed for you to choose which services you want to add. If you running Statping inside of a container, this container must be attached to all networks you want to communicate with.`, - Fields: []*types.IntegrationField{ + Fields: []*integrations.IntegrationField{ { Name: "path", Description: "The absolute path to the Docker unix socket", @@ -53,11 +54,11 @@ this container must be attached to all networks you want to communicate with.`, var cli *client.Client -func (t *dockerIntegration) Get() *types.Integration { +func (t *dockerIntegration) Get() *integrations.Integration { return t.Integration } -func (t *dockerIntegration) List() ([]*types.Service, error) { +func (t *dockerIntegration) List() ([]*services.Service, error) { var err error path := Value(t, "path").(string) version := Value(t, "version").(string) @@ -69,7 +70,7 @@ func (t *dockerIntegration) List() ([]*types.Service, error) { } defer cli.Close() - var services []*types.Service + var srvs []*services.Service containers, err := cli.ContainerList(context.Background(), dTypes.ContainerListOptions{}) if err != nil { @@ -86,7 +87,7 @@ func (t *dockerIntegration) List() ([]*types.Service, error) { continue } - service := &types.Service{ + service := &services.Service{ Name: container.Names[0][1:], Domain: v.IP, Type: v.Type, @@ -95,9 +96,9 @@ func (t *dockerIntegration) List() ([]*types.Service, error) { Timeout: 2, } - services = append(services, service) + srvs = append(srvs, service) } } - return services, nil + return srvs, nil } diff --git a/core/integrations/docker_test.go b/integrators/docker_test.go similarity index 97% rename from core/integrations/docker_test.go rename to integrators/docker_test.go index 8c1a0cc6..907fda7c 100644 --- a/core/integrations/docker_test.go +++ b/integrators/docker_test.go @@ -1,4 +1,4 @@ -package integrations +package integrators import ( "github.com/stretchr/testify/assert" diff --git a/core/integrations/integrations.go b/integrators/integrations.go similarity index 70% rename from core/integrations/integrations.go rename to integrators/integrations.go index 65bfe5d9..7c27edb2 100644 --- a/core/integrations/integrations.go +++ b/integrators/integrations.go @@ -13,19 +13,19 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package integrations +package integrators import ( "encoding/json" "errors" "fmt" "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/integrations" "github.com/hunterlong/statping/utils" ) var ( - Integrations []types.Integrator + Integrations []integrations.Integrator log = utils.Log.WithField("type", "integration") db database.Database ) @@ -38,9 +38,17 @@ var ( // ) //} +func init() { + AddIntegrations( + CsvIntegrator, + TraefikIntegrator, + DockerIntegrator, + ) +} + // integrationsDb returns the 'integrations' database column func integrationsDb() database.Database { - return db.Model(&types.Integration{}) + return db.Model(&integrations.Integration{}) } // SetDB is called by core to inject the database for a integrator to use @@ -48,7 +56,7 @@ func SetDB(d database.Database) { db = d } -func Value(intg types.Integrator, fieldName string) interface{} { +func Value(intg integrations.Integrator, fieldName string) interface{} { for _, v := range intg.Get().Fields { if fieldName == v.Name { return v.Value @@ -57,16 +65,16 @@ func Value(intg types.Integrator, fieldName string) interface{} { return nil } -func Update(integrator *types.Integration) error { +func Update(integrator *integrations.Integration) error { fields := FieldsToJson(integrator) fmt.Println(fields) - set := db.Model(&types.Integration{}).Where("name = ?", integrator.Name) + set := db.Model(&integrations.Integration{}).Where("name = ?", integrator.Name) set.Set("enabled", integrator.Enabled) set.Set("fields", fields) return set.Error() } -func FieldsToJson(integrator *types.Integration) string { +func FieldsToJson(integrator *integrations.Integration) string { jsonData := make(map[string]interface{}) for _, v := range integrator.Fields { jsonData[v.Name] = v.Value @@ -75,7 +83,7 @@ func FieldsToJson(integrator *types.Integration) string { return string(data) } -func JsonToFields(intg types.Integrator, input string) []*types.IntegrationField { +func JsonToFields(intg integrations.Integrator, input string) []*integrations.IntegrationField { integrator := intg.Get() var jsonData map[string]interface{} json.Unmarshal([]byte(input), &jsonData) @@ -86,7 +94,7 @@ func JsonToFields(intg types.Integrator, input string) []*types.IntegrationField return integrator.Fields } -func SetFields(intg types.Integrator, data map[string][]string) (*types.Integration, error) { +func SetFields(intg integrations.Integrator, data map[string][]string) (*integrations.Integration, error) { i := intg.Get() for _, v := range i.Fields { if data[v.Name] != nil { @@ -96,7 +104,7 @@ func SetFields(intg types.Integrator, data map[string][]string) (*types.Integrat return i, nil } -func Find(name string) (types.Integrator, error) { +func Find(name string) (integrations.Integrator, error) { for _, i := range Integrations { obj := i.Get() if obj.ShortName == name { @@ -107,27 +115,27 @@ func Find(name string) (types.Integrator, error) { } // db will return the notifier database column/record -func integratorDb(n *types.Integration) database.Database { - return db.Model(&types.Integration{}).Where("name = ?", n.Name).Find(n) +func integratorDb(n *integrations.Integration) database.Database { + return db.Model(&integrations.Integration{}).Where("name = ?", n.Name).Find(n) } // isInDatabase returns true if the integration has already been installed -func isInDatabase(i types.Integrator) bool { +func isInDatabase(i integrations.Integrator) bool { inDb := integratorDb(i.Get()).RecordNotFound() return !inDb } // SelectIntegration returns the Notification struct from the database -func SelectIntegration(i types.Integrator) (*types.Integration, error) { +func SelectIntegration(i integrations.Integrator) (*integrations.Integration, error) { integration := i.Get() - err := db.Model(&types.Integration{}).Where("name = ?", integration.Name).Scan(&integration) + err := db.Model(&integrations.Integration{}).Where("name = ?", integration.Name).Scan(&integration) return integration, err.Error() } // AddIntegrations accept a Integrator interface to be added into the array -func AddIntegrations(integrations ...types.Integrator) error { - for _, i := range integrations { - if utils.IsType(i, new(types.Integrator)) { +func AddIntegrations(inte ...integrations.Integrator) error { + for _, i := range inte { + if utils.IsType(i, new(integrations.Integrator)) { Integrations = append(Integrations, i) err := install(i) if err != nil { @@ -141,7 +149,7 @@ func AddIntegrations(integrations ...types.Integrator) error { } // install will check the database for the notification, if its not inserted it will insert a new record for it -func install(i types.Integrator) error { +func install(i integrations.Integrator) error { inDb := isInDatabase(i) log.WithField("installed", inDb). WithFields(utils.ToFields(i)). @@ -157,7 +165,7 @@ func install(i types.Integrator) error { } // insertDatabase will create a new record into the database for the integrator -func insertDatabase(i types.Integrator) (string, error) { +func insertDatabase(i integrations.Integrator) (string, error) { integrator := i.Get() query := db.Create(integrator) if query.Error() != nil { diff --git a/core/integrations/traefik.go b/integrators/traefik.go similarity index 81% rename from core/integrations/traefik.go rename to integrators/traefik.go index d7d98671..286531d1 100644 --- a/core/integrations/traefik.go +++ b/integrators/traefik.go @@ -13,26 +13,27 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package integrations +package integrators import ( "encoding/json" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "net/url" "time" ) type traefikIntegration struct { - *types.Integration + *integrations.Integration } -var TraefikIntegrator = &traefikIntegration{&types.Integration{ +var TraefikIntegrator = &traefikIntegration{&integrations.Integration{ ShortName: "traefik", Name: "Traefik", Icon: "", Description: ``, - Fields: []*types.IntegrationField{ + Fields: []*integrations.IntegrationField{ { Name: "endpoint", Description: "The URL for the traefik API Endpoint", @@ -52,13 +53,13 @@ var TraefikIntegrator = &traefikIntegration{&types.Integration{ }, }} -func (t *traefikIntegration) Get() *types.Integration { +func (t *traefikIntegration) Get() *integrations.Integration { return t.Integration } -func (t *traefikIntegration) List() ([]*types.Service, error) { +func (t *traefikIntegration) List() ([]*services.Service, error) { var err error - var services []*types.Service + var services []*services.Service endpoint := Value(t, "endpoint").(string) @@ -77,9 +78,9 @@ func (t *traefikIntegration) List() ([]*types.Service, error) { return services, err } -func fetchMethod(endpoint, method string) ([]*types.Service, error) { +func fetchMethod(endpoint, method string) ([]*services.Service, error) { var traefikServices []traefikService - var services []*types.Service + var srvs []*services.Service d, _, err := utils.HttpRequest(endpoint+"/api/"+method+"/services", "GET", nil, []string{}, nil, 10*time.Second, false) if err != nil { return nil, err @@ -97,7 +98,7 @@ func fetchMethod(endpoint, method string) ([]*types.Service, error) { return nil, err } - service := &types.Service{ + service := &services.Service{ Name: s.Name, Domain: url.Hostname(), Port: int(utils.ToInt(url.Port())), @@ -105,11 +106,11 @@ func fetchMethod(endpoint, method string) ([]*types.Service, error) { Interval: 60, Timeout: 2, } - services = append(services, service) + srvs = append(srvs, service) } } - return services, err + return srvs, err } type traefikService struct { diff --git a/core/integrations/traefik_test.go b/integrators/traefik_test.go similarity index 96% rename from core/integrations/traefik_test.go rename to integrators/traefik_test.go index 3e91de1f..fe7ef99c 100644 --- a/core/integrations/traefik_test.go +++ b/integrators/traefik_test.go @@ -1,4 +1,4 @@ -package integrations +package integrators import ( "github.com/stretchr/testify/assert" diff --git a/notifiers/command.go b/notifiers/command.go index 461684b0..553b1f94 100644 --- a/notifiers/command.go +++ b/notifiers/command.go @@ -17,18 +17,19 @@ package notifiers import ( "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "strings" "time" ) type commandLine struct { - *notifier.Notification + *notifications.Notification } -var Command = &commandLine{¬ifier.Notification{ +var Command = &commandLine{¬ifications.Notification{ Method: "Command", Title: "Shell Command", Description: "Shell Command allows you to run a customized shell/bash Command on the local machine it's running on.", @@ -37,7 +38,7 @@ var Command = &commandLine{¬ifier.Notification{ Delay: time.Duration(1 * time.Second), Icon: "fas fa-terminal", Host: "/bin/bash", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Shell or Bash", Placeholder: "/bin/bash", @@ -63,17 +64,17 @@ func runCommand(app string, cmd ...string) (string, string, error) { return outStr, errStr, err } -func (u *commandLine) Select() *notifier.Notification { +func (u *commandLine) Select() *notifications.Notification { return u.Notification } // OnFailure for commandLine will trigger failing service -func (u *commandLine) OnFailure(s *types.Service, f *types.Failure) { +func (u *commandLine) OnFailure(s *services.Service, f *failures.Failure) { u.AddQueue(fmt.Sprintf("service_%v", s.Id), u.Var2) } // OnSuccess for commandLine will trigger successful service -func (u *commandLine) OnSuccess(s *types.Service) { +func (u *commandLine) OnSuccess(s *services.Service) { if !s.Online { u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) u.AddQueue(fmt.Sprintf("service_%v", s.Id), u.Var1) diff --git a/notifiers/discord.go b/notifiers/discord.go index f7e7e01b..3fd60401 100644 --- a/notifiers/discord.go +++ b/notifiers/discord.go @@ -20,18 +20,19 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "strings" "time" ) type discord struct { - *notifier.Notification + *notifications.Notification } -var Discorder = &discord{¬ifier.Notification{ +var Discorder = &discord{¬ifications.Notification{ Method: "discord", Title: "discord", Description: "Send notifications to your discord channel using discord webhooks. Insert your discord channel Webhook URL to receive notifications. Based on the discord webhooker API.", @@ -40,7 +41,7 @@ var Discorder = &discord{¬ifier.Notification{ Delay: time.Duration(5 * time.Second), Host: "https://discordapp.com/api/webhooks/****/*****", Icon: "fab fa-discord", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "discord webhooker URL", Placeholder: "Insert your Webhook URL here", @@ -55,18 +56,18 @@ func (u *discord) Send(msg interface{}) error { return err } -func (u *discord) Select() *notifier.Notification { +func (u *discord) Select() *notifications.Notification { return u.Notification } // OnFailure will trigger failing service -func (u *discord) OnFailure(s *types.Service, f *types.Failure) { +func (u *discord) OnFailure(s *services.Service, f *failures.Failure) { msg := fmt.Sprintf(`{"content": "Your service '%v' is currently failing! Reason: %v"}`, s.Name, f.Issue) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnSuccess will trigger successful service -func (u *discord) OnSuccess(s *types.Service) { +func (u *discord) OnSuccess(s *services.Service) { if !s.Online || !s.SuccessNotified { u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) var msg interface{} diff --git a/notifiers/email.go b/notifiers/email.go index 75452483..ca6515ee 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -20,8 +20,10 @@ import ( "crypto/tls" "fmt" "github.com/go-mail/mail" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "html/template" "time" @@ -108,17 +110,17 @@ var ( ) type email struct { - *notifier.Notification + *notifications.Notification } -var Emailer = &email{¬ifier.Notification{ +var Emailer = &email{¬ifications.Notification{ Method: "email", Title: "email", Description: "Send emails via SMTP when services are online or offline.", Author: "Hunter Long", AuthorUrl: "https://github.com/hunterlong", Icon: "far fa-envelope", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "SMTP Host", Placeholder: "Insert your SMTP Host here.", @@ -178,7 +180,7 @@ type emailOutgoing struct { } // OnFailure will trigger failing service -func (u *email) OnFailure(s *types.Service, f *types.Failure) { +func (u *email) OnFailure(s *services.Service, f *failures.Failure) { email := &emailOutgoing{ To: u.Var2, Subject: fmt.Sprintf("Service %v is Failing", s.Name), @@ -190,7 +192,7 @@ func (u *email) OnFailure(s *types.Service, f *types.Failure) { } // OnSuccess will trigger successful service -func (u *email) OnSuccess(s *types.Service) { +func (u *email) OnSuccess(s *services.Service) { if !s.Online || !s.SuccessNotified { var msg string msg = s.DownText @@ -207,7 +209,7 @@ func (u *email) OnSuccess(s *types.Service) { } } -func (u *email) Select() *notifier.Notification { +func (u *email) Select() *notifications.Notification { return u.Notification } @@ -220,7 +222,7 @@ func (u *email) OnSave() error { // OnTest triggers when this notifier has been saved func (u *email) OnTest() error { - testService := &types.Service{ + testService := &services.Service{ Id: 1, Name: "Example Service", Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU", @@ -230,7 +232,7 @@ func (u *email) OnTest() error { Method: "GET", Timeout: 20, LastStatusCode: 200, - Expected: types.NewNullString("test example"), + Expected: null.NewNullString("test example"), LastResponse: "this is an example response", CreatedAt: utils.Now().Add(-24 * time.Hour), } diff --git a/notifiers/line_notify.go b/notifiers/line_notify.go index 61d5f8c2..3a6145e5 100644 --- a/notifiers/line_notify.go +++ b/notifiers/line_notify.go @@ -17,8 +17,9 @@ package notifiers import ( "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "net/url" "strings" @@ -30,17 +31,17 @@ const ( ) type lineNotifier struct { - *notifier.Notification + *notifications.Notification } -var LineNotify = &lineNotifier{¬ifier.Notification{ +var LineNotify = &lineNotifier{¬ifications.Notification{ Method: lineNotifyMethod, Title: "LINE Notify", Description: "LINE Notify will send notifications to your LINE Notify account when services are offline or online. Based on the LINE Notify API.", Author: "Kanin Peanviriyakulkit", AuthorUrl: "https://github.com/dogrocker", Icon: "far fa-bell", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Access Token", Placeholder: "Insert your Line Notify Access Token here.", @@ -58,18 +59,18 @@ func (u *lineNotifier) Send(msg interface{}) error { return err } -func (u *lineNotifier) Select() *notifier.Notification { +func (u *lineNotifier) Select() *notifications.Notification { return u.Notification } // OnFailure will trigger failing service -func (u *lineNotifier) OnFailure(s *types.Service, f *types.Failure) { +func (u *lineNotifier) OnFailure(s *services.Service, f *failures.Failure) { msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnSuccess will trigger successful service -func (u *lineNotifier) OnSuccess(s *types.Service) { +func (u *lineNotifier) OnSuccess(s *services.Service) { if !s.Online || !s.SuccessNotified { var msg string msg = s.DownText diff --git a/notifiers/mobile.go b/notifiers/mobile.go index fc1d5d9e..9557757f 100644 --- a/notifiers/mobile.go +++ b/notifiers/mobile.go @@ -19,8 +19,9 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "time" ) @@ -28,10 +29,10 @@ import ( const mobileIdentifier = "com.statping" type mobilePush struct { - *notifier.Notification + *notifications.Notification } -var Mobile = &mobilePush{¬ifier.Notification{ +var Mobile = &mobilePush{¬ifications.Notification{ Method: "mobile", Title: "Mobile Notifications", Description: `Receive push notifications on your Mobile device using the Statping App. You can scan the Authentication QR Code found in Settings to get the Mobile app setup in seconds. @@ -40,7 +41,7 @@ var Mobile = &mobilePush{¬ifier.Notification{ AuthorUrl: "https://github.com/hunterlong", Delay: time.Duration(5 * time.Second), Icon: "fas fa-mobile-alt", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Device Identifiers", Placeholder: "A list of your Mobile device push notification ID's.", @@ -55,11 +56,11 @@ var Mobile = &mobilePush{¬ifier.Notification{ }}}, } -func (u *mobilePush) Select() *notifier.Notification { +func (u *mobilePush) Select() *notifications.Notification { return u.Notification } -func dataJson(s *types.Service, f *types.Failure) map[string]interface{} { +func dataJson(s *services.Service, f *failures.Failure) map[string]interface{} { serviceId := "0" if s != nil { serviceId = utils.ToString(s.Id) @@ -83,7 +84,7 @@ func dataJson(s *types.Service, f *types.Failure) map[string]interface{} { } // OnFailure will trigger failing service -func (u *mobilePush) OnFailure(s *types.Service, f *types.Failure) { +func (u *mobilePush) OnFailure(s *services.Service, f *failures.Failure) { data := dataJson(s, f) msg := &pushArray{ Message: fmt.Sprintf("Your service '%v' is currently failing! Reason: %v", s.Name, f.Issue), @@ -95,7 +96,7 @@ func (u *mobilePush) OnFailure(s *types.Service, f *types.Failure) { } // OnSuccess will trigger successful service -func (u *mobilePush) OnSuccess(s *types.Service) { +func (u *mobilePush) OnSuccess(s *services.Service) { data := dataJson(s, nil) if !s.Online || !s.SuccessNotified { var msgStr string diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go new file mode 100644 index 00000000..304048da --- /dev/null +++ b/notifiers/notifiers.go @@ -0,0 +1,75 @@ +package notifiers + +import ( + "fmt" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/utils" + "strings" +) + +var ( + allowedVars = []string{"host", "username", "password", "port", "api_key", "api_secret", "var1", "var2"} +) + +func checkNotifierForm(n notifications.Notifier) error { + notifier := n.Select() + for _, f := range notifier.Form { + contains := contains(f.DbField, allowedVars) + if !contains { + return fmt.Errorf("the DbField '%v' is not allowed, allowed vars: %v", f.DbField, allowedVars) + } + } + return nil +} + +func contains(s string, arr []string) bool { + for _, v := range arr { + if strings.ToLower(s) == v { + return true + } + } + return false +} + +// AddNotifier accept a Notifier interface to be added into the array +func AddNotifiers(notifiers ...notifications.Notifier) error { + for _, n := range notifiers { + if err := checkNotifierForm(n); err != nil { + return err + } + notifications.AllCommunications = append(notifications.AllCommunications, n) + if _, err := notifications.Init(n); err != nil { + return err + } + } + startAllNotifiers() + return nil +} + +// startAllNotifiers will start the go routine for each loaded notifier +func startAllNotifiers() { + for _, comm := range notifications.AllCommunications { + if utils.IsType(comm, new(notifications.Notifier)) { + notify := comm.(notifications.Notifier) + if notify.Select().Enabled.Bool { + notify.Select().Close() + notify.Select().Start() + go notifications.Queue(notify) + } + } + } +} + +func AttachNotifiers() error { + return AddNotifiers( + Command, + Discorder, + Emailer, + LineNotify, + Mobile, + Slacker, + Telegram, + Twilio, + Webhook, + ) +} diff --git a/notifiers/notifiers_test.go b/notifiers/notifiers_test.go index b8569076..f3fcd449 100644 --- a/notifiers/notifiers_test.go +++ b/notifiers/notifiers_test.go @@ -32,7 +32,7 @@ var ( currentCount int ) -var TestService = &types.Service{ +var TestService = &services.Service{ Id: 1, Name: "Interpol - All The Rage Back Home", Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU", diff --git a/notifiers/slack.go b/notifiers/slack.go index 9c3094d4..2ac64c04 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -19,8 +19,9 @@ import ( "bytes" "errors" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "strings" "text/template" @@ -35,10 +36,10 @@ const ( ) type slack struct { - *notifier.Notification + *notifications.Notification } -var Slacker = &slack{¬ifier.Notification{ +var Slacker = &slack{¬ifications.Notification{ Method: slackMethod, Title: "slack", Description: "Send notifications to your slack channel when a service is offline. Insert your Incoming webhooker URL for your channel to receive notifications. Based on the slack API.", @@ -47,7 +48,7 @@ var Slacker = &slack{¬ifier.Notification{ Delay: time.Duration(10 * time.Second), Host: "https://webhooksurl.slack.com/***", Icon: "fab fa-slack", - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Incoming webhooker Url", Placeholder: "Insert your slack Webhook URL here.", @@ -69,7 +70,7 @@ func parseSlackMessage(id int64, temp string, data interface{}) error { } type slackMessage struct { - Service *types.Service + Service *services.Service Template string Time int64 Issue string @@ -82,7 +83,7 @@ func (u *slack) Send(msg interface{}) error { return err } -func (u *slack) Select() *notifier.Notification { +func (u *slack) Select() *notifications.Notification { return u.Notification } @@ -95,7 +96,7 @@ func (u *slack) OnTest() error { } // OnFailure will trigger failing service -func (u *slack) OnFailure(s *types.Service, f *types.Failure) { +func (u *slack) OnFailure(s *services.Service, f *failures.Failure) { message := slackMessage{ Service: s, Template: failingTemplate, @@ -106,7 +107,7 @@ func (u *slack) OnFailure(s *types.Service, f *types.Failure) { } // OnSuccess will trigger successful service -func (u *slack) OnSuccess(s *types.Service) { +func (u *slack) OnSuccess(s *services.Service) { if !s.Online { u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) message := slackMessage{ diff --git a/notifiers/telegram.go b/notifiers/telegram.go index c8bc5f5e..f1e1c5c6 100644 --- a/notifiers/telegram.go +++ b/notifiers/telegram.go @@ -19,8 +19,9 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "net/url" "strings" @@ -28,10 +29,10 @@ import ( ) type telegram struct { - *notifier.Notification + *notifications.Notification } -var Telegram = &telegram{¬ifier.Notification{ +var Telegram = &telegram{¬ifications.Notification{ Method: "telegram", Title: "Telegram", Description: "Receive notifications on your Telegram channel when a service has an issue. You must get a Telegram API token from the /botfather. Review the Telegram API Tutorial to learn how to generate a new API Token.", @@ -39,7 +40,7 @@ var Telegram = &telegram{¬ifier.Notification{ AuthorUrl: "https://github.com/hunterlong", Icon: "fab fa-telegram-plane", Delay: time.Duration(5 * time.Second), - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Telegram API Token", Placeholder: "383810182:EEx829dtCeufeQYXG7CUdiQopqdmmxBPO7-s", @@ -56,7 +57,7 @@ var Telegram = &telegram{¬ifier.Notification{ }}}, } -func (u *telegram) Select() *notifier.Notification { +func (u *telegram) Select() *notifications.Notification { return u.Notification } @@ -82,13 +83,13 @@ func (u *telegram) Send(msg interface{}) error { } // OnFailure will trigger failing service -func (u *telegram) OnFailure(s *types.Service, f *types.Failure) { +func (u *telegram) OnFailure(s *services.Service, f *failures.Failure) { msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnSuccess will trigger successful service -func (u *telegram) OnSuccess(s *types.Service) { +func (u *telegram) OnSuccess(s *services.Service) { if !s.Online || !s.SuccessNotified { u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) var msg interface{} diff --git a/notifiers/twilio.go b/notifiers/twilio.go index 6c6efc62..a472eb4a 100644 --- a/notifiers/twilio.go +++ b/notifiers/twilio.go @@ -19,8 +19,9 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "net/url" "strings" @@ -28,10 +29,10 @@ import ( ) type twilio struct { - *notifier.Notification + *notifications.Notification } -var Twilio = &twilio{¬ifier.Notification{ +var Twilio = &twilio{¬ifications.Notification{ Method: "twilio", Title: "Twilio", Description: "Receive SMS text messages directly to your cellphone when a service is offline. You can use a Twilio test account with limits. This notifier uses the Twilio API.", @@ -39,7 +40,7 @@ var Twilio = &twilio{¬ifier.Notification{ AuthorUrl: "https://github.com/hunterlong", Icon: "far fa-comment-alt", Delay: time.Duration(10 * time.Second), - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "Account SID", Placeholder: "Insert your Twilio Account SID", @@ -66,7 +67,7 @@ var Twilio = &twilio{¬ifier.Notification{ }}}, } -func (u *twilio) Select() *notifier.Notification { +func (u *twilio) Select() *notifications.Notification { return u.Notification } @@ -92,13 +93,13 @@ func (u *twilio) Send(msg interface{}) error { } // OnFailure will trigger failing service -func (u *twilio) OnFailure(s *types.Service, f *types.Failure) { +func (u *twilio) OnFailure(s *services.Service, f *failures.Failure) { msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) u.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnSuccess will trigger successful service -func (u *twilio) OnSuccess(s *types.Service) { +func (u *twilio) OnSuccess(s *services.Service) { if !s.Online || !s.SuccessNotified { u.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) var msg string diff --git a/notifiers/webhook.go b/notifiers/webhook.go index 83cd4d94..54a82b7e 100644 --- a/notifiers/webhook.go +++ b/notifiers/webhook.go @@ -18,8 +18,9 @@ package notifiers import ( "bytes" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "io/ioutil" "net/http" @@ -32,10 +33,10 @@ const ( ) type webhooker struct { - *notifier.Notification + *notifications.Notification } -var Webhook = &webhooker{¬ifier.Notification{ +var Webhook = &webhooker{¬ifications.Notification{ Method: webhookMethod, Title: "HTTP webhooker", Description: "Send a custom HTTP request to a specific URL with your own body, headers, and parameters.", @@ -43,7 +44,7 @@ var Webhook = &webhooker{¬ifier.Notification{ AuthorUrl: "https://github.com/hunterlong", Icon: "fas fa-code-branch", Delay: time.Duration(1 * time.Second), - Form: []notifier.NotificationForm{{ + Form: []notifications.NotificationForm{{ Type: "text", Title: "HTTP Endpoint", Placeholder: "http://webhookurl.com/JW2MCP4SKQP", @@ -87,11 +88,11 @@ func (w *webhooker) Send(msg interface{}) error { return err } -func (w *webhooker) Select() *notifier.Notification { +func (w *webhooker) Select() *notifications.Notification { return w.Notification } -func replaceBodyText(body string, s *types.Service, f *types.Failure) string { +func replaceBodyText(body string, s *services.Service, f *failures.Failure) string { body = utils.ConvertInterface(body, s) body = utils.ConvertInterface(body, f) return body @@ -129,7 +130,7 @@ func (w *webhooker) sendHttpWebhook(body string) (*http.Response, error) { } func (w *webhooker) OnTest() error { - body := replaceBodyText(w.Var2, notifier.ExampleService, nil) + body := replaceBodyText(w.Var2, notifications.ExampleService, nil) resp, err := w.sendHttpWebhook(body) if err != nil { return err @@ -141,13 +142,13 @@ func (w *webhooker) OnTest() error { } // OnFailure will trigger failing service -func (w *webhooker) OnFailure(s *types.Service, f *types.Failure) { +func (w *webhooker) OnFailure(s *services.Service, f *failures.Failure) { msg := replaceBodyText(w.Var2, s, f) w.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnSuccess will trigger successful service -func (w *webhooker) OnSuccess(s *types.Service) { +func (w *webhooker) OnSuccess(s *services.Service) { if !s.Online { w.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) msg := replaceBodyText(w.Var2, s, nil) diff --git a/source/wiki.go b/source/wiki.go index 15f58b44..4cc633e3 100644 --- a/source/wiki.go +++ b/source/wiki.go @@ -6,4 +6,4 @@ package source // CompiledWiki contains all of the Statping Wiki pages from the Github Wiki repo. -var CompiledWiki = []byte("Types of Monitoring
Features
Start Statping
Linux
Mac
Windows
AWS EC2
Docker
Mobile App
Heroku
API
Makefile
Notifiers
Notifier Events
Notifier Example
Prometheus Exporter
SSL
Config with .env File
Static Export
Statping Plugins
Statuper
Build and Test
Contributing
PGP Signature
Testing
Deployment
\n\n

Types of Monitoring

\nYou can monitor your application by using a simple HTTP GET to the endpoint to return back a response and status code. Normally you want a 200 status code on an HTTP request. You might want to require a 404 or 500 error as a response code though. With each service you can include a Timeout in seconds to work with your long running services.\n\n# HTTP Endpoints with Custom POST\nFor more advanced monitoring you can add a data as a HTTP POST request. This is useful for automatically submitting JSON, or making sure your signup form is working correctly.\n\n

\n\n

\n\nWith a HTTP service, you can POST a JSON string to your endpoint to retrieve any type of response back. You can then use Regex in the Expected Response field to parse a custom response that exactly matches your status requirements. \n\n# TCP/UDP Services\nFor other services that don't use HTTP, you can monitor any type of service by using the PORT of the service. If you're Ethereum Blockchain server is running on 8545, you can use TCP to monitor your server. With a TCP service, you can monitor your Docker containers, or remove service running on a custom port. You don't need to include `http` in the endpoint field, just IP or Hostname.\n\n

\n\n

\n\n# ICMP Service\nYou can send a [ICMP](https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol) (ping) to an endpoint rather than HTTP/TCP/UDP request for a quick response.\n\n\n

Features

\nStatping is a great Status Page that can be deployed with 0 effort.\n\n# 3 Different Databases\nYou can use MySQL, Postgres, or SQLite as a database for your Statping status page. The server will automatically upgrade your database tables depending on which database you have.\n\n# Easy to Startup\nStatping is an extremely easy to setup website monitoring tool without fussing with dependencies or packages. Simply download and install the precompile binary for your operating system. Statping works on Windows, Mac, Linux, Docker, and even the Raspberry Pi.\n\n# Plugins\nStatping is an awesome Status Page generator that allows you to create your own plugins with Golang Plugins! You don't need to request a PR or even tell us about your plugin. Plugin's are compiled and then send as a binary to the Statping `/plugins` folder. Test your plugins using the `statup test plugin` command, checkout the [Plugin Wiki](https://github.com/hunterlong/statping/wiki/Statping-Plugins) to see detailed information about creating plugins.\n\n# No Maintenance\nMany other website monitoring applications will collect data until the server fails because of hard drive is 100% full. Statping will automatically delete records to make sure your server will stay UP for years. The EC2 AMI Image is a great way to host your status page without worrying about it crashing one day. Statping will automatically upgrade its software when you reboot your computer.\n\n# Email & Slack Notifications\nReceive email notifications if your website or application goes offline. Statping includes SMTP connections so you can use AWS SES, or any other SMTP emailing service. Go in the Email Settings in Settings to configure these options.\n\n# Prometheus Exporter\nIf you want a deeper view of your applications status, you can use Grafana and Prometheus to graph all types of data about your services. Read more about the [Prometheus Exporter](https://github.com/hunterlong/statping/wiki/Prometheus-Exporter)\n\n

Start Statping

\n\n\n

Linux

\n# Installing on Linux\nInstalling Statping on Linux can be done by downloading the latest tar.gz file, unzipping, and running the executable. You can also install using [Snapcraft](https://snapcraft.io/) for Ubuntu systems.\n\n```shell\ncurl -o- -L https://statping.com/install.sh | bash\n```\n\n## Install using Snapcraft\n\n[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/statping)\n\nIf you are using [snap](https://snapcraft.io/statping), you can simply run this command to install Statping.\n```shell\nsudo snap install statping\n```\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Apt: `apt install ruby-sass -y`\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n## Systemd Service\nSetting up a systemd service is a great way to make sure your Statping server will automatically reboot when needed. You can use the file below for your service. You should have Statping already installed by this step.\n###### /etc/systemd/system/statping.service\n```\n[Unit]\nDescription=Statping Server\nAfter=network.target\nAfter=systemd-user-sessions.service\nAfter=network-online.target\n\n[Service]\nType=simple\nRestart=always\nExecStart=/usr/local/bin/statping\nWorkingDirectory=/usr/local/bin\n\n[Install]\nWantedBy=multi-user.target\n```\nThen you can enable and start your systemd service with:\n```\nsystemctl daemon-reload\n\nsystemctl enable statping.service\n\nsystemctl start statping\n```\nYou're Statping server will now automatically restart when your server restarts.\n\n## Raspberry Pi\nYou can even run Statping on your Raspberry Pi by installing the precompiled binary from [Latest Releases](https://github.com/hunterlong/statping/releases/latest). For the Raspberry Pi 3 you'll want to download the `statping-linux-arm7.tar.gz` file. Be sure to change `VERSION` to the latest version in Releases, and include the 'v'.\n\n```\nVERSION=$(curl -s \"https://github.com/hunterlong/statping/releases/latest\" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}')\nwget https://github.com/hunterlong/statping/releases/download/$VERSION/statping-linux-arm7.tar.gz\ntar -xvzf statping-linux-arm7.tar.gz\nchmod +x statping\nmv statping /usr/local/bin/statping\n\nstatping version\n``` \n\n## Alpine Linux\nThe Docker image is using the Statping Alpine binary since it's so incredibly small. You can run it on your own alpine image by downloading `statping-linux-alpine.tar.gz` from [Latest Releases](https://github.com/hunterlong/statping/releases/latest).\n\n

Mac

\n# Installing on Mac\nStatping includes an easy to use [Homebrew Formula](https://github.com/hunterlong/homebrew-statping) to quick get your Status Page up and running locally. Statping on brew is automatically generated for each new release to master. Install with the commands below,\n###### Using Homebrew\n```bash\nbrew tap hunterlong/statping\nbrew install statping\n```\n###### Using the Terminal\n```shell\ncurl -o- -L https://statping.com/install.sh | bash\n```\n\n

\n\n

\n\nOnce you've installed it, checkout which version you have by running `statping version`.\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n\n

Windows

\n# Installing on Windows\nCurrently, Statping only works on Windows 64-bit computers. Just download the exe file from [Latest Releases](https://github.com/hunterlong/statping/releases/latest) and run it in your command prompt. It will create a HTTP server on port 8080, so you can visit `http://localhost:8080` to see your Statping Status Page.\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n# Running Statping as a Service\nTo ensure Statping is always running, it can be installed to run as a service on a Windows machine. The easiest way to do that is by using NSSM, the [Non-Sucking Service Manager](https://nssm.cc/download). Download and unzip the compressed file to a location on your machine running Statping to get started:\n1. Open an administrative command prompt.\n2. Change to the folder that contains the 64 bit version of NSSM.\n3. Type \"nssm install Statping\" and press enter.\n4. For the properties, use the following as an example:\n Path: C:\\Program Files\\Statping\\statping.exe\n Startup directory: C:\\Program Files\\Statping\n5. Click \"Install\".\n6. Launch the windows services manager.\n7. Run Statping.\n\n## Known Issues with Windows\nUnfortunately, Statping only works on Windows 64-bit processors. If you have more than 4gb of ram, there's a good chance you already have a 64-bit processor. Download the [Latest Releases](https://github.com/hunterlong/statping/releases/latest) of Statping, extract the ZIP file, then double click on the `statping.exe` file. You can use a SQLite database for a quick setup, or connect to a local/remote Postgres or MySQL database server.\n\n

AWS EC2

\nRunning Statping on the smallest EC2 server is very quick using the AWS AMI Image. The AWS AMI Image will automatically start a Statping Docker container that will automatically update to the latest version. Once the EC2 is booted, you can go to the Public DNS domain to view the Statping installation page. The Statping root folder is located at: `/statping` on the server.\n\n# AMI Image\nChoose the correct AMI Image ID based on your AWS region.\n- us-east-1 `ami-09ccd23d9c7afba61` (Virginia)\n- us-east-2 `ami-0c6c9b714a501cdb3` (Ohio)\n- us-west-1 `ami-02159cc1fc701a77e` (California)\n- us-west-2 `ami-007c6990949f5ccee` (Oregon)\n- eu-central-1 `ami-06e252d6d8b0c2f1f` (Frankfurt)\n\n# Upgrading Staping\nYou can upgrade the Statping executable by running the commands below on your EC2 instance.\n```\nVERSION=$(curl -s \"https://github.com/hunterlong/statping/releases/latest\" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}')\nwget https://github.com/hunterlong/statping/releases/download/$VERSION/statping-linux-x64.tar.gz\ntar -xvzf statping-linux-x64.tar.gz\nchmod +x statping\nmv statping /usr/local/bin/statping\n```\nYou can test the version number by running `statping version`.\n\n# Instructions\n\n### 1. Create an EC2 instance from AMI Image\nGo to the main EC2 dashboard and click 'Launch Instance'. Then type `Statping` inside the search field for 'Community AMI'. Once you've found it in your region, click Select!\n\n\n\n### 2. Get the Public DNS for EC2 Instance\nCopy the 'Public DNS' URL and paste it into your browser.\n\n\n\n### 3. Setup Statping\nUse SQLite if you don't want to connect to a remote MySQL or Postgres database.\n\n\n\n# EC2 Server Features\nRunning your Statping server on a small EC2 instance is perfect for most users. Below you'll find some commands to get up and running in seconds.\n- Super cheap on the t2.nano (~$4.60 monthly)\n- Small usage, 8gb of hard drive\n- Automatic SSL certificate if you require it\n- Automatic reboot when the server needs it\n- Automatic database cleanup, so you'll never be at 100% full.\n- Automatic docker containers/images removal\n\n## Create Security Groups\nUsing the AWS CLI you can copy and paste the commands below to auto create everything for you. The server opens port 80 and 443.\n```bash\naws ec2 create-security-group --group-name StatpingPublicHTTP --description \"Statping HTTP Server on port 80 and 443\"\n# will response back a Group ID. Copy ID and use it for --group-id below.\n```\n```bash\nGROUPS=sg-7e8b830f\naws ec2 authorize-security-group-ingress --group-id $GROUPS --protocol tcp --port 80 --cidr 0.0.0.0/0\naws ec2 authorize-security-group-ingress --group-id $GROUPS --protocol tcp --port 443 --cidr 0.0.0.0/0\n```\n## Create EC2 without SSL\nOnce your server has started, go to the EC2 Public DNS endpoint. You should be redirected to /setup to continue your installation process! The database information is already inputed for you.\n```bash\nGROUPS=sg-7e8b830f\nKEY=MYKEYHERE\nAMI_IMAGE=ami-7be8a103\n\naws ec2 run-instances \\\n --image-id $AMI_IMAGE \\\n --count 1 --instance-type t2.nano \\\n --key-name $KEY \\\n --security-group-ids $GROUPS\n```\n## Create EC2 with Automatic SSL Certification\nStart a Statping server with an SSL cert that will automatically regenerate when it's near expiration time. You'll need to point your domain's A record (IP address) or CNAME (public DNS endpoint) to use this feature.\n\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/dev/ec2-ssl.sh\n```\n\n```bash\n# edit the contents inside of ec2-ssl.sh then continue\nLETSENCRYPT_HOST=\"status.MYDOMAIN.com\"\nLETSENCRYPT_EMAIL=\"noreply@MYEMAIL.com\"\n```\nEdit ec2-ssl.sh and insert your domain you want to use, then run command below. Use the Security Group ID that you used above for --security-group-ids\n```\nGROUPS=sg-7e8b830f\nAMI_IMAGE=ami-7be8a103\nKEY=MYKEYHERE\n\naws ec2 run-instances \\\n --user-data file://ec2-ssl.sh \\\n --image-id $AMI_IMAGE \\\n --count 1 --instance-type t2.nano \\\n --key-name $KEY \\\n --security-group-ids $GROUPS\n```\n\n### EC2 Server Specs\n- t2.nano ($4.60 monthly)\n- 8gb SSD Memory\n- 0.5gb RAM\n- Docker with Docker Compose installed\n- Running Statping, NGINX, and Postgres\n- boot scripts to automatically clean unused containers.\n\n\n\n

Docker

\nStatping is easily ran on Docker with the light weight Alpine linux image. View on [Docker Hub](https://hub.docker.com/r/hunterlong/statping).\n\n[![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/)\n\n# Latest Docker Image\nThe `latest` Docker image uses Alpine Linux to keep it ultra small.\n```bash\ndocker run -d \\\n -p 8080:8080 \\\n --restart always \\\n hunterlong/statping\n```\n\n# Mounting Volume\nYou can mount a volume to the `/app` Statping directory. This folder will contain `logs`, `config.yml`, and static assets if you want to edit the SCSS/CSS. \n```bash\ndocker run -d \\\n -p 8080:8080 \\\n -v /mydir/statping:/app \\\n --restart always \\\n hunterlong/statping\n```\n\n# Attach a SSL Certificate\nWhen you mount `server.crt` and `server.key` to the `/app` directory, Statping will run a HTTPS server on port 443. Checkout the [SSL Wiki](https://github.com/hunterlong/statping/wiki/SSL) documentation to see more information about this.\n```bash\ndocker run -d \\\n -p 443:443 \\\n -v /mydir/domain.crt:/app/server.crt \\\n -v /mydir/domain.key:/app/server.key \\\n -v /mydir:/app \\\n --restart always \\\n hunterlong/statping\n```\n\n# Development Docker Image\nIf you want to run Statping that was build from the source, use the `dev` Docker image.\n```bash\ndocker run -d -p 8080:8080 hunterlong/statping:dev\n```\n\n# Cypress Testing Docker Image\nThis Docker image will pull the latest version of Statping and test the web interface with [Cypress](https://www.cypress.io/).\n```bash\ndocker run -it -p 8080:8080 hunterlong/statping:cypress\n```\n\n#### Or use Docker Compose\nThis Docker Compose file inlcudes NGINX, Postgres, and Statping.\n\n### Docker Compose with NGINX and Postgres\nOnce you initiate the `docker-compose.yml` file below go to http://localhost and you'll be forwarded to the /setup page. \nDatabase Authentication\n- database: `postgres`\n- port: `5432`\n- username: `statup`\n- password: `password123`\n- database: `statup`\n\n```yaml\nversion: '2.3'\n\nservices:\n\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs:ro\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html:ro\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: localhost\n\n statup:\n container_name: statup\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n - database\n depends_on:\n - postgres\n volumes:\n - ./statup/app:/app\n environment:\n VIRTUAL_HOST: localhost\n VIRTUAL_PORT: 8080\n DB_CONN: postgres\n DB_HOST: postgres\n DB_USER: statup\n DB_PASS: password123\n DB_DATABASE: statup\n NAME: EC2 Example\n DESCRIPTION: This is a Statping Docker Compose instance\n\n postgres:\n container_name: postgres\n image: postgres:10\n restart: always\n networks:\n - database\n volumes:\n - ./statup/postgres:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD: password123\n POSTGRES_USER: statup\n POSTGRES_DB: statup\n\nnetworks:\n internet:\n driver: bridge\n database:\n driver: bridge\n```\nOr a simple wget...\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/servers/docker-compose.yml\ndocker-compose up -d\n```\n\n### Docker Compose with Automatic SSL\nYou can automatically start a Statping server with automatic SSL encryption using this docker-compose file. First point your domain's DNS to the Statping server, and then run this docker-compose command with DOMAIN and EMAIL. Email is for letsencrypt services.\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/servers/docker-compose-ssl.yml\n\nLETSENCRYPT_HOST=mydomain.com \\\n LETSENCRYPT_EMAIL=info@mydomain.com \\\n docker-compose -f docker-compose-ssl.yml up -d\n```\n\n#### Full docker-compose with Automatic SSL\n\n```yaml\nversion: '2.3'\n\nservices:\n\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n labels:\n - \"com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy\"\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs:ro\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html:ro\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: ${LETSENCRYPT_HOST}\n\n letsencrypt:\n container_name: letsencrypt\n image: jrcs/letsencrypt-nginx-proxy-companion\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n\n statup:\n container_name: statup\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n - database\n depends_on:\n - postgres\n volumes:\n - ./statup/app:/app\n environment:\n VIRTUAL_HOST: ${LETSENCRYPT_HOST}\n VIRTUAL_PORT: 8080\n LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}\n LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}\n DB_CONN: postgres\n DB_HOST: postgres\n DB_USER: statup\n DB_PASS: password123\n DB_DATABASE: statup\n NAME: SSL Example\n DESCRIPTION: This Status Status Page should be running ${LETSENCRYPT_HOST} with SSL.\n\n postgres:\n container_name: postgres\n image: postgres:10\n restart: always\n networks:\n - database\n volumes:\n - ./statup/postgres:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD: password123\n POSTGRES_USER: statup\n POSTGRES_DB: statup\n\nnetworks:\n internet:\n driver: bridge\n database:\n driver: bridge\n```\n\n

Mobile App

\nStatping has a free mobile app so you can monitor your websites and applications without the need of a computer. \n\n![iTunes App Store](https://img.shields.io/itunes/v/1445513219.svg)\n\n

\n\n\n

\n\n

\n\n

\n\n\n

Heroku

\nYou can now instantly deploy your Statping instance on a free Heroku container. Simply click the deploy button below and get up in running within seconds. This Heroku deployment is based on the Statping Docker image so you will have all the great features including SASS and all the notifiers without any setup. \n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hunterlong/statping/tree/master)\n\nView the live Heroku Statping instance at: [https://statping.herokuapp.com](https://statping.herokuapp.com)\n\n# Database Configuration\nYou will need to deploy a Postgres database to your instance and insert some configuration variables. View the image below to see what environment variable you need to configure. If you insert `DB_CONN`, Statping will attempt to automatically connect to the database without the need for the `config.yml` file. \n\n![](https://img.cjx.io/herokustatping.png)\n\n\n

API

\nStatping includes a RESTFUL API so you can view, update, and edit your services with easy to use routes. You can currently view, update and delete services, view, create, update users, and get detailed information about the Statping instance. To make life easy, try out a Postman or Swagger JSON file and use it on your Statping Server.\n\n

\nPostman | Postman JSON Export | Swagger Export\n

\n\n## Authentication\nAuthentication uses the Statping API Secret to accept remote requests. You can find the API Secret in the Settings page of your Statping server. To send requests to your Statping API, include a Authorization Header when you send the request. The API will accept any one of the headers below.\n\n- HTTP Header: `Authorization: API SECRET HERE`\n- HTTP Header: `Authorization: Bearer API SECRET HERE`\n\n## Main Route `/api`\nThe main API route will show you all services and failures along with them.\n\n## Services\nThe services API endpoint will show you detailed information about services and will allow you to edit/delete services with POST/DELETE http methods.\n\n### Viewing All Services\n- Endpoint: `/api/services`\n- Method: `GET`\n- Response: Array of [Services](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Viewing Service\n- Endpoint: `/api/services/{id}`\n- Method: `GET`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Updating Service\n- Endpoint: `/api/services/{id}`\n- Method: `POST`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"name\": \"Updated Service\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 15,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0\n}\n```\n\n### Create New Service\n- Endpoint: `/api/services`\n- Method: `POST`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"name\": \"Create New Service\",\n \"domain\": \"https://www.coogger.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 15,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0\n}\n```\n\n### Deleting Service\n- Endpoint: `/api/services/{id}`\n- Method: `DELETE`\n- Response: [Object Response](https://github.com/hunterlong/statping/wiki/API#object-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nResponse:\n```json\n{\n \"status\": \"success\",\n \"id\": 4,\n \"type\": \"service\",\n \"method\": \"delete\"\n}\n```\n\n## Users\nThe users API endpoint will show you users that are registered inside your Statping instance.\n\n### View All Users\n- Endpoint: `/api/users`\n- Method: `GET`\n- Response: Array of [Users](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Viewing User\n- Endpoint: `/api/users/{id}`\n- Method: `GET`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Creating New User\n- Endpoint: `/api/users`\n- Method: `POST`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"username\": \"newadmin\",\n \"email\": \"info@email.com\",\n \"password\": \"password123\",\n \"admin\": true\n}\n```\n\n### Updating User\n- Endpoint: `/api/users/{id}`\n- Method: `POST`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"username\": \"updatedadmin\",\n \"email\": \"info@email.com\",\n \"password\": \"password123\",\n \"admin\": true\n}\n```\n\n### Deleting User\n- Endpoint: `/api/services/{id}`\n- Method: `DELETE`\n- Response: [Object Response](https://github.com/hunterlong/statping/wiki/API#object-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nResponse:\n```json\n{\n \"status\": \"success\",\n \"id\": 3,\n \"type\": \"user\",\n \"method\": \"delete\"\n}\n```\n\n# Service Response\n```json\n{\n \"id\": 8,\n \"name\": \"Test Service 0\",\n \"domain\": \"https://status.coinapp.io\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 1,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 30,\n \"order_id\": 0,\n \"created_at\": \"2018-09-12T09:07:03.045832088-07:00\",\n \"updated_at\": \"2018-09-12T09:07:03.046114305-07:00\",\n \"online\": false,\n \"latency\": 0.031411064,\n \"24_hours_online\": 0,\n \"avg_response\": \"\",\n \"status_code\": 502,\n \"last_online\": \"0001-01-01T00:00:00Z\",\n \"dns_lookup_time\": 0.001727175,\n \"failures\": [\n {\n \"id\": 5187,\n \"issue\": \"HTTP Status Code 502 did not match 200\",\n \"created_at\": \"2018-09-12T10:41:46.292277471-07:00\"\n },\n {\n \"id\": 5188,\n \"issue\": \"HTTP Status Code 502 did not match 200\",\n \"created_at\": \"2018-09-12T10:41:47.337659862-07:00\"\n }\n ]\n}\n```\n\n# User Response\n```json\n{\n \"id\": 1,\n \"username\": \"admin\",\n \"api_key\": \"02f324450a631980121e8fd6ea7dfe4a7c685a2f\",\n \"admin\": true,\n \"created_at\": \"2018-09-12T09:06:53.906398511-07:00\",\n \"updated_at\": \"2018-09-12T09:06:54.972440207-07:00\"\n}\n```\n\n# Object Response\n```json\n{\n \"type\": \"service\",\n \"id\": 19,\n \"method\": \"delete\",\n \"status\": \"success\"\n}\n```\n\n# Main API Response\n```json\n{\n \"name\": \"Awesome Status\",\n \"description\": \"An awesome status page by Statping\",\n \"footer\": \"This is my custom footer\",\n \"domain\": \"https://demo.statping.com\",\n \"version\": \"v0.56\",\n \"migration_id\": 1536768413,\n \"created_at\": \"2018-09-12T09:06:53.905374829-07:00\",\n \"updated_at\": \"2018-09-12T09:07:01.654201225-07:00\",\n \"database\": \"sqlite\",\n \"started_on\": \"2018-09-12T10:43:07.760729349-07:00\",\n \"services\": [\n {\n \"id\": 1,\n \"name\": \"Google\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 10,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0,\n \"created_at\": \"2018-09-12T09:06:54.97549122-07:00\",\n \"updated_at\": \"2018-09-12T09:06:54.975624103-07:00\",\n \"online\": true,\n \"latency\": 0.09080986,\n \"24_hours_online\": 0,\n \"avg_response\": \"\",\n \"status_code\": 200,\n \"last_online\": \"2018-09-12T10:44:07.931990439-07:00\",\n \"dns_lookup_time\": 0.005543935\n }\n ]\n}\n```\n\n\n

Makefile

\nHere's a simple list of Makefile commands you can run using `make`. The [Makefile](https://github.com/hunterlong/statping/blob/master/Makefile) may change often, so i'll try to keep this Wiki up-to-date.\n\n- Ubuntu `apt-get install build-essential`\n- MacOSX `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer`\n- Windows [Install Guide for GNU make utility](http://gnuwin32.sourceforge.net/packages/make.htm)\n- CentOS/RedHat `yum groupinstall \"Development Tools\"`\n\n### Commands\n```bash\nmake build # build the binary\nmake install\nmake run\nmake test\nmake coverage\nmake docs\n# Building Statping\nmake build-all\nmake build-alpine\nmake docker\nmake docker-run\nmake docker-dev\nmake docker-run-dev\nmake databases\nmake dev-deps\nmake clean\nmake compress\nmake cypress-install\nmake cypress-test\n```\n\n

Notifiers

\n

\n\n

\n\nStatping includes multiple Notifiers to alert you when your services are offline. You can also create your own notifier and send a Push Request to this repo! Creating a custom notifier is pretty easy as long as you follow the requirements. A notifier will automatically be installed into the users Statping database, and form values will save without any hassles. 💃\n\n

\nExample Code | Events | View Notifiers
\n\n

\n\n## Notifier Requirements\n- Must have a unique `METHOD` name\n- Struct must have `*notifier.Notification` pointer in it. \n- Must create and add your notifier variable in `init()`\n- Should have a form for user to input their variables/keys. `Form: []notifier.NotificationForm`\n\n## Notifier Interface (required)\nStatping has the `Notifier` interface which you'll need to include in your notifier. Statping includes many other events/triggers for your notifier, checkout Notifier Events to see all of them.\n```go\n// Notifier interface is required to create a new Notifier\ntype Notifier interface {\n\tOnSave() error // OnSave is triggered when the notifier is saved\n\tSend(interface{}) error // OnSave is triggered when the notifier is saved\n\tSelect() *Notification // Select returns the *Notification for a notifier\n}\n```\n\n### Basic Interface (required)\nInclude `OnSuccess` and `OnFailure` to receive events when a service is online or offline.\n```go\n// BasicEvents includes the most minimal events, failing and successful service triggers\ntype BasicEvents interface {\n\t// OnSuccess is triggered when a service is successful\n\tOnSuccess(*types.Service)\n\t// OnFailure is triggered when a service is failing\n\tOnFailure(*types.Service, *types.Failure)\n}\n```\n\n### Test Interface\nThe OnTest method will give the front end user the ability to test your notifier without saving, the OnTest method for your notifier run the functionality to test the user's submitted parameters and respond an error if notifier is not correctly setup.\n```go\n// Tester interface will include a function to Test users settings before saving\ntype Tester interface {\n\tOnTest() error\n}\n```\nIf your notifier includes this interface, the Test button will appear.\n\n## Notifier Struct\n```go\nvar example = &Example{¬ifier.Notification{\n\tMethod: \"example\", // unique method name\n\tHost: \"http://exmaplehost.com\", // default 'host' field\n\tForm: []notifier.NotificationForm{{\n\t\tType: \"text\", // text, password, number, or email\n\t\tTitle: \"Host\", // The title of value in form\n\t\tPlaceholder: \"Insert your Host here.\", // Optional placeholder in input\n\t\tDbField: \"host\", // An accepted DbField value (read below)\n\t}},\n}\n```\n\n## Notifier Form\nInclude a form with your notifier so other users can save API keys, username, passwords, and other values. \n```go\n// NotificationForm contains the HTML fields for each variable/input you want the notifier to accept.\ntype NotificationForm struct {\n\tType string `json:\"type\"` // the html input type (text, password, email)\n\tTitle string `json:\"title\"` // include a title for ease of use\n\tPlaceholder string `json:\"placeholder\"` // add a placeholder for the input\n\tDbField string `json:\"field\"` // true variable key for input\n\tSmallText string `json:\"small_text\"` // insert small text under a html input\n\tRequired bool `json:\"required\"` // require this input on the html form\n\tIsHidden bool `json:\"hidden\"` // hide this form element from end user\n\tIsList bool `json:\"list\"` // make this form element a comma separated list\n\tIsSwitch bool `json:\"switch\"` // make the notifier a boolean true/false switch\n}\n```\n\n### Example Notifier Form\nThis is the Slack Notifier `Form` fields.\n```go\nForm: []notifier.NotificationForm{{\n\t\tType: \"text\",\n\t\tTitle: \"Incoming webhooker Url\",\n\t\tPlaceholder: \"Insert your slack webhook URL here.\",\n\t\tSmallText: \"Incoming webhooker URL from slack Apps\",\n\t\tDbField: \"Host\",\n\t\tRequired: true,\n\t}}\n}\n```\n\n### Accepted DbField Values\nThe `notifier.NotificationForm` has a field called `DbField` which is the column to save the value into the database. Below are the acceptable DbField string names to include in your form. \n- `host` used for a URL or API endpoint\n- `username` used for a username\n- `password` used for a password\n- `port` used for a integer port number\n- `api_key` used for some kind of API key\n- `api_secret` used for some API secret\n- `var1` used for any type of string\n- `var2` used for any type of string (extra)\n\n### Form Elements\nYou can completely custom your notifications to include a detailed form. \n- `Type` is a HTML input type for your field\n- `Title` give your input element a title\n- `Placeholder` optional field if you want a placeholder in input\n- `DbField` required field to save variable into database (read above)\n- `Placeholder` optional field for inserting small hint under the input\n\n# Adding Notifiers\nTo add a notifier to the Statping application, simply append your Notifier in the `AttachNotifiers()` function inside of [core/core.go](https://github.com/hunterlong/statping/blob/master/core/core.go).\n\n```go\n// AttachNotifiers will attach all the notifier's into the system\nfunc AttachNotifiers() error {\n\treturn notifier.AddNotifiers(\n\t\tnotifiers.Command,\n\t\tnotifiers.Discorder,\n\t\tnotifiers.Emailer,\n\t\tnotifiers.LineNotify,\n\t\tnotifiers.Mobile,\n\t\tnotifiers.Slacker,\n\t\tnotifiers.Telegram,\n\t\tnotifiers.Twilio,\n\t\tnotifiers.Webhook,\n\t)\n}\n```\n###### [AttachNotifiers](https://github.com/hunterlong/statping/blob/master/core/core.go#L183)\n\n

Notifier Events

\nEvents are handled by added interfaces for the elements you want to monitor.\n\n## Required Notifier Interface\n```go\n// Notifier interface is required to create a new Notifier\ntype Notifier interface {\n\t// Run will trigger inside of the notifier when enabled\n\tRun() error\n\t// OnSave is triggered when the notifier is saved\n\tOnSave() error\n\t// Test will run a function inside the notifier to Test if it works\n\tTest() error\n\t// Select returns the *Notification for a notifier\n\tSelect() *Notification\n}\n```\n\n## Basic Success/Failure Interface\n```go\n// BasicEvents includes the most minimal events, failing and successful service triggers\ntype BasicEvents interface {\n\t// OnSuccess is triggered when a service is successful\n\tOnSuccess(*types.Service)\n\t// OnFailure is triggered when a service is failing\n\tOnFailure(*types.Service, *types.Failure)\n}\n```\n\n\n## Service Events\n```go\n// ServiceEvents are events for Services\ntype ServiceEvents interface {\n\tOnNewService(*types.Service)\n\tOnUpdatedService(*types.Service)\n\tOnDeletedService(*types.Service)\n}\n```\n\n## User Events\n```go\n// UserEvents are events for Users\ntype UserEvents interface {\n\tOnNewUser(*types.User)\n\tOnUpdatedUser(*types.User)\n\tOnDeletedUser(*types.User)\n}\n```\n\n## Core Events\n```go\n// CoreEvents are events for the main Core app\ntype CoreEvents interface {\n\tOnUpdatedCore(*types.Core)\n}\n```\n\n## Notifier Events\n```go\n// NotifierEvents are events for other Notifiers\ntype NotifierEvents interface {\n\tOnNewNotifier(*Notification)\n\tOnUpdatedNotifier(*Notification)\n}\n```\n\n

Notifier Example

\nBelow is a full example of a Statping notifier which will give you a good example of how to create your own. Insert your new notifier inside the `/notifiers` folder once your ready!\n\n```go\npackage notifiers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/hunterlong/statping/types\"\n \"github.com/hunterlong/statping/core/notifier\"\n\t\"time\"\n)\n\ntype Example struct {\n\t*notifier.Notification\n}\n\nvar example = &Example{¬ifier.Notification{\n\tMethod: METHOD,\n\tTitle: \"Example\",\n\tDescription: \"Example Notifier\",\n\tAuthor: \"Hunter Long\",\n\tAuthorUrl: \"https://github.com/hunterlong\",\n\tDelay: time.Duration(5 * time.Second),\n\tForm: []notifier.NotificationForm{{\n\t\tType: \"text\",\n\t\tTitle: \"Host\",\n\t\tPlaceholder: \"Insert your Host here.\",\n\t\tDbField: \"host\",\n\t\tSmallText: \"this is where you would put the host\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Username\",\n\t\tPlaceholder: \"Insert your Username here.\",\n\t\tDbField: \"username\",\n\t}, {\n\t\tType: \"password\",\n\t\tTitle: \"Password\",\n\t\tPlaceholder: \"Insert your Password here.\",\n\t\tDbField: \"password\",\n\t}, {\n\t\tType: \"number\",\n\t\tTitle: \"Port\",\n\t\tPlaceholder: \"Insert your Port here.\",\n\t\tDbField: \"port\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"API Key\",\n\t\tPlaceholder: \"Insert your API Key here\",\n\t\tDbField: \"api_key\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"API Secret\",\n\t\tPlaceholder: \"Insert your API Secret here\",\n\t\tDbField: \"api_secret\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Var 1\",\n\t\tPlaceholder: \"Insert your Var1 here\",\n\t\tDbField: \"var1\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Var2\",\n\t\tPlaceholder: \"Var2 goes here\",\n\t\tDbField: \"var2\",\n\t}},\n}}\n\n// REQUIRED init() will install/load the notifier\nfunc init() {\n\tnotifier.AddNotifier(example)\n}\n\n// REQUIRED - Send is where you would put the action's of your notifier\nfunc (n *Example) Send(msg interface{}) error {\n\tmessage := msg.(string)\n\tfmt.Printf(\"i received this string: %v\\n\", message)\n\treturn nil\n}\n\n// REQUIRED\nfunc (n *Example) Select() *notifier.Notification {\n\treturn n.Notification\n}\n\n// REQUIRED\nfunc (n *Example) OnSave() error {\n\tmsg := fmt.Sprintf(\"received on save trigger\")\n\tn.AddQueue(msg)\n\treturn nil\n}\n\n// REQUIRED\nfunc (n *Example) Test() error {\n\tmsg := fmt.Sprintf(\"received a test trigger\\n\")\n\tn.AddQueue(msg)\n\treturn nil\n}\n\n// REQUIRED - BASIC EVENT\nfunc (n *Example) OnSuccess(s *types.Service) {\n\tmsg := fmt.Sprintf(\"received a count trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// REQUIRED - BASIC EVENT\nfunc (n *Example) OnFailure(s *types.Service, f *types.Failure) {\n\tmsg := fmt.Sprintf(\"received a failure trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewService(s *types.Service) {\n\tmsg := fmt.Sprintf(\"received a new service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedService(s *types.Service) {\n\tmsg := fmt.Sprintf(\"received a update service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnDeletedService(s *types.Service) {\n\tmsg := fmt.Sprintf(\"received a delete service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a new user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a updated user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnDeletedUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a deleted user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedCore(s *types.Core) {\n\tmsg := fmt.Sprintf(\"received a updated core trigger for core: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewNotifier(s *Notification) {\n\tmsg := fmt.Sprintf(\"received a new notifier trigger for notifier: %v\\n\", s.Method)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedNotifier(s *Notification) {\n\tmsg := fmt.Sprintf(\"received a update notifier trigger for notifier: %v\\n\", s.Method)\n\tn.AddQueue(msg)\n}\n```\n\n\n

Prometheus Exporter

\nStatping includes a prometheus exporter so you can have even more monitoring power with your services. The prometheus exporter can be seen on `/metrics`, simply create another exporter in your prometheus config. Use your Statping API Secret for the Authorization Bearer header, the `/metrics` URL is dedicated for Prometheus and requires the correct API Secret has `Authorization` header.\n\n# Grafana Dashboard\nStatping has a [Grafana Dashboard](https://grafana.com/dashboards/6950) that you can quickly implement if you've added your Statping service to Prometheus. Import Dashboard ID: `6950` into your Grafana dashboard and watch the metrics come in!\n\n

\n\n## Basic Prometheus Exporter\nIf you have Statping and the Prometheus server in the same Docker network, you can use the yaml config below.\n```yaml\nscrape_configs:\n - job_name: 'statping'\n scrape_interval: 30s\n bearer_token: 'SECRET API KEY HERE'\n static_configs:\n - targets: ['statping:8080']\n```\n\n## Remote URL Prometheus Exporter\nThis exporter yaml below has `scheme: https`, which you can remove if you arn't using HTTPS.\n```yaml\nscrape_configs:\n - job_name: 'statping'\n scheme: https\n scrape_interval: 30s\n bearer_token: 'SECRET API KEY HERE'\n static_configs:\n - targets: ['status.mydomain.com']\n```\n\n### `/metrics` Output\n```\nstatping_total_failures 206\nstatping_total_services 4\nstatping_service_failures{id=\"1\" name=\"Google\"} 0\nstatping_service_latency{id=\"1\" name=\"Google\"} 12\nstatping_service_online{id=\"1\" name=\"Google\"} 1\nstatping_service_status_code{id=\"1\" name=\"Google\"} 200\nstatping_service_response_length{id=\"1\" name=\"Google\"} 10777\nstatping_service_failures{id=\"2\" name=\"Statping.com\"} 0\nstatping_service_latency{id=\"2\" name=\"Statping.com\"} 3\nstatping_service_online{id=\"2\" name=\"Statping.com\"} 1\nstatping_service_status_code{id=\"2\" name=\"Statping.com\"} 200\nstatping_service_response_length{id=\"2\" name=\"Statping.com\"} 2\n```\n\n

SSL

\nYou can run Statping with a valid certificate by including 2 files in the root directory. Although, I personally recommend using NGINX or Apache to serve the SSL and then have the webserver direct traffic to the Statping instance. This guide will show you how to implement SSL onto your Statping server with multiple options.\n\n## SSL Certificate with Statping\nTo run the Statping HTTP server in SSL mode, you must include 2 files in the root directory of your Statping application. The 2 files you must include are:\n- `server.crt` SSL Certificate File\n- `server.key` SSL Certificate Key File\n\nThe filenames and extensions must match the exact naming above. If these 2 files are found, Statping will automatically start the HTTP server in SSL mode using your certificates. You can also generate your own SSL certificates, but you will receive a \"ERR_CERT_AUTHORITY_INVALID\" error. To generate your own, follow the commands below:\n\n```shell\nopenssl req -new -sha256 -key server.key -out server.csr\nopenssl x509 -req -sha256 -in server.csr -signkey server.key -out server.crt -days 3650\n```\nThis will generate a self signed certificate that you can use for your Statup instance. I recommend using a web server to do SSL termination for your server though.\n\n## Choose a Web Server or Environment\n\n**Choose the environment running the Statping instance.**\n- [Docker](#docker)\n- [NGINX](#nginx)\n- [Apache](#apache)\n\n## Docker\nDocker might be the easiest way to get up and running with a SSL certificate. Below is a `docker-compose.yml` file that will run NGINX, LetEncrypt, and Statping.\n\n1. Point your domain or subdomain to the IP address of the Docker server. This would be done on CloudFlare, Route53, or some other DNS provider.\n\n2. Replace the `docker-compose.yml` contents:\n- `MY.DOMAIN.COM` with the domain you want to use\n- `MY@EMAIL.COM` with your email address\n\n3. Run the docker container by running command `docker-compose up -d`. Give a little bit of time for LetEncrypt to automatically generate your SSL certificate.\n\n###### `docker-compose.yml`\n```yaml\nversion: '2.3'\nservices:\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n labels:\n - \"com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy\"\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statping/nginx/certs:/etc/nginx/certs:ro\n - ./statping/nginx/vhost:/etc/nginx/vhost.d\n - ./statping/nginx/html:/usr/share/nginx/html:ro\n - ./statping/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: MY.DOMAIN.COM\n\n letsencrypt:\n container_name: letsencrypt\n image: jrcs/letsencrypt-nginx-proxy-companion\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - ./statping/nginx/certs:/etc/nginx/certs\n - ./statping/nginx/vhost:/etc/nginx/vhost.d\n - ./statping/nginx/html:/usr/share/nginx/html\n - ./statping/nginx/dhparam:/etc/nginx/dhparam\n\n statping:\n container_name: statping\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n depends_on:\n - nginx\n volumes:\n - ./statping/app:/app\n environment:\n VIRTUAL_HOST: MY.DOMAIN.COM\n VIRTUAL_PORT: 8080\n LETSENCRYPT_HOST: MY.DOMAIN.COM\n LETSENCRYPT_EMAIL: MY@EMAIL.COM\n\nnetworks:\n internet:\n driver: bridge\n```\n\n## NGINX\nIf you already have a NGINX web server running, you just have to add a proxy pass and your SSL certs to the nginx config or as a vhost. By default Statping runs on port 8080, you can change this port by starting server with `statping -ip 127.0.0.1 -port 9595`.\n\n- Replace `/my/absolute/directory/for/cert/server.crt` with SSL certificate file.\n- Replace `/my/absolute/directory/for/key/server.key` with SSL key file.\n- Run `service nginx restart` and try out https on your domain.\n\n##### Tutorials\n- [NGINX Guide](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/)\n- [How To Set Up Nginx Load Balancing with SSL Termination](https://www.digitalocean.com/community/tutorials/how-to-set-up-nginx-load-balancing-with-ssl-termination)\n\n###### `/etc/nginx/nginx.conf`\n```\n#user nobody;\nworker_processes 1;\nevents {\n worker_connections 1024;\n}\nhttp {\n include mime.types;\n default_type application/octet-stream;\n send_timeout 1800;\n sendfile on;\n keepalive_timeout 6500;\n server {\n listen 80;\n server_name localhost;\n location / {\n proxy_pass http://localhost:8080;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Client-Verify SUCCESS;\n proxy_set_header X-Client-DN $ssl_client_s_dn;\n proxy_set_header X-SSL-Subject $ssl_client_s_dn;\n proxy_set_header X-SSL-Issuer $ssl_client_i_dn;\n proxy_read_timeout 1800;\n proxy_connect_timeout 1800;\n }\n }\n # HTTPS server\n \n server {\n listen 443;\n server_name localhost;\n \n ssl on;\n ssl_certificate /my/absolute/directory/for/cert/server.crt;\n ssl_certificate_key /my/absolute/directory/for/key/server.key;\n ssl_session_timeout 5m;\n \n ssl_protocols SSLv2 SSLv3 TLSv1;\n ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;\n ssl_prefer_server_ciphers on;\n \n location / {\n proxy_pass http://localhost:8080;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Client-Verify SUCCESS;\n proxy_set_header X-Client-DN $ssl_client_s_dn;\n proxy_set_header X-SSL-Subject $ssl_client_s_dn;\n proxy_set_header X-SSL-Issuer $ssl_client_i_dn;\n proxy_read_timeout 1800;\n proxy_connect_timeout 1800;\n }\n }\n}\n```\n\n## Apache\n\n

Config with .env File

\nIt may be useful to load your environment using a `.env` file in the root directory of your Statping server. The .env file will be automatically loaded on startup and will overwrite all values you have in config.yml.\n\nIf you have the `DB_CONN` environment variable set Statping will bypass all values in config.yml and will require you to have the other DB_* variables in place. You can pass in these environment variables without requiring a .env file.\n\n## `.env` File\n```bash\nDB_CONN=postgres\nDB_HOST=0.0.0.0\nDB_PORT=5432\nDB_USER=root\nDB_PASS=password123\nDB_DATABASE=root\n\nNAME=Demo\nDESCRIPTION=This is an awesome page\nDOMAIN=https://domain.com\nADMIN_USER=admin\nADMIN_PASSWORD=admin\nADMIN_EMAIL=info@admin.com\nUSE_CDN=true\nPOSTGRES_SSLMODE=false # enable ssl_mode for postgres (To enable use require)\nDISABLE_LOGS=false # disable logs from appearing and writing to disk\n\nIS_DOCKER=false\nIS_AWS=false\nSASS=/usr/local/bin/sass\nCMD_FILE=/bin/bash\n```\nThis .env file will include additional variables in the future, subscribe to this repo to keep up-to-date with changes and updates. \n\n

Static Export

\nIf you want to use Statping as a CLI application without running a server, you can export your status page to a static HTML.\nThis export tool is very useful for people who want to export their HTML and upload/commit it to Github Pages or an FTP server.\n```dash\nstatup export\n```\n###### Creates `index.html` in the current directory with CDN asset URL's. 💃 \n\n

Statping Plugins

\nSince Statping is built in Go Language we can use the [Go Plugin](https://golang.org/pkg/plugin/) feature to create dynamic plugins that run on load. Statping has an event anytime anything happens, you can create your own plugins and do any type of function. To implement your own ideas into Statping, use the plugin using the [statup/plugin](https://github.com/hunterlong/statping/blob/master/plugin/main.go) package.\n```\ngo get github.com/hunterlong/statping/plugin\n```\n\n## Example Plugin\nStart off with the [Example Statping Plugin](https://github.com/hunterlong/statping_plugin) that includes all the interfaces and some custom options for you to expand on. You can include any type of function in your own plugin!\n\n

\n\n

\n\n## Building Plugins\nPlugins don't need a push request and they can be private! You'll need to compile your plugin to the Golang `.so` binary format. Once you've built your plugin, insert it into the `plugins` folder in your Statping directory and reboot the application. Clone the [Example Statping Plugin](https://github.com/hunterlong/statping_plugin) repo and try to build it yourself!\n\n#### Build Requirements\n- You must have `main.go`\n- You must create the Plugin variable on `init()`\n\n```bash\ngit clone https://github.com/hunterlong/statping_plugin\ncd statup-plugin\ngo build -buildmode=plugin -o example.so\n```\n###### Insert `example.so` into the `plugins` directory and reload Statping\n\n## Testing Statping Plugins\nStatping includes a couple tools to help you on your Plugin journey, you can use `statup test plugins` command to test all plugins in your `/plugins` folder. This test will attempt to parse your plugin details, and then it will send events for your plugin to be fired.\n```\nstatup test plugins\n```\n

\n\n

\n\nYour plugin should be able to parse and receive events before distributing it. The test tools creates a temporary database (SQLite) that your plugin can interact with. Statping uses [upper.io/db.v3](https://upper.io/db.v3) for database interactions. The database is passed to your plugin `OnLoad(db sqlbuilder.Database)`, so you can use the `db` variable passed here.\n\n## Statping Plugin Interface\nPlease remember Golang plugin's are very new and Statping plugin package may change and 'could' brake your plugin. Checkout the [statup/plugin package](https://github.com/hunterlong/statping/blob/master/plugin/main.go) to see the most current interfaces.\n```go\ntype PluginActions interface {\n\tGetInfo() Info\n\tGetForm() string\n\tSetInfo(map[string]interface{}) Info\n\tRoutes() []Routing\n\tOnSave(map[string]interface{})\n\tOnFailure(map[string]interface{})\n\tOnSuccess(map[string]interface{})\n\tOnSettingsSaved(map[string]interface{})\n\tOnNewUser(map[string]interface{})\n\tOnNewService(map[string]interface{})\n\tOnUpdatedService(map[string]interface{})\n\tOnDeletedService(map[string]interface{})\n\tOnInstall(map[string]interface{})\n\tOnUninstall(map[string]interface{})\n\tOnBeforeRequest(map[string]interface{})\n\tOnAfterRequest(map[string]interface{})\n\tOnShutdown()\n\tOnLoad(sqlbuilder.Database)\n}\n```\n\n## Event Parameters\nAll event interfaces for the Statping Plugin will return a `map[string]interface{}` type, this is because the plugin package will most likely update and change in the future, but using this type will allow your plugin to continue even after updates.\n\n## Example of an Event\nKnowing what happens during an event is important for your plugin. For example, lets have an event that echo something when a service has a Failure status being issued. Checkout some example below to see how this golang plugin action works. \n\n```go\nfunc (p pkg) OnSuccess(data map[string]interface{}) {\n fmt.Println(\"Statping Example Plugin received a successful service hit! \")\n fmt.Println(\"Name: \", data[\"Name\"])\n fmt.Println(\"Domain: \", data[\"Domain\"])\n fmt.Println(\"Method: \", data[\"Method\"])\n fmt.Println(\"Latency: \", data[\"Latency\"])\n}\n```\n###### OnSuccess is fired every time a service has check it be online\n\n```go\nfunc OnFailure(service map[string]interface{}) {\n fmt.Println(\"oh no! an event is failing right now! do something!\")\n fmt.Println(service)\n}\n```\n###### OnFailure is fired every time a service is failing\n\n```go\nfunc (p pkg) OnLoad(db sqlbuilder.Database) {\n fmt.Println(\"=============================================================\")\n fmt.Printf(\" Statping Example Plugin Loaded using %v database\\n\", db.Name())\n fmt.Println(\"=============================================================\")\n}\n```\n###### OnLoad is fired after plugin is loaded into the environment\n\n\n## Interacting with Database\nThe Example Statping Plugin includes a variable `Database` that will allow you to interact with the Statping database. Checkout [database.go](https://github.com/hunterlong/statping_plugin/blob/master/database.go) to see a full example of Create, Read, Update and then Deleting a custom Communication entry into the database.\n```go\n// Insert a new communication into database\n// once inserted, return the Communication\nfunc (c *Communication) Create() *Communication {\n\tuuid, err := CommunicationTable().Insert(c)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tc.Id = uuid.(int64)\n\treturn c\n}\n```\n\n## Custom HTTP Routes\nPlugin's can include their own HTTP route to accept GET/POST requests. Route are loaded after Statping loads all of it's Routes. Checkout [routes.go](https://github.com/hunterlong/statping_plugin/blob/master/routes.go) on the example plugin to see a full example of how to use it.\n```go\n// You must have a Routes() method in your plugin\nfunc (p *pkg) Routes() []plugin.Routing {\n\treturn []plugin.Routing{{\n\t\tURL: \"hello\",\n\t\tMethod: \"GET\",\n\t\tHandler: CustomInfoHandler,\n\t}}\n}\n\n// This is the HTTP handler for the '/hello' URL created above\nfunc CustomInfoHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tfmt.Fprintln(w, \"Oh Wow!!! This is cool...\")\n}\n```\n\n\n## Plugin To-Do List\n- [ ] Ability to includes assets like jpg, json, etc\n\n

Statuper

\nStatping includes a simple to use installation shell script that will help you install locally, Docker, and even onto a AWS EC2 instance.\n\n

\n\n

\n\n## Installation\n```bash\ncurl -O https://assets.statup.io/statuper && chmod +x statuper\n```\n\n## Usage\n- `statuper`\n\n

Build and Test

\nBuilding from the Go Language source code is pretty easy if you already have Go installed. Clone this repo and `cd` into it. \n\n### Git n' Go Get\n```bash\ngit clone https://github.com/hunterlong/statping.git\ncd statup\ngo get -v\n```\n\n### Install go.rice\nStatping uses go.rice to compile HTML, JS, and CSS files into it's single binary output.\n```\ngo get github.com/GeertJohan/go.rice\ngo get github.com/GeertJohan/go.rice/rice\n```\n\n### Build Statping Binary\nStatping uses go.rice to compile HTML, JS, and CSS files into it's single binary output.\n```\nrice embed-go\ngo build -o statup .\n./statup version\n```\n\n### Test Coverage\nYou can also test Statio on your localhost, but it does require a MySQL, and Postgres server to be accessible since testing does create/drop tables for multiple databases. \n```\ngo test -v\n```\n\n

Contributing

\nHave a feature you want to implement into Statping!? Awesome! Follow this guide to see how you can test, compile and build Statping for production use. I recommend you use `make` with this process, it will save you time and it will auto include many customized parameters to get everything working correctly.\n\n# Dependencies\nStatping has a couple of required dependencies when testing and compiling the binary. The [Makefile](https://github.com/hunterlong/statping/blob/master/Makefile) will make these tasks a lot easier. Take a look at the Makefile to see what commands are ran. Run the command below to get setup right away.\n```bash\nmake dev-deps\n```\nList of requirements for compiling assets, building binary, and testing.\n- [Go Language](https://golang.org/) (currently `1.10.3`)\n- [Docker](https://docs.docker.com/)\n- [SASS](https://sass-lang.com/install)\n- [Cypress](https://www.cypress.io/) (only used for UI testing, `make cypress-install`)\n\n# Compiling Assets\nThis Golang project uses [rice](https://github.com/GeertJohan/go.rice) to compile static assets into a single file. The file `source/rice-box.go` is never committed to the Github repo, it is automatically created on build. Statping also requires `sass` to be installed on your local OS. To compile all the static assets run the command below:\n\n```bash\nmake compile\n```\nAfter this is complete, you'll notice the `source/rice-box.go` file has been generated. You can now continue to build, and test.\n\n# Testing\nStatping includes multiple ways to Test the application, you can run the `make` command, or the normal `go test` command. To see the full experience of your updates, you can even run Cypress tests which is in the `.dev/test` folder.\n\nStatping will run all tests in `cmd` folder on MySQL, Postgres, and SQLite databases. You can run `make databases` to automatically create MySQL and Postgres with Docker.\n\n###### Go Unit Testing:\n```bash\nmake test\n```\n\n###### Cypress UI Testing:\n```bash\nmake cypress-test\n```\n\n###### Test Everything:\n```bash\nmake test-all\n```\n\n# Build\nStatping will build on all operating systems except Windows 32-bit. I personally use [xgo](https://github.com/karalabe/xgo) to cross-compile on multiple systems using Docker. Follow the commands below to build on your local system.\n\n###### Build for local operating system:\n```bash\nmake build\n```\n\n# Compile for Production\nOnce you've tested and built locally, you can compile Statping for all available operating systems using the command below. This command will require you to have Docker.\n\n```bash\nmake build-all\n```\n\n# What Now\nEverything tested, compiled and worked out!? Awesome! 💃 You can now commit your changes, and submit a Pull Request with the features/bugs you added or removed.\n\n\n\n\n\n

PGP Signature

\nYou can check if the Statping binary you downloaded is authentic by running a few commands.\n\n### Steps to Authenticate\n1. Download the Statping `tar.gz` file from [Latest Releases](https://github.com/hunterlong/statping/releases/latest) and extract the `statping` binary and the `statup.asc` file.\n2. Run command: `gpg --verify statping.asc`\n3. You should see `Good signature from \"Hunter Long \" [ultimate]`.\n\n# Statping Public Key\n- [https://statping.com/statping.gpg](https://statping.com/statping.gpg)\n\nYou can also download the key with the command below:\n```\nwget https://statping.com/statping.gpg\n```\n\n###### `statping.gpg`\n```\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFwGUYIBEADNsDY4aUOx8EoZuTRFPtjuadJzFRyKtHhw/tLlAnoFACanZPIT\nNZoRYvRR5v6lMDXdxsteDbJEOhZ1WDiKIr4OyMahPsyyH6ULzSBKgePUswa0sDef\nUnXYzPFQCzqQyQQFbp9AYfDP7dW6dTL9I6qU2NqlJvjxJiiZTAq87SmsLqHiASnI\n+ottnQuu6vJQBJz2PFIuaS1c3js/+HBbth9GK5B9YN1BIIyZoFmWKVU9HnJf+aM3\nUs6OLjjwYwWzQH38ZV84IjVXyiP9PQVhlCXeHK7XdhPZvnSP1m5Wszj/jowwY6Mz\nLgLotfL540X7yOJ7hJTFYLFBOtJdJr/3Ov8SH4HXdPFPVG+UqxsmtmPqUQ9iAxAE\njRFfkAxBvH5Szf2WZdaLnlrrOcOKJIIjZgHqalquBTAhlh5ul0lUVSSPxetwIBlW\n60L41k94NJFGDt8xOJ+122mLcywmQ1CzhDfeIKlxl6JDiVHjoRqlQQrqIoNZMV85\nrzGfrmbuwv1MXGBJoiNy3330ujOBmhQ9dQVwKpxhBKdjnAgIGM9szbUYxIkGgM1O\nU4b1WF3AF/9JOpKJ0LewslpM3BFFYnemGsHXAv3TBPqKidNdwMAiBOtNykGoXF6i\n0D6jOW/IB1da0gUA+kr5JdAOwIG7JXKhur2MO7Ncid59DL2N8RePRWj+jwARAQAB\ntB9IdW50ZXIgTG9uZyA8aW5mb0BzdGF0cGluZy5jb20+iQJOBBMBCAA4FiEEt21h\n+qbbdZRm6D2ZZLnGquLVUngFAlwGUYICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\nF4AACgkQZLnGquLVUnizwA//c7vmwTMq/8LYlbo37WM2kDE9AKIrz6VSMq4RhGbC\nLikH0X0epa+if79n9BZrVU/Af3aKTn7vu2J4XrvzcdCXtcsR0YmCWML2Y6OSFmhX\nw3o6woiFcp+SUWdcM/kithRun+j9sKV4akdgkdBQUdh/RMVln+radz1c6G59iTdh\nS+Ip3ObO7Gn5VnrLwxix+W9Jhg8YhDgDGEDt8e1yvjuMRY+WhjHFlwEMoE0kvQL8\nQvQH2dGD3dExWAuIL7+0xC0ZGU0PR8vRrq1ukdIsWlDY+42vvhcyPZKFFDTM/QLF\nFcCNiPSGhiK/NQq67xnRMFdh0fnqbydWj2atMpacIrheEkOt8db2/UMyDOwlIxgy\nKOG8x+yNKiG9LyvW4axRLctN608/+TbvtFo5TVOFJYxJQp4b5uz7LgJAJw7PBvfC\nbqx64BH8WGzgyGcAl9unQEtpDuxXoKvP2kbsS7hjvhK0gJgW9llpV4sRJJGApTBc\nWtbcS9DBGs3k1aZdA72bxnayD32syVz7czl4+tkRsbQZ4VgJh1yrHIDsdWQXFnYu\nEQJfCgX5HvvC13MpDUth0NWCFtWQirY3EFbIgSuhB/D5iXA+Dt1Dq5c1u7wQlUVi\nLQCU++oMGrlU3gZrnov5lnBGCEjn0O9bKQm8zmLdEcENFxUZvfPjOIY64YprZxD9\nBv65Ag0EXAZRggEQAMmjHmnvH8SvNJhku/oI96dFKen3bg9xdaFUD1vAuNglCalH\nwgXcCZd0RdobYNG46cXTzTQadtHS4hi/UBJ+oy5ZUpIRglW12eTYtqM2G11VbcQi\nj6rLITP9NIP+G1xBICSYK4UwmH55BolMEQ/1ZX0a9rESM9stDNglheCCudbMGR/1\nZYnufdEsh0yPwyC/1upZeu8LPWK62pt9mE/gccx77QTeDi5OJcRf1fPbUTCm3vSS\nwPPV2AGANodIhostjDymt5vh0tGwc7oUZZLnVdErfuctv7yMgZdiCpYu0jFy1NYf\nJgOpZasrcK7/1ozGzsfAo/sSU4kIkMwuWGgqfx5kGRK2CgU4T0i7oI6DMpOX9ZS8\ns3+oCWu83X0ofvm5R2CbjiUj2gR6JOhBQbJpCeTkLe+SFcUpnyrr7lG8B8QZHm5N\nnBi05V/s63RE3g/6WpR/fWuh+uswe01uqlSx9deW7jT49BL/MdSxwjfwLBLz/hLM\n0ld385XAd9bqMjUtp0XhZX2YORx3f/aKY7PYA62baGibb5RdPRw6viEAWU20eb+8\nX9Pa7hGmwUeal5lka4SD/TGl7wdY+g4oYP+jtKinH/ZftWA5wHTe3jWT5bdWrT2d\ne+0qA0SBkmKIDLpktvtTa19w2nfwBIwJ6fN36ZjYpOn/stxR7aRtnhSqvzxbABEB\nAAGJAjYEGAEIACAWIQS3bWH6ptt1lGboPZlkucaq4tVSeAUCXAZRggIbDAAKCRBk\nucaq4tVSeGWmD/9Pg1x6s98zdZCQa2apmUnuoQAQA9Gf2RBBuglCDGsY67wbvdHZ\n9wdFRs2QEhl2O3oFmidxthBOBRl9z62nXliLwNn1Lcy/yDfaB8wH6gMm4jn2N/z9\nvQXnyIzg8m4PItZ1p5mnY3qH5lpGF8r9Gb7tzK10rqulM2XTDojZOevlEGI6LGw8\nFjccXtNquqGZwxzytmKF3T7UBmpmt2qock8N5iJn987m6WeYmbFNc0ii0guHfdtO\nzQcItz2ngCdyvfgQPwCAoAv72ysSGhz5KZgAXRrEdcqj6Jw3ivoEUKq1aUrYncXQ\n3zC3ED6AjWOGRzjvTZzj22IVacUZ0gqx0x/oldXLOhMB9u6nFXHKj1n9nc0XHMNi\nLp9EuvQgcNLjFZGE9sxh25u9V+OhItfT/aarYEu/Xq0IkUUcdz4GehXth1/Cq1wH\nlSUie4nCs7I7OWhqMNClqP7ywElDXsQ66MCgvf01Dh64YUVjJNnyyK0QiYlCx/JQ\nZ85hNLtVXZfYqC5BRZlVFp8I8Rs2Qos9YEgn2M22+Rj+RIeD74LZFB7Q4myRvTMB\n/P466dFI83KYhwvjBYOP3jPTrV7Ky8poEGifQp2mM294CFIPS7z0z7a8+yMzcsRP\nOluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ==\n=Xn0G\n-----END PGP PUBLIC KEY BLOCK-----\n```\n\n

Testing

\nIf you want to test your updates with the current golang testing units, you can follow the guide below to run a full test process. Each test for Statping will run in MySQL, Postgres, and SQlite to make sure all database types work correctly.\n\n## Create Docker Databases\nThe easiest way to run the tests on all 3 databases is by starting temporary databases servers with Docker. Docker is available for Linux, Mac and Windows. You can download/install it by going to the [Docker Installation](https://docs.docker.com/install/) site.\n\n```go\ndocker run -it -d \\\n -p 3306:3306 \\\n -env MYSQL_ROOT_PASSWORD=password123 \\\n -env MYSQL_DATABASE=root mysql\n```\n\n```go\ndocker run -it -d \\\n -p 5432:5432 \\\n -env POSTGRES_PASSWORD=password123 \\\n -env POSTGRES_USER=root \\\n -env POSTGRES_DB=root postgres\n```\n\nOnce you have MySQL and Postgres running, you can begin the testing. SQLite database will automatically create a `statup.db` file and will delete after testing.\n\n## Run Tests\nInsert the database environment variables to auto connect the the databases and run the normal test command: `go test -v`. You'll see a verbose output of each test. If all tests pass, make a push request! 💃\n```go\nDB_DATABASE=root \\\n DB_USER=root \\\n DB_PASS=password123 \\\n DB_HOST=localhost \\\n go test -v\n```\n\n

Deployment

\nStatping is a pretty cool server for monitoring your services. The way we deploy might be a little cooler though. Statping is using the most bleeding edge technology to release updates and distribute binary files automatically.\n\n1. Source code commits get pushed to Github\n2. [Rice](https://github.com/GeertJohan/go.rice) will compile all the static assets into 1 file (rice-box.go in source)\n3. SASS will generate a compiled version of the CSS. \n4. Statping Help page is generated by cloning the Wiki repo using `go generate`.\n5. Travis-CI tests the Golang application.\n6. Travis-CI tests the Statping API using [Postman](https://github.com/hunterlong/statping/blob/master/source/tmpl/postman.json).\n7. If all tests are successful, Travis-CI will compile the binaries using [xgo](https://github.com/karalabe/xgo).\n8. Binaries are code signed using the official [PGP key](https://github.com/hunterlong/statping/wiki/PGP-Signature) and compressed.\n9. [Docker](https://cloud.docker.com/repository/docker/hunterlong/statping/builds) receives a trigger to build for the `latest` tag.\n10. Travis-CI uploads the [latest release](https://github.com/hunterlong/statping/releases) as a tagged version on Github.\n11. Travis-CI updates the [homebrew-statping](https://github.com/hunterlong/homebrew-statping) repo with the latest version.\n\nAnd that's it! Statping is ready to be shipped and installed.\n\n") +var CompiledWiki = []byte("Types of Monitoring
Features
Start Statping
Linux
Mac
Windows
AWS EC2
Docker
Mobile App
Heroku
API
Makefile
Notifiers
Notifier Events
Notifier Example
Prometheus Exporter
SSL
Config with .env File
Static Export
Statping Plugins
Statuper
Build and Test
Contributing
PGP Signature
Testing
Deployment
\n\n

Types of Monitoring

\nYou can monitor your application by using a simple HTTP GET to the endpoint to return back a response and status code. Normally you want a 200 status code on an HTTP request. You might want to require a 404 or 500 error as a response code though. With each service you can include a Timeout in seconds to work with your long running services.\n\n# HTTP Endpoints with Custom POST\nFor more advanced monitoring you can add a data as a HTTP POST request. This is useful for automatically submitting JSON, or making sure your signup form is working correctly.\n\n

\n\n

\n\nWith a HTTP service, you can POST a JSON string to your endpoint to retrieve any type of response back. You can then use Regex in the Expected Response field to parse a custom response that exactly matches your status requirements. \n\n# TCP/UDP Services\nFor other services that don't use HTTP, you can monitor any type of service by using the PORT of the service. If you're Ethereum Blockchain server is running on 8545, you can use TCP to monitor your server. With a TCP service, you can monitor your Docker containers, or remove service running on a custom port. You don't need to include `http` in the endpoint field, just IP or Hostname.\n\n

\n\n

\n\n# ICMP Service\nYou can send a [ICMP](https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol) (ping) to an endpoint rather than HTTP/TCP/UDP request for a quick response.\n\n\n

Features

\nStatping is a great Status Page that can be deployed with 0 effort.\n\n# 3 Different Databases\nYou can use MySQL, Postgres, or SQLite as a database for your Statping status page. The server will automatically upgrade your database tables depending on which database you have.\n\n# Easy to Startup\nStatping is an extremely easy to setup website monitoring tool without fussing with dependencies or packages. Simply download and install the precompile binary for your operating system. Statping works on Windows, Mac, Linux, Docker, and even the Raspberry Pi.\n\n# Plugins\nStatping is an awesome Status Page generator that allows you to create your own plugins with Golang Plugins! You don't need to request a PR or even tell us about your plugin. Plugin's are compiled and then send as a binary to the Statping `/plugins` folder. Test your plugins using the `statup test plugin` command, checkout the [Plugin Wiki](https://github.com/hunterlong/statping/wiki/Statping-Plugins) to see detailed information about creating plugins.\n\n# No Maintenance\nMany other website monitoring applications will collect data until the server fails because of hard drive is 100% full. Statping will automatically delete records to make sure your server will stay UP for years. The EC2 AMI Image is a great way to host your status page without worrying about it crashing one day. Statping will automatically upgrade its software when you reboot your computer.\n\n# Email & Slack Notifications\nReceive email notifications if your website or application goes offline. Statping includes SMTP connections so you can use AWS SES, or any other SMTP emailing service. Go in the Email Settings in Settings to configure these options.\n\n# Prometheus Exporter\nIf you want a deeper view of your applications status, you can use Grafana and Prometheus to graph all types of data about your services. Read more about the [Prometheus Exporter](https://github.com/hunterlong/statping/wiki/Prometheus-Exporter)\n\n

Start Statping

\n\n\n

Linux

\n# Installing on Linux\nInstalling Statping on Linux can be done by downloading the latest tar.gz file, unzipping, and running the executable. You can also install using [Snapcraft](https://snapcraft.io/) for Ubuntu systems.\n\n```shell\ncurl -o- -L https://statping.com/install.sh | bash\n```\n\n## Install using Snapcraft\n\n[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-white.svg)](https://snapcraft.io/statping)\n\nIf you are using [snap](https://snapcraft.io/statping), you can simply run this command to install Statping.\n```shell\nsudo snap install statping\n```\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Apt: `apt install ruby-sass -y`\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n## Systemd Service\nSetting up a systemd service is a great way to make sure your Statping server will automatically reboot when needed. You can use the file below for your service. You should have Statping already installed by this step.\n###### /etc/systemd/system/statping.service\n```\n[Unit]\nDescription=Statping Server\nAfter=network.target\nAfter=systemd-user-sessions.service\nAfter=network-online.target\n\n[Service]\nType=simple\nRestart=always\nExecStart=/usr/local/bin/statping\nWorkingDirectory=/usr/local/bin\n\n[Install]\nWantedBy=multi-user.target\n```\nThen you can enable and start your systemd service with:\n```\nsystemctl daemon-reload\n\nsystemctl enable statping.service\n\nsystemctl start statping\n```\nYou're Statping server will now automatically restart when your server restarts.\n\n## Raspberry Pi\nYou can even run Statping on your Raspberry Pi by installing the precompiled binary from [Latest Releases](https://github.com/hunterlong/statping/releases/latest). For the Raspberry Pi 3 you'll want to download the `statping-linux-arm7.tar.gz` file. Be sure to change `VERSION` to the latest version in Releases, and include the 'v'.\n\n```\nVERSION=$(curl -s \"https://github.com/hunterlong/statping/releases/latest\" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}')\nwget https://github.com/hunterlong/statping/releases/download/$VERSION/statping-linux-arm7.tar.gz\ntar -xvzf statping-linux-arm7.tar.gz\nchmod +x statping\nmv statping /usr/local/bin/statping\n\nstatping version\n``` \n\n## Alpine Linux\nThe Docker image is using the Statping Alpine binary since it's so incredibly small. You can run it on your own alpine image by downloading `statping-linux-alpine.tar.gz` from [Latest Releases](https://github.com/hunterlong/statping/releases/latest).\n\n

Mac

\n# Installing on Mac\nStatping includes an easy to use [Homebrew Formula](https://github.com/hunterlong/homebrew-statping) to quick get your Status Page up and running locally. Statping on brew is automatically generated for each new release to master. Install with the commands below,\n###### Using Homebrew\n```bash\nbrew tap hunterlong/statping\nbrew install statping\n```\n###### Using the Terminal\n```shell\ncurl -o- -L https://statping.com/install.sh | bash\n```\n\n

\n\n

\n\nOnce you've installed it, checkout which version you have by running `statping version`.\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n\n

Windows

\n# Installing on Windows\nCurrently, Statping only works on Windows 64-bit computers. Just download the exe file from [Latest Releases](https://github.com/hunterlong/statping/releases/latest) and run it in your command prompt. It will create a HTTP server on port 8080, so you can visit `http://localhost:8080` to see your Statping Status Page.\n\n# Compiling SCSS for Custom Theme\nStatping requires `sass` to be installed to the local machine to compile SCSS into CSS if you want to use the Custom Theme features. \n\n- Node: `npm install sass -g`\n- Ruby: `gem install sass`\n\n# Running Statping as a Service\nTo ensure Statping is always running, it can be installed to run as a service on a Windows machine. The easiest way to do that is by using NSSM, the [Non-Sucking Service Manager](https://nssm.cc/download). Download and unzip the compressed file to a location on your machine running Statping to get started:\n1. Open an administrative command prompt.\n2. Change to the folder that contains the 64 bit version of NSSM.\n3. Type \"nssm install Statping\" and press enter.\n4. For the properties, use the following as an example:\n Path: C:\\Program Files\\Statping\\statping.exe\n Startup directory: C:\\Program Files\\Statping\n5. Click \"Install\".\n6. Launch the windows services manager.\n7. Run Statping.\n\n## Known Issues with Windows\nUnfortunately, Statping only works on Windows 64-bit processors. If you have more than 4gb of ram, there's a good chance you already have a 64-bit processor. Download the [Latest Releases](https://github.com/hunterlong/statping/releases/latest) of Statping, extract the ZIP file, then double click on the `statping.exe` file. You can use a SQLite database for a quick setup, or connect to a local/remote Postgres or MySQL database server.\n\n

AWS EC2

\nRunning Statping on the smallest EC2 server is very quick using the AWS AMI Image. The AWS AMI Image will automatically start a Statping Docker container that will automatically update to the latest version. Once the EC2 is booted, you can go to the Public DNS domain to view the Statping installation page. The Statping root folder is located at: `/statping` on the server.\n\n# AMI Image\nChoose the correct AMI Image ID based on your AWS region.\n- us-east-1 `ami-09ccd23d9c7afba61` (Virginia)\n- us-east-2 `ami-0c6c9b714a501cdb3` (Ohio)\n- us-west-1 `ami-02159cc1fc701a77e` (California)\n- us-west-2 `ami-007c6990949f5ccee` (Oregon)\n- eu-central-1 `ami-06e252d6d8b0c2f1f` (Frankfurt)\n\n# Upgrading Staping\nYou can upgrade the Statping executable by running the commands below on your EC2 instance.\n```\nVERSION=$(curl -s \"https://github.com/hunterlong/statping/releases/latest\" | grep -o 'tag/[v.0-9]*' | awk -F/ '{print $2}')\nwget https://github.com/hunterlong/statping/releases/download/$VERSION/statping-linux-x64.tar.gz\ntar -xvzf statping-linux-x64.tar.gz\nchmod +x statping\nmv statping /usr/local/bin/statping\n```\nYou can test the version number by running `statping version`.\n\n# Instructions\n\n### 1. Create an EC2 instance from AMI Image\nGo to the main EC2 dashboard and click 'Launch Instance'. Then type `Statping` inside the search field for 'Community AMI'. Once you've found it in your region, click Select!\n\n\n\n### 2. Get the Public DNS for EC2 Instance\nCopy the 'Public DNS' URL and paste it into your browser.\n\n\n\n### 3. Setup Statping\nUse SQLite if you don't want to connect to a remote MySQL or Postgres database.\n\n\n\n# EC2 Server Features\nRunning your Statping server on a small EC2 instance is perfect for most users. Below you'll find some commands to get up and running in seconds.\n- Super cheap on the t2.nano (~$4.60 monthly)\n- Small usage, 8gb of hard drive\n- Automatic SSL certificate if you require it\n- Automatic reboot when the server needs it\n- Automatic database cleanup, so you'll never be at 100% full.\n- Automatic docker containers/images removal\n\n## Create Security Groups\nUsing the AWS CLI you can copy and paste the commands below to auto create everything for you. The server opens port 80 and 443.\n```bash\naws ec2 create-security-group --group-name StatpingPublicHTTP --description \"Statping HTTP Server on port 80 and 443\"\n# will response back a Group ID. Copy ID and use it for --group-id below.\n```\n```bash\nGROUPS=sg-7e8b830f\naws ec2 authorize-security-group-ingress --group-id $GROUPS --protocol tcp --port 80 --cidr 0.0.0.0/0\naws ec2 authorize-security-group-ingress --group-id $GROUPS --protocol tcp --port 443 --cidr 0.0.0.0/0\n```\n## Create EC2 without SSL\nOnce your server has started, go to the EC2 Public DNS endpoint. You should be redirected to /setup to continue your installation process! The database information is already inputed for you.\n```bash\nGROUPS=sg-7e8b830f\nKEY=MYKEYHERE\nAMI_IMAGE=ami-7be8a103\n\naws ec2 run-instances \\\n --image-id $AMI_IMAGE \\\n --count 1 --instance-type t2.nano \\\n --key-name $KEY \\\n --security-group-ids $GROUPS\n```\n## Create EC2 with Automatic SSL Certification\nStart a Statping server with an SSL cert that will automatically regenerate when it's near expiration time. You'll need to point your domain's A record (IP address) or CNAME (public DNS endpoint) to use this feature.\n\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/dev/ec2-ssl.sh\n```\n\n```bash\n# edit the contents inside of ec2-ssl.sh then continue\nLETSENCRYPT_HOST=\"status.MYDOMAIN.com\"\nLETSENCRYPT_EMAIL=\"noreply@MYEMAIL.com\"\n```\nEdit ec2-ssl.sh and insert your domain you want to use, then run command below. Use the Security Group ID that you used above for --security-group-ids\n```\nGROUPS=sg-7e8b830f\nAMI_IMAGE=ami-7be8a103\nKEY=MYKEYHERE\n\naws ec2 run-instances \\\n --user-data file://ec2-ssl.sh \\\n --image-id $AMI_IMAGE \\\n --count 1 --instance-type t2.nano \\\n --key-name $KEY \\\n --security-group-ids $GROUPS\n```\n\n### EC2 Server Specs\n- t2.nano ($4.60 monthly)\n- 8gb SSD Memory\n- 0.5gb RAM\n- Docker with Docker Compose installed\n- Running Statping, NGINX, and Postgres\n- boot scripts to automatically clean unused containers.\n\n\n\n

Docker

\nStatping is easily ran on Docker with the light weight Alpine linux image. View on [Docker Hub](https://hub.docker.com/r/hunterlong/statping).\n\n[![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/)\n\n# Latest Docker Image\nThe `latest` Docker image uses Alpine Linux to keep it ultra small.\n```bash\ndocker run -d \\\n -p 8080:8080 \\\n --restart always \\\n hunterlong/statping\n```\n\n# Mounting Volume\nYou can mount a volume to the `/app` Statping directory. This folder will contain `logs`, `config.yml`, and static assets if you want to edit the SCSS/CSS. \n```bash\ndocker run -d \\\n -p 8080:8080 \\\n -v /mydir/statping:/app \\\n --restart always \\\n hunterlong/statping\n```\n\n# Attach a SSL Certificate\nWhen you mount `server.crt` and `server.key` to the `/app` directory, Statping will run a HTTPS server on port 443. Checkout the [SSL Wiki](https://github.com/hunterlong/statping/wiki/SSL) documentation to see more information about this.\n```bash\ndocker run -d \\\n -p 443:443 \\\n -v /mydir/domain.crt:/app/server.crt \\\n -v /mydir/domain.key:/app/server.key \\\n -v /mydir:/app \\\n --restart always \\\n hunterlong/statping\n```\n\n# Development Docker Image\nIf you want to run Statping that was build from the source, use the `dev` Docker image.\n```bash\ndocker run -d -p 8080:8080 hunterlong/statping:dev\n```\n\n# Cypress Testing Docker Image\nThis Docker image will pull the latest version of Statping and test the web interface with [Cypress](https://www.cypress.io/).\n```bash\ndocker run -it -p 8080:8080 hunterlong/statping:cypress\n```\n\n#### Or use Docker Compose\nThis Docker Compose file inlcudes NGINX, Postgres, and Statping.\n\n### Docker Compose with NGINX and Postgres\nOnce you initiate the `docker-compose.yml` file below go to http://localhost and you'll be forwarded to the /setup page. \nDatabase Authentication\n- database: `postgres`\n- port: `5432`\n- username: `statup`\n- password: `password123`\n- database: `statup`\n\n```yaml\nversion: '2.3'\n\nservices:\n\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs:ro\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html:ro\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: localhost\n\n statup:\n container_name: statup\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n - database\n depends_on:\n - postgres\n volumes:\n - ./statup/app:/app\n environment:\n VIRTUAL_HOST: localhost\n VIRTUAL_PORT: 8080\n DB_CONN: postgres\n DB_HOST: postgres\n DB_USER: statup\n DB_PASS: password123\n DB_DATABASE: statup\n NAME: EC2 Example\n DESCRIPTION: This is a Statping Docker Compose instance\n\n postgres:\n container_name: postgres\n image: postgres:10\n restart: always\n networks:\n - database\n volumes:\n - ./statup/postgres:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD: password123\n POSTGRES_USER: statup\n POSTGRES_DB: statup\n\nnetworks:\n internet:\n driver: bridge\n database:\n driver: bridge\n```\nOr a simple wget...\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/servers/docker-compose.yml\ndocker-compose up -d\n```\n\n### Docker Compose with Automatic SSL\nYou can automatically start a Statping server with automatic SSL encryption using this docker-compose file. First point your domain's DNS to the Statping server, and then run this docker-compose command with DOMAIN and EMAIL. Email is for letsencrypt services.\n```bash\nwget https://raw.githubusercontent.com/hunterlong/statping/master/servers/docker-compose-ssl.yml\n\nLETSENCRYPT_HOST=mydomain.com \\\n LETSENCRYPT_EMAIL=info@mydomain.com \\\n docker-compose -f docker-compose-ssl.yml up -d\n```\n\n#### Full docker-compose with Automatic SSL\n\n```yaml\nversion: '2.3'\n\nservices:\n\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n labels:\n - \"com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy\"\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs:ro\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html:ro\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: ${LETSENCRYPT_HOST}\n\n letsencrypt:\n container_name: letsencrypt\n image: jrcs/letsencrypt-nginx-proxy-companion\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - ./statup/nginx/certs:/etc/nginx/certs\n - ./statup/nginx/vhost:/etc/nginx/vhost.d\n - ./statup/nginx/html:/usr/share/nginx/html\n - ./statup/nginx/dhparam:/etc/nginx/dhparam\n\n statup:\n container_name: statup\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n - database\n depends_on:\n - postgres\n volumes:\n - ./statup/app:/app\n environment:\n VIRTUAL_HOST: ${LETSENCRYPT_HOST}\n VIRTUAL_PORT: 8080\n LETSENCRYPT_HOST: ${LETSENCRYPT_HOST}\n LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL}\n DB_CONN: postgres\n DB_HOST: postgres\n DB_USER: statup\n DB_PASS: password123\n DB_DATABASE: statup\n NAME: SSL Example\n DESCRIPTION: This Status Status Page should be running ${LETSENCRYPT_HOST} with SSL.\n\n postgres:\n container_name: postgres\n image: postgres:10\n restart: always\n networks:\n - database\n volumes:\n - ./statup/postgres:/var/lib/postgresql/data\n environment:\n POSTGRES_PASSWORD: password123\n POSTGRES_USER: statup\n POSTGRES_DB: statup\n\nnetworks:\n internet:\n driver: bridge\n database:\n driver: bridge\n```\n\n

Mobile App

\nStatping has a free mobile app so you can monitor your websites and applications without the need of a computer. \n\n![iTunes App Store](https://img.shields.io/itunes/v/1445513219.svg)\n\n

\n\n\n

\n\n

\n\n

\n\n\n

Heroku

\nYou can now instantly deploy your Statping instance on a free Heroku container. Simply click the deploy button below and get up in running within seconds. This Heroku deployment is based on the Statping Docker image so you will have all the great features including SASS and all the notifiers without any setup. \n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hunterlong/statping/tree/master)\n\nView the live Heroku Statping instance at: [https://statping.herokuapp.com](https://statping.herokuapp.com)\n\n# Database Configuration\nYou will need to deploy a Postgres database to your instance and insert some configuration variables. View the image below to see what environment variable you need to configure. If you insert `DB_CONN`, Statping will attempt to automatically connect to the database without the need for the `config.yml` file. \n\n![](https://img.cjx.io/herokustatping.png)\n\n\n

API

\nStatping includes a RESTFUL API so you can view, update, and edit your services with easy to use routes. You can currently view, update and delete services, view, create, update users, and get detailed information about the Statping instance. To make life easy, try out a Postman or Swagger JSON file and use it on your Statping Server.\n\n

\nPostman | Postman JSON Export | Swagger Export\n

\n\n## Authentication\nAuthentication uses the Statping API Secret to accept remote requests. You can find the API Secret in the Settings page of your Statping server. To send requests to your Statping API, include a Authorization Header when you send the request. The API will accept any one of the headers below.\n\n- HTTP Header: `Authorization: API SECRET HERE`\n- HTTP Header: `Authorization: Bearer API SECRET HERE`\n\n## Main Route `/api`\nThe main API route will show you all services and failures along with them.\n\n## Services\nThe services API endpoint will show you detailed information about services and will allow you to edit/delete services with POST/DELETE http methods.\n\n### Viewing All Services\n- Endpoint: `/api/services`\n- Method: `GET`\n- Response: Array of [Services](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Viewing Service\n- Endpoint: `/api/services/{id}`\n- Method: `GET`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Updating Service\n- Endpoint: `/api/services/{id}`\n- Method: `POST`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"name\": \"Updated Service\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 15,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0\n}\n```\n\n### Create New Service\n- Endpoint: `/api/services`\n- Method: `POST`\n- Response: [Service](https://github.com/hunterlong/statping/wiki/API#service-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"name\": \"Create New Service\",\n \"domain\": \"https://www.coogger.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 15,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0\n}\n```\n\n### Deleting Service\n- Endpoint: `/api/services/{id}`\n- Method: `DELETE`\n- Response: [Object Response](https://github.com/hunterlong/statping/wiki/API#object-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nResponse:\n```json\n{\n \"status\": \"success\",\n \"id\": 4,\n \"type\": \"service\",\n \"method\": \"delete\"\n}\n```\n\n## Users\nThe users API endpoint will show you users that are registered inside your Statping instance.\n\n### View All Users\n- Endpoint: `/api/users`\n- Method: `GET`\n- Response: Array of [Users](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Viewing User\n- Endpoint: `/api/users/{id}`\n- Method: `GET`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\n### Creating New User\n- Endpoint: `/api/users`\n- Method: `POST`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"username\": \"newadmin\",\n \"email\": \"info@email.com\",\n \"password\": \"password123\",\n \"admin\": true\n}\n```\n\n### Updating User\n- Endpoint: `/api/users/{id}`\n- Method: `POST`\n- Response: [User](https://github.com/hunterlong/statping/wiki/API#user-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nPOST Data:\n```json\n{\n \"username\": \"updatedadmin\",\n \"email\": \"info@email.com\",\n \"password\": \"password123\",\n \"admin\": true\n}\n```\n\n### Deleting User\n- Endpoint: `/api/services/{id}`\n- Method: `DELETE`\n- Response: [Object Response](https://github.com/hunterlong/statping/wiki/API#object-response)\n- Response Type: `application/json`\n- Request Type: `application/json`\n\nResponse:\n```json\n{\n \"status\": \"success\",\n \"id\": 3,\n \"type\": \"user\",\n \"method\": \"delete\"\n}\n```\n\n# Service Response\n```json\n{\n \"id\": 8,\n \"name\": \"Test Service 0\",\n \"domain\": \"https://status.coinapp.io\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 1,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 30,\n \"order_id\": 0,\n \"created_at\": \"2018-09-12T09:07:03.045832088-07:00\",\n \"updated_at\": \"2018-09-12T09:07:03.046114305-07:00\",\n \"online\": false,\n \"latency\": 0.031411064,\n \"24_hours_online\": 0,\n \"avg_response\": \"\",\n \"status_code\": 502,\n \"last_online\": \"0001-01-01T00:00:00Z\",\n \"dns_lookup_time\": 0.001727175,\n \"failures\": [\n {\n \"id\": 5187,\n \"issue\": \"HTTP Status Code 502 did not match 200\",\n \"created_at\": \"2018-09-12T10:41:46.292277471-07:00\"\n },\n {\n \"id\": 5188,\n \"issue\": \"HTTP Status Code 502 did not match 200\",\n \"created_at\": \"2018-09-12T10:41:47.337659862-07:00\"\n }\n ]\n}\n```\n\n# User Response\n```json\n{\n \"id\": 1,\n \"username\": \"admin\",\n \"api_key\": \"02f324450a631980121e8fd6ea7dfe4a7c685a2f\",\n \"admin\": true,\n \"created_at\": \"2018-09-12T09:06:53.906398511-07:00\",\n \"updated_at\": \"2018-09-12T09:06:54.972440207-07:00\"\n}\n```\n\n# Object Response\n```json\n{\n \"type\": \"service\",\n \"id\": 19,\n \"method\": \"delete\",\n \"status\": \"success\"\n}\n```\n\n# Main API Response\n```json\n{\n \"name\": \"Awesome Status\",\n \"description\": \"An awesome status page by Statping\",\n \"footer\": \"This is my custom footer\",\n \"domain\": \"https://demo.statping.com\",\n \"version\": \"v0.56\",\n \"migration_id\": 1536768413,\n \"created_at\": \"2018-09-12T09:06:53.905374829-07:00\",\n \"updated_at\": \"2018-09-12T09:07:01.654201225-07:00\",\n \"database\": \"sqlite\",\n \"started_on\": \"2018-09-12T10:43:07.760729349-07:00\",\n \"services\": [\n {\n \"id\": 1,\n \"name\": \"Google\",\n \"domain\": \"https://google.com\",\n \"expected\": \"\",\n \"expected_status\": 200,\n \"check_interval\": 10,\n \"type\": \"http\",\n \"method\": \"GET\",\n \"post_data\": \"\",\n \"port\": 0,\n \"timeout\": 10,\n \"order_id\": 0,\n \"created_at\": \"2018-09-12T09:06:54.97549122-07:00\",\n \"updated_at\": \"2018-09-12T09:06:54.975624103-07:00\",\n \"online\": true,\n \"latency\": 0.09080986,\n \"24_hours_online\": 0,\n \"avg_response\": \"\",\n \"status_code\": 200,\n \"last_online\": \"2018-09-12T10:44:07.931990439-07:00\",\n \"dns_lookup_time\": 0.005543935\n }\n ]\n}\n```\n\n\n

Makefile

\nHere's a simple list of Makefile commands you can run using `make`. The [Makefile](https://github.com/hunterlong/statping/blob/master/Makefile) may change often, so i'll try to keep this Wiki up-to-date.\n\n- Ubuntu `apt-get install build-essential`\n- MacOSX `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer`\n- Windows [Install Guide for GNU make utility](http://gnuwin32.sourceforge.net/packages/make.htm)\n- CentOS/RedHat `yum groupinstall \"Development Tools\"`\n\n### Commands\n```bash\nmake build # build the binary\nmake install\nmake run\nmake test\nmake coverage\nmake docs\n# Building Statping\nmake build-all\nmake build-alpine\nmake docker\nmake docker-run\nmake docker-dev\nmake docker-run-dev\nmake databases\nmake dev-deps\nmake clean\nmake compress\nmake cypress-install\nmake cypress-test\n```\n\n

Notifiers

\n

\n\n

\n\nStatping includes multiple Notifiers to alert you when your services are offline. You can also create your own notifier and send a Push Request to this repo! Creating a custom notifier is pretty easy as long as you follow the requirements. A notifier will automatically be installed into the users Statping database, and form values will save without any hassles. 💃\n\n

\nExample Code | Events | View Notifiers
\n\n

\n\n## Notifier Requirements\n- Must have a unique `METHOD` name\n- Struct must have `*notifier.Notification` pointer in it. \n- Must create and add your notifier variable in `init()`\n- Should have a form for user to input their variables/keys. `Form: []notifier.NotificationForm`\n\n## Notifier Interface (required)\nStatping has the `Notifier` interface which you'll need to include in your notifier. Statping includes many other events/triggers for your notifier, checkout Notifier Events to see all of them.\n```go\n// Notifier interface is required to create a new Notifier\ntype Notifier interface {\n\tOnSave() error // OnSave is triggered when the notifier is saved\n\tSend(interface{}) error // OnSave is triggered when the notifier is saved\n\tSelect() *Notification // Select returns the *Notification for a notifier\n}\n```\n\n### Basic Interface (required)\nInclude `OnSuccess` and `OnFailure` to receive events when a service is online or offline.\n```go\n// BasicEvents includes the most minimal events, failing and successful service triggers\ntype BasicEvents interface {\n\t// OnSuccess is triggered when a service is successful\n\tOnSuccess(*services.Service)\n\t// OnFailure is triggered when a service is failing\n\tOnFailure(*services.Service, *types.Failure)\n}\n```\n\n### Test Interface\nThe OnTest method will give the front end user the ability to test your notifier without saving, the OnTest method for your notifier run the functionality to test the user's submitted parameters and respond an error if notifier is not correctly setup.\n```go\n// Tester interface will include a function to Test users settings before saving\ntype Tester interface {\n\tOnTest() error\n}\n```\nIf your notifier includes this interface, the Test button will appear.\n\n## Notifier Struct\n```go\nvar example = &Example{¬ifier.Notification{\n\tMethod: \"example\", // unique method name\n\tHost: \"http://exmaplehost.com\", // default 'host' field\n\tForm: []notifier.NotificationForm{{\n\t\tType: \"text\", // text, password, number, or email\n\t\tTitle: \"Host\", // The title of value in form\n\t\tPlaceholder: \"Insert your Host here.\", // Optional placeholder in input\n\t\tDbField: \"host\", // An accepted DbField value (read below)\n\t}},\n}\n```\n\n## Notifier Form\nInclude a form with your notifier so other users can save API keys, username, passwords, and other values. \n```go\n// NotificationForm contains the HTML fields for each variable/input you want the notifier to accept.\ntype NotificationForm struct {\n\tType string `json:\"type\"` // the html input type (text, password, email)\n\tTitle string `json:\"title\"` // include a title for ease of use\n\tPlaceholder string `json:\"placeholder\"` // add a placeholder for the input\n\tDbField string `json:\"field\"` // true variable key for input\n\tSmallText string `json:\"small_text\"` // insert small text under a html input\n\tRequired bool `json:\"required\"` // require this input on the html form\n\tIsHidden bool `json:\"hidden\"` // hide this form element from end user\n\tIsList bool `json:\"list\"` // make this form element a comma separated list\n\tIsSwitch bool `json:\"switch\"` // make the notifier a boolean true/false switch\n}\n```\n\n### Example Notifier Form\nThis is the Slack Notifier `Form` fields.\n```go\nForm: []notifier.NotificationForm{{\n\t\tType: \"text\",\n\t\tTitle: \"Incoming webhooker Url\",\n\t\tPlaceholder: \"Insert your slack webhook URL here.\",\n\t\tSmallText: \"Incoming webhooker URL from slack Apps\",\n\t\tDbField: \"Host\",\n\t\tRequired: true,\n\t}}\n}\n```\n\n### Accepted DbField Values\nThe `notifier.NotificationForm` has a field called `DbField` which is the column to save the value into the database. Below are the acceptable DbField string names to include in your form. \n- `host` used for a URL or API endpoint\n- `username` used for a username\n- `password` used for a password\n- `port` used for a integer port number\n- `api_key` used for some kind of API key\n- `api_secret` used for some API secret\n- `var1` used for any type of string\n- `var2` used for any type of string (extra)\n\n### Form Elements\nYou can completely custom your notifications to include a detailed form. \n- `Type` is a HTML input type for your field\n- `Title` give your input element a title\n- `Placeholder` optional field if you want a placeholder in input\n- `DbField` required field to save variable into database (read above)\n- `Placeholder` optional field for inserting small hint under the input\n\n# Adding Notifiers\nTo add a notifier to the Statping application, simply append your Notifier in the `AttachNotifiers()` function inside of [core/core.go](https://github.com/hunterlong/statping/blob/master/core/core.go).\n\n```go\n// AttachNotifiers will attach all the notifier's into the system\nfunc AttachNotifiers() error {\n\treturn notifier.AddNotifiers(\n\t\tnotifiers.Command,\n\t\tnotifiers.Discorder,\n\t\tnotifiers.Emailer,\n\t\tnotifiers.LineNotify,\n\t\tnotifiers.Mobile,\n\t\tnotifiers.Slacker,\n\t\tnotifiers.Telegram,\n\t\tnotifiers.Twilio,\n\t\tnotifiers.Webhook,\n\t)\n}\n```\n###### [AttachNotifiers](https://github.com/hunterlong/statping/blob/master/core/core.go#L183)\n\n

Notifier Events

\nEvents are handled by added interfaces for the elements you want to monitor.\n\n## Required Notifier Interface\n```go\n// Notifier interface is required to create a new Notifier\ntype Notifier interface {\n\t// Run will trigger inside of the notifier when enabled\n\tRun() error\n\t// OnSave is triggered when the notifier is saved\n\tOnSave() error\n\t// Test will run a function inside the notifier to Test if it works\n\tTest() error\n\t// Select returns the *Notification for a notifier\n\tSelect() *Notification\n}\n```\n\n## Basic Success/Failure Interface\n```go\n// BasicEvents includes the most minimal events, failing and successful service triggers\ntype BasicEvents interface {\n\t// OnSuccess is triggered when a service is successful\n\tOnSuccess(*services.Service)\n\t// OnFailure is triggered when a service is failing\n\tOnFailure(*services.Service, *types.Failure)\n}\n```\n\n\n## Service Events\n```go\n// ServiceEvents are events for Services\ntype ServiceEvents interface {\n\tOnNewService(*services.Service)\n\tOnUpdatedService(*services.Service)\n\tOnDeletedService(*services.Service)\n}\n```\n\n## User Events\n```go\n// UserEvents are events for Users\ntype UserEvents interface {\n\tOnNewUser(*types.User)\n\tOnUpdatedUser(*types.User)\n\tOnDeletedUser(*types.User)\n}\n```\n\n## Core Events\n```go\n// CoreEvents are events for the main Core app\ntype CoreEvents interface {\n\tOnUpdatedCore(*types.Core)\n}\n```\n\n## Notifier Events\n```go\n// NotifierEvents are events for other Notifiers\ntype NotifierEvents interface {\n\tOnNewNotifier(*Notification)\n\tOnUpdatedNotifier(*Notification)\n}\n```\n\n

Notifier Example

\nBelow is a full example of a Statping notifier which will give you a good example of how to create your own. Insert your new notifier inside the `/notifiers` folder once your ready!\n\n```go\npackage notifiers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/hunterlong/statping/types\"\n \"github.com/hunterlong/statping/core/notifier\"\n\t\"time\"\n)\n\ntype Example struct {\n\t*notifier.Notification\n}\n\nvar example = &Example{¬ifier.Notification{\n\tMethod: METHOD,\n\tTitle: \"Example\",\n\tDescription: \"Example Notifier\",\n\tAuthor: \"Hunter Long\",\n\tAuthorUrl: \"https://github.com/hunterlong\",\n\tDelay: time.Duration(5 * time.Second),\n\tForm: []notifier.NotificationForm{{\n\t\tType: \"text\",\n\t\tTitle: \"Host\",\n\t\tPlaceholder: \"Insert your Host here.\",\n\t\tDbField: \"host\",\n\t\tSmallText: \"this is where you would put the host\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Username\",\n\t\tPlaceholder: \"Insert your Username here.\",\n\t\tDbField: \"username\",\n\t}, {\n\t\tType: \"password\",\n\t\tTitle: \"Password\",\n\t\tPlaceholder: \"Insert your Password here.\",\n\t\tDbField: \"password\",\n\t}, {\n\t\tType: \"number\",\n\t\tTitle: \"Port\",\n\t\tPlaceholder: \"Insert your Port here.\",\n\t\tDbField: \"port\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"API Key\",\n\t\tPlaceholder: \"Insert your API Key here\",\n\t\tDbField: \"api_key\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"API Secret\",\n\t\tPlaceholder: \"Insert your API Secret here\",\n\t\tDbField: \"api_secret\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Var 1\",\n\t\tPlaceholder: \"Insert your Var1 here\",\n\t\tDbField: \"var1\",\n\t}, {\n\t\tType: \"text\",\n\t\tTitle: \"Var2\",\n\t\tPlaceholder: \"Var2 goes here\",\n\t\tDbField: \"var2\",\n\t}},\n}}\n\n// REQUIRED init() will install/load the notifier\nfunc init() {\n\tnotifier.AddNotifier(example)\n}\n\n// REQUIRED - Send is where you would put the action's of your notifier\nfunc (n *Example) Send(msg interface{}) error {\n\tmessage := msg.(string)\n\tfmt.Printf(\"i received this string: %v\\n\", message)\n\treturn nil\n}\n\n// REQUIRED\nfunc (n *Example) Select() *notifier.Notification {\n\treturn n.Notification\n}\n\n// REQUIRED\nfunc (n *Example) OnSave() error {\n\tmsg := fmt.Sprintf(\"received on save trigger\")\n\tn.AddQueue(msg)\n\treturn nil\n}\n\n// REQUIRED\nfunc (n *Example) Test() error {\n\tmsg := fmt.Sprintf(\"received a test trigger\\n\")\n\tn.AddQueue(msg)\n\treturn nil\n}\n\n// REQUIRED - BASIC EVENT\nfunc (n *Example) OnSuccess(s *services.Service) {\n\tmsg := fmt.Sprintf(\"received a count trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// REQUIRED - BASIC EVENT\nfunc (n *Example) OnFailure(s *services.Service, f *types.Failure) {\n\tmsg := fmt.Sprintf(\"received a failure trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewService(s *services.Service) {\n\tmsg := fmt.Sprintf(\"received a new service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedService(s *services.Service) {\n\tmsg := fmt.Sprintf(\"received a update service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnDeletedService(s *services.Service) {\n\tmsg := fmt.Sprintf(\"received a delete service trigger for service: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a new user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a updated user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnDeletedUser(s *types.User) {\n\tmsg := fmt.Sprintf(\"received a deleted user trigger for user: %v\\n\", s.Username)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedCore(s *types.Core) {\n\tmsg := fmt.Sprintf(\"received a updated core trigger for core: %v\\n\", s.Name)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnNewNotifier(s *Notification) {\n\tmsg := fmt.Sprintf(\"received a new notifier trigger for notifier: %v\\n\", s.Method)\n\tn.AddQueue(msg)\n}\n\n// OPTIONAL\nfunc (n *Example) OnUpdatedNotifier(s *Notification) {\n\tmsg := fmt.Sprintf(\"received a update notifier trigger for notifier: %v\\n\", s.Method)\n\tn.AddQueue(msg)\n}\n```\n\n\n

Prometheus Exporter

\nStatping includes a prometheus exporter so you can have even more monitoring power with your services. The prometheus exporter can be seen on `/metrics`, simply create another exporter in your prometheus config. Use your Statping API Secret for the Authorization Bearer header, the `/metrics` URL is dedicated for Prometheus and requires the correct API Secret has `Authorization` header.\n\n# Grafana Dashboard\nStatping has a [Grafana Dashboard](https://grafana.com/dashboards/6950) that you can quickly implement if you've added your Statping service to Prometheus. Import Dashboard ID: `6950` into your Grafana dashboard and watch the metrics come in!\n\n

\n\n## Basic Prometheus Exporter\nIf you have Statping and the Prometheus server in the same Docker network, you can use the yaml config below.\n```yaml\nscrape_configs:\n - job_name: 'statping'\n scrape_interval: 30s\n bearer_token: 'SECRET API KEY HERE'\n static_configs:\n - targets: ['statping:8080']\n```\n\n## Remote URL Prometheus Exporter\nThis exporter yaml below has `scheme: https`, which you can remove if you arn't using HTTPS.\n```yaml\nscrape_configs:\n - job_name: 'statping'\n scheme: https\n scrape_interval: 30s\n bearer_token: 'SECRET API KEY HERE'\n static_configs:\n - targets: ['status.mydomain.com']\n```\n\n### `/metrics` Output\n```\nstatping_total_failures 206\nstatping_total_services 4\nstatping_service_failures{id=\"1\" name=\"Google\"} 0\nstatping_service_latency{id=\"1\" name=\"Google\"} 12\nstatping_service_online{id=\"1\" name=\"Google\"} 1\nstatping_service_status_code{id=\"1\" name=\"Google\"} 200\nstatping_service_response_length{id=\"1\" name=\"Google\"} 10777\nstatping_service_failures{id=\"2\" name=\"Statping.com\"} 0\nstatping_service_latency{id=\"2\" name=\"Statping.com\"} 3\nstatping_service_online{id=\"2\" name=\"Statping.com\"} 1\nstatping_service_status_code{id=\"2\" name=\"Statping.com\"} 200\nstatping_service_response_length{id=\"2\" name=\"Statping.com\"} 2\n```\n\n

SSL

\nYou can run Statping with a valid certificate by including 2 files in the root directory. Although, I personally recommend using NGINX or Apache to serve the SSL and then have the webserver direct traffic to the Statping instance. This guide will show you how to implement SSL onto your Statping server with multiple options.\n\n## SSL Certificate with Statping\nTo run the Statping HTTP server in SSL mode, you must include 2 files in the root directory of your Statping application. The 2 files you must include are:\n- `server.crt` SSL Certificate File\n- `server.key` SSL Certificate Key File\n\nThe filenames and extensions must match the exact naming above. If these 2 files are found, Statping will automatically start the HTTP server in SSL mode using your certificates. You can also generate your own SSL certificates, but you will receive a \"ERR_CERT_AUTHORITY_INVALID\" error. To generate your own, follow the commands below:\n\n```shell\nopenssl req -new -sha256 -key server.key -out server.csr\nopenssl x509 -req -sha256 -in server.csr -signkey server.key -out server.crt -days 3650\n```\nThis will generate a self signed certificate that you can use for your Statup instance. I recommend using a web server to do SSL termination for your server though.\n\n## Choose a Web Server or Environment\n\n**Choose the environment running the Statping instance.**\n- [Docker](#docker)\n- [NGINX](#nginx)\n- [Apache](#apache)\n\n## Docker\nDocker might be the easiest way to get up and running with a SSL certificate. Below is a `docker-compose.yml` file that will run NGINX, LetEncrypt, and Statping.\n\n1. Point your domain or subdomain to the IP address of the Docker server. This would be done on CloudFlare, Route53, or some other DNS provider.\n\n2. Replace the `docker-compose.yml` contents:\n- `MY.DOMAIN.COM` with the domain you want to use\n- `MY@EMAIL.COM` with your email address\n\n3. Run the docker container by running command `docker-compose up -d`. Give a little bit of time for LetEncrypt to automatically generate your SSL certificate.\n\n###### `docker-compose.yml`\n```yaml\nversion: '2.3'\nservices:\n nginx:\n container_name: nginx\n image: jwilder/nginx-proxy\n ports:\n - 0.0.0.0:80:80\n - 0.0.0.0:443:443\n labels:\n - \"com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy\"\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/tmp/docker.sock:ro\n - ./statping/nginx/certs:/etc/nginx/certs:ro\n - ./statping/nginx/vhost:/etc/nginx/vhost.d\n - ./statping/nginx/html:/usr/share/nginx/html:ro\n - ./statping/nginx/dhparam:/etc/nginx/dhparam\n environment:\n DEFAULT_HOST: MY.DOMAIN.COM\n\n letsencrypt:\n container_name: letsencrypt\n image: jrcs/letsencrypt-nginx-proxy-companion\n networks:\n - internet\n restart: always\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - ./statping/nginx/certs:/etc/nginx/certs\n - ./statping/nginx/vhost:/etc/nginx/vhost.d\n - ./statping/nginx/html:/usr/share/nginx/html\n - ./statping/nginx/dhparam:/etc/nginx/dhparam\n\n statping:\n container_name: statping\n image: hunterlong/statping:latest\n restart: always\n networks:\n - internet\n depends_on:\n - nginx\n volumes:\n - ./statping/app:/app\n environment:\n VIRTUAL_HOST: MY.DOMAIN.COM\n VIRTUAL_PORT: 8080\n LETSENCRYPT_HOST: MY.DOMAIN.COM\n LETSENCRYPT_EMAIL: MY@EMAIL.COM\n\nnetworks:\n internet:\n driver: bridge\n```\n\n## NGINX\nIf you already have a NGINX web server running, you just have to add a proxy pass and your SSL certs to the nginx config or as a vhost. By default Statping runs on port 8080, you can change this port by starting server with `statping -ip 127.0.0.1 -port 9595`.\n\n- Replace `/my/absolute/directory/for/cert/server.crt` with SSL certificate file.\n- Replace `/my/absolute/directory/for/key/server.key` with SSL key file.\n- Run `service nginx restart` and try out https on your domain.\n\n##### Tutorials\n- [NGINX Guide](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-http/)\n- [How To Set Up Nginx Load Balancing with SSL Termination](https://www.digitalocean.com/community/tutorials/how-to-set-up-nginx-load-balancing-with-ssl-termination)\n\n###### `/etc/nginx/nginx.conf`\n```\n#user nobody;\nworker_processes 1;\nevents {\n worker_connections 1024;\n}\nhttp {\n include mime.types;\n default_type application/octet-stream;\n send_timeout 1800;\n sendfile on;\n keepalive_timeout 6500;\n server {\n listen 80;\n server_name localhost;\n location / {\n proxy_pass http://localhost:8080;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Client-Verify SUCCESS;\n proxy_set_header X-Client-DN $ssl_client_s_dn;\n proxy_set_header X-SSL-Subject $ssl_client_s_dn;\n proxy_set_header X-SSL-Issuer $ssl_client_i_dn;\n proxy_read_timeout 1800;\n proxy_connect_timeout 1800;\n }\n }\n # HTTPS server\n \n server {\n listen 443;\n server_name localhost;\n \n ssl on;\n ssl_certificate /my/absolute/directory/for/cert/server.crt;\n ssl_certificate_key /my/absolute/directory/for/key/server.key;\n ssl_session_timeout 5m;\n \n ssl_protocols SSLv2 SSLv3 TLSv1;\n ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;\n ssl_prefer_server_ciphers on;\n \n location / {\n proxy_pass http://localhost:8080;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Client-Verify SUCCESS;\n proxy_set_header X-Client-DN $ssl_client_s_dn;\n proxy_set_header X-SSL-Subject $ssl_client_s_dn;\n proxy_set_header X-SSL-Issuer $ssl_client_i_dn;\n proxy_read_timeout 1800;\n proxy_connect_timeout 1800;\n }\n }\n}\n```\n\n## Apache\n\n

Config with .env File

\nIt may be useful to load your environment using a `.env` file in the root directory of your Statping server. The .env file will be automatically loaded on startup and will overwrite all values you have in config.yml.\n\nIf you have the `DB_CONN` environment variable set Statping will bypass all values in config.yml and will require you to have the other DB_* variables in place. You can pass in these environment variables without requiring a .env file.\n\n## `.env` File\n```bash\nDB_CONN=postgres\nDB_HOST=0.0.0.0\nDB_PORT=5432\nDB_USER=root\nDB_PASS=password123\nDB_DATABASE=root\n\nNAME=Demo\nDESCRIPTION=This is an awesome page\nDOMAIN=https://domain.com\nADMIN_USER=admin\nADMIN_PASSWORD=admin\nADMIN_EMAIL=info@admin.com\nUSE_CDN=true\nPOSTGRES_SSLMODE=false # enable ssl_mode for postgres (To enable use require)\nDISABLE_LOGS=false # disable logs from appearing and writing to disk\n\nIS_DOCKER=false\nIS_AWS=false\nSASS=/usr/local/bin/sass\nCMD_FILE=/bin/bash\n```\nThis .env file will include additional variables in the future, subscribe to this repo to keep up-to-date with changes and updates. \n\n

Static Export

\nIf you want to use Statping as a CLI application without running a server, you can export your status page to a static HTML.\nThis export tool is very useful for people who want to export their HTML and upload/commit it to Github Pages or an FTP server.\n```dash\nstatup export\n```\n###### Creates `index.html` in the current directory with CDN asset URL's. 💃 \n\n

Statping Plugins

\nSince Statping is built in Go Language we can use the [Go Plugin](https://golang.org/pkg/plugin/) feature to create dynamic plugins that run on load. Statping has an event anytime anything happens, you can create your own plugins and do any type of function. To implement your own ideas into Statping, use the plugin using the [statup/plugin](https://github.com/hunterlong/statping/blob/master/plugin/main.go) package.\n```\ngo get github.com/hunterlong/statping/plugin\n```\n\n## Example Plugin\nStart off with the [Example Statping Plugin](https://github.com/hunterlong/statping_plugin) that includes all the interfaces and some custom options for you to expand on. You can include any type of function in your own plugin!\n\n

\n\n

\n\n## Building Plugins\nPlugins don't need a push request and they can be private! You'll need to compile your plugin to the Golang `.so` binary format. Once you've built your plugin, insert it into the `plugins` folder in your Statping directory and reboot the application. Clone the [Example Statping Plugin](https://github.com/hunterlong/statping_plugin) repo and try to build it yourself!\n\n#### Build Requirements\n- You must have `main.go`\n- You must create the Plugin variable on `init()`\n\n```bash\ngit clone https://github.com/hunterlong/statping_plugin\ncd statup-plugin\ngo build -buildmode=plugin -o example.so\n```\n###### Insert `example.so` into the `plugins` directory and reload Statping\n\n## Testing Statping Plugins\nStatping includes a couple tools to help you on your Plugin journey, you can use `statup test plugins` command to test all plugins in your `/plugins` folder. This test will attempt to parse your plugin details, and then it will send events for your plugin to be fired.\n```\nstatup test plugins\n```\n

\n\n

\n\nYour plugin should be able to parse and receive events before distributing it. The test tools creates a temporary database (SQLite) that your plugin can interact with. Statping uses [upper.io/db.v3](https://upper.io/db.v3) for database interactions. The database is passed to your plugin `OnLoad(db sqlbuilder.Database)`, so you can use the `db` variable passed here.\n\n## Statping Plugin Interface\nPlease remember Golang plugin's are very new and Statping plugin package may change and 'could' brake your plugin. Checkout the [statup/plugin package](https://github.com/hunterlong/statping/blob/master/plugin/main.go) to see the most current interfaces.\n```go\ntype PluginActions interface {\n\tGetInfo() Info\n\tGetForm() string\n\tSetInfo(map[string]interface{}) Info\n\tRoutes() []Routing\n\tOnSave(map[string]interface{})\n\tOnFailure(map[string]interface{})\n\tOnSuccess(map[string]interface{})\n\tOnSettingsSaved(map[string]interface{})\n\tOnNewUser(map[string]interface{})\n\tOnNewService(map[string]interface{})\n\tOnUpdatedService(map[string]interface{})\n\tOnDeletedService(map[string]interface{})\n\tOnInstall(map[string]interface{})\n\tOnUninstall(map[string]interface{})\n\tOnBeforeRequest(map[string]interface{})\n\tOnAfterRequest(map[string]interface{})\n\tOnShutdown()\n\tOnLoad(sqlbuilder.Database)\n}\n```\n\n## Event Parameters\nAll event interfaces for the Statping Plugin will return a `map[string]interface{}` type, this is because the plugin package will most likely update and change in the future, but using this type will allow your plugin to continue even after updates.\n\n## Example of an Event\nKnowing what happens during an event is important for your plugin. For example, lets have an event that echo something when a service has a Failure status being issued. Checkout some example below to see how this golang plugin action works. \n\n```go\nfunc (p pkg) OnSuccess(data map[string]interface{}) {\n fmt.Println(\"Statping Example Plugin received a successful service hit! \")\n fmt.Println(\"Name: \", data[\"Name\"])\n fmt.Println(\"Domain: \", data[\"Domain\"])\n fmt.Println(\"Method: \", data[\"Method\"])\n fmt.Println(\"Latency: \", data[\"Latency\"])\n}\n```\n###### OnSuccess is fired every time a service has check it be online\n\n```go\nfunc OnFailure(service map[string]interface{}) {\n fmt.Println(\"oh no! an event is failing right now! do something!\")\n fmt.Println(service)\n}\n```\n###### OnFailure is fired every time a service is failing\n\n```go\nfunc (p pkg) OnLoad(db sqlbuilder.Database) {\n fmt.Println(\"=============================================================\")\n fmt.Printf(\" Statping Example Plugin Loaded using %v database\\n\", db.Name())\n fmt.Println(\"=============================================================\")\n}\n```\n###### OnLoad is fired after plugin is loaded into the environment\n\n\n## Interacting with Database\nThe Example Statping Plugin includes a variable `Database` that will allow you to interact with the Statping database. Checkout [database.go](https://github.com/hunterlong/statping_plugin/blob/master/database.go) to see a full example of Create, Read, Update and then Deleting a custom Communication entry into the database.\n```go\n// Insert a new communication into database\n// once inserted, return the Communication\nfunc (c *Communication) Create() *Communication {\n\tuuid, err := CommunicationTable().Insert(c)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tc.Id = uuid.(int64)\n\treturn c\n}\n```\n\n## Custom HTTP Routes\nPlugin's can include their own HTTP route to accept GET/POST requests. Route are loaded after Statping loads all of it's Routes. Checkout [routes.go](https://github.com/hunterlong/statping_plugin/blob/master/routes.go) on the example plugin to see a full example of how to use it.\n```go\n// You must have a Routes() method in your plugin\nfunc (p *pkg) Routes() []plugin.Routing {\n\treturn []plugin.Routing{{\n\t\tURL: \"hello\",\n\t\tMethod: \"GET\",\n\t\tHandler: CustomInfoHandler,\n\t}}\n}\n\n// This is the HTTP handler for the '/hello' URL created above\nfunc CustomInfoHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tfmt.Fprintln(w, \"Oh Wow!!! This is cool...\")\n}\n```\n\n\n## Plugin To-Do List\n- [ ] Ability to includes assets like jpg, json, etc\n\n

Statuper

\nStatping includes a simple to use installation shell script that will help you install locally, Docker, and even onto a AWS EC2 instance.\n\n

\n\n

\n\n## Installation\n```bash\ncurl -O https://assets.statup.io/statuper && chmod +x statuper\n```\n\n## Usage\n- `statuper`\n\n

Build and Test

\nBuilding from the Go Language source code is pretty easy if you already have Go installed. Clone this repo and `cd` into it. \n\n### Git n' Go Get\n```bash\ngit clone https://github.com/hunterlong/statping.git\ncd statup\ngo get -v\n```\n\n### Install go.rice\nStatping uses go.rice to compile HTML, JS, and CSS files into it's single binary output.\n```\ngo get github.com/GeertJohan/go.rice\ngo get github.com/GeertJohan/go.rice/rice\n```\n\n### Build Statping Binary\nStatping uses go.rice to compile HTML, JS, and CSS files into it's single binary output.\n```\nrice embed-go\ngo build -o statup .\n./statup version\n```\n\n### Test Coverage\nYou can also test Statio on your localhost, but it does require a MySQL, and Postgres server to be accessible since testing does create/drop tables for multiple databases. \n```\ngo test -v\n```\n\n

Contributing

\nHave a feature you want to implement into Statping!? Awesome! Follow this guide to see how you can test, compile and build Statping for production use. I recommend you use `make` with this process, it will save you time and it will auto include many customized parameters to get everything working correctly.\n\n# Dependencies\nStatping has a couple of required dependencies when testing and compiling the binary. The [Makefile](https://github.com/hunterlong/statping/blob/master/Makefile) will make these tasks a lot easier. Take a look at the Makefile to see what commands are ran. Run the command below to get setup right away.\n```bash\nmake dev-deps\n```\nList of requirements for compiling assets, building binary, and testing.\n- [Go Language](https://golang.org/) (currently `1.10.3`)\n- [Docker](https://docs.docker.com/)\n- [SASS](https://sass-lang.com/install)\n- [Cypress](https://www.cypress.io/) (only used for UI testing, `make cypress-install`)\n\n# Compiling Assets\nThis Golang project uses [rice](https://github.com/GeertJohan/go.rice) to compile static assets into a single file. The file `source/rice-box.go` is never committed to the Github repo, it is automatically created on build. Statping also requires `sass` to be installed on your local OS. To compile all the static assets run the command below:\n\n```bash\nmake compile\n```\nAfter this is complete, you'll notice the `source/rice-box.go` file has been generated. You can now continue to build, and test.\n\n# Testing\nStatping includes multiple ways to Test the application, you can run the `make` command, or the normal `go test` command. To see the full experience of your updates, you can even run Cypress tests which is in the `.dev/test` folder.\n\nStatping will run all tests in `cmd` folder on MySQL, Postgres, and SQLite databases. You can run `make databases` to automatically create MySQL and Postgres with Docker.\n\n###### Go Unit Testing:\n```bash\nmake test\n```\n\n###### Cypress UI Testing:\n```bash\nmake cypress-test\n```\n\n###### Test Everything:\n```bash\nmake test-all\n```\n\n# Build\nStatping will build on all operating systems except Windows 32-bit. I personally use [xgo](https://github.com/karalabe/xgo) to cross-compile on multiple systems using Docker. Follow the commands below to build on your local system.\n\n###### Build for local operating system:\n```bash\nmake build\n```\n\n# Compile for Production\nOnce you've tested and built locally, you can compile Statping for all available operating systems using the command below. This command will require you to have Docker.\n\n```bash\nmake build-all\n```\n\n# What Now\nEverything tested, compiled and worked out!? Awesome! 💃 You can now commit your changes, and submit a Pull Request with the features/bugs you added or removed.\n\n\n\n\n\n

PGP Signature

\nYou can check if the Statping binary you downloaded is authentic by running a few commands.\n\n### Steps to Authenticate\n1. Download the Statping `tar.gz` file from [Latest Releases](https://github.com/hunterlong/statping/releases/latest) and extract the `statping` binary and the `statup.asc` file.\n2. Run command: `gpg --verify statping.asc`\n3. You should see `Good signature from \"Hunter Long \" [ultimate]`.\n\n# Statping Public Key\n- [https://statping.com/statping.gpg](https://statping.com/statping.gpg)\n\nYou can also download the key with the command below:\n```\nwget https://statping.com/statping.gpg\n```\n\n###### `statping.gpg`\n```\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFwGUYIBEADNsDY4aUOx8EoZuTRFPtjuadJzFRyKtHhw/tLlAnoFACanZPIT\nNZoRYvRR5v6lMDXdxsteDbJEOhZ1WDiKIr4OyMahPsyyH6ULzSBKgePUswa0sDef\nUnXYzPFQCzqQyQQFbp9AYfDP7dW6dTL9I6qU2NqlJvjxJiiZTAq87SmsLqHiASnI\n+ottnQuu6vJQBJz2PFIuaS1c3js/+HBbth9GK5B9YN1BIIyZoFmWKVU9HnJf+aM3\nUs6OLjjwYwWzQH38ZV84IjVXyiP9PQVhlCXeHK7XdhPZvnSP1m5Wszj/jowwY6Mz\nLgLotfL540X7yOJ7hJTFYLFBOtJdJr/3Ov8SH4HXdPFPVG+UqxsmtmPqUQ9iAxAE\njRFfkAxBvH5Szf2WZdaLnlrrOcOKJIIjZgHqalquBTAhlh5ul0lUVSSPxetwIBlW\n60L41k94NJFGDt8xOJ+122mLcywmQ1CzhDfeIKlxl6JDiVHjoRqlQQrqIoNZMV85\nrzGfrmbuwv1MXGBJoiNy3330ujOBmhQ9dQVwKpxhBKdjnAgIGM9szbUYxIkGgM1O\nU4b1WF3AF/9JOpKJ0LewslpM3BFFYnemGsHXAv3TBPqKidNdwMAiBOtNykGoXF6i\n0D6jOW/IB1da0gUA+kr5JdAOwIG7JXKhur2MO7Ncid59DL2N8RePRWj+jwARAQAB\ntB9IdW50ZXIgTG9uZyA8aW5mb0BzdGF0cGluZy5jb20+iQJOBBMBCAA4FiEEt21h\n+qbbdZRm6D2ZZLnGquLVUngFAlwGUYICGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\nF4AACgkQZLnGquLVUnizwA//c7vmwTMq/8LYlbo37WM2kDE9AKIrz6VSMq4RhGbC\nLikH0X0epa+if79n9BZrVU/Af3aKTn7vu2J4XrvzcdCXtcsR0YmCWML2Y6OSFmhX\nw3o6woiFcp+SUWdcM/kithRun+j9sKV4akdgkdBQUdh/RMVln+radz1c6G59iTdh\nS+Ip3ObO7Gn5VnrLwxix+W9Jhg8YhDgDGEDt8e1yvjuMRY+WhjHFlwEMoE0kvQL8\nQvQH2dGD3dExWAuIL7+0xC0ZGU0PR8vRrq1ukdIsWlDY+42vvhcyPZKFFDTM/QLF\nFcCNiPSGhiK/NQq67xnRMFdh0fnqbydWj2atMpacIrheEkOt8db2/UMyDOwlIxgy\nKOG8x+yNKiG9LyvW4axRLctN608/+TbvtFo5TVOFJYxJQp4b5uz7LgJAJw7PBvfC\nbqx64BH8WGzgyGcAl9unQEtpDuxXoKvP2kbsS7hjvhK0gJgW9llpV4sRJJGApTBc\nWtbcS9DBGs3k1aZdA72bxnayD32syVz7czl4+tkRsbQZ4VgJh1yrHIDsdWQXFnYu\nEQJfCgX5HvvC13MpDUth0NWCFtWQirY3EFbIgSuhB/D5iXA+Dt1Dq5c1u7wQlUVi\nLQCU++oMGrlU3gZrnov5lnBGCEjn0O9bKQm8zmLdEcENFxUZvfPjOIY64YprZxD9\nBv65Ag0EXAZRggEQAMmjHmnvH8SvNJhku/oI96dFKen3bg9xdaFUD1vAuNglCalH\nwgXcCZd0RdobYNG46cXTzTQadtHS4hi/UBJ+oy5ZUpIRglW12eTYtqM2G11VbcQi\nj6rLITP9NIP+G1xBICSYK4UwmH55BolMEQ/1ZX0a9rESM9stDNglheCCudbMGR/1\nZYnufdEsh0yPwyC/1upZeu8LPWK62pt9mE/gccx77QTeDi5OJcRf1fPbUTCm3vSS\nwPPV2AGANodIhostjDymt5vh0tGwc7oUZZLnVdErfuctv7yMgZdiCpYu0jFy1NYf\nJgOpZasrcK7/1ozGzsfAo/sSU4kIkMwuWGgqfx5kGRK2CgU4T0i7oI6DMpOX9ZS8\ns3+oCWu83X0ofvm5R2CbjiUj2gR6JOhBQbJpCeTkLe+SFcUpnyrr7lG8B8QZHm5N\nnBi05V/s63RE3g/6WpR/fWuh+uswe01uqlSx9deW7jT49BL/MdSxwjfwLBLz/hLM\n0ld385XAd9bqMjUtp0XhZX2YORx3f/aKY7PYA62baGibb5RdPRw6viEAWU20eb+8\nX9Pa7hGmwUeal5lka4SD/TGl7wdY+g4oYP+jtKinH/ZftWA5wHTe3jWT5bdWrT2d\ne+0qA0SBkmKIDLpktvtTa19w2nfwBIwJ6fN36ZjYpOn/stxR7aRtnhSqvzxbABEB\nAAGJAjYEGAEIACAWIQS3bWH6ptt1lGboPZlkucaq4tVSeAUCXAZRggIbDAAKCRBk\nucaq4tVSeGWmD/9Pg1x6s98zdZCQa2apmUnuoQAQA9Gf2RBBuglCDGsY67wbvdHZ\n9wdFRs2QEhl2O3oFmidxthBOBRl9z62nXliLwNn1Lcy/yDfaB8wH6gMm4jn2N/z9\nvQXnyIzg8m4PItZ1p5mnY3qH5lpGF8r9Gb7tzK10rqulM2XTDojZOevlEGI6LGw8\nFjccXtNquqGZwxzytmKF3T7UBmpmt2qock8N5iJn987m6WeYmbFNc0ii0guHfdtO\nzQcItz2ngCdyvfgQPwCAoAv72ysSGhz5KZgAXRrEdcqj6Jw3ivoEUKq1aUrYncXQ\n3zC3ED6AjWOGRzjvTZzj22IVacUZ0gqx0x/oldXLOhMB9u6nFXHKj1n9nc0XHMNi\nLp9EuvQgcNLjFZGE9sxh25u9V+OhItfT/aarYEu/Xq0IkUUcdz4GehXth1/Cq1wH\nlSUie4nCs7I7OWhqMNClqP7ywElDXsQ66MCgvf01Dh64YUVjJNnyyK0QiYlCx/JQ\nZ85hNLtVXZfYqC5BRZlVFp8I8Rs2Qos9YEgn2M22+Rj+RIeD74LZFB7Q4myRvTMB\n/P466dFI83KYhwvjBYOP3jPTrV7Ky8poEGifQp2mM294CFIPS7z0z7a8+yMzcsRP\nOluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ==\n=Xn0G\n-----END PGP PUBLIC KEY BLOCK-----\n```\n\n

Testing

\nIf you want to test your updates with the current golang testing units, you can follow the guide below to run a full test process. Each test for Statping will run in MySQL, Postgres, and SQlite to make sure all database types work correctly.\n\n## Create Docker Databases\nThe easiest way to run the tests on all 3 databases is by starting temporary databases servers with Docker. Docker is available for Linux, Mac and Windows. You can download/install it by going to the [Docker Installation](https://docs.docker.com/install/) site.\n\n```go\ndocker run -it -d \\\n -p 3306:3306 \\\n -env MYSQL_ROOT_PASSWORD=password123 \\\n -env MYSQL_DATABASE=root mysql\n```\n\n```go\ndocker run -it -d \\\n -p 5432:5432 \\\n -env POSTGRES_PASSWORD=password123 \\\n -env POSTGRES_USER=root \\\n -env POSTGRES_DB=root postgres\n```\n\nOnce you have MySQL and Postgres running, you can begin the testing. SQLite database will automatically create a `statup.db` file and will delete after testing.\n\n## Run Tests\nInsert the database environment variables to auto connect the the databases and run the normal test command: `go test -v`. You'll see a verbose output of each test. If all tests pass, make a push request! 💃\n```go\nDB_DATABASE=root \\\n DB_USER=root \\\n DB_PASS=password123 \\\n DB_HOST=localhost \\\n go test -v\n```\n\n

Deployment

\nStatping is a pretty cool server for monitoring your services. The way we deploy might be a little cooler though. Statping is using the most bleeding edge technology to release updates and distribute binary files automatically.\n\n1. Source code commits get pushed to Github\n2. [Rice](https://github.com/GeertJohan/go.rice) will compile all the static assets into 1 file (rice-box.go in source)\n3. SASS will generate a compiled version of the CSS. \n4. Statping Help page is generated by cloning the Wiki repo using `go generate`.\n5. Travis-CI tests the Golang application.\n6. Travis-CI tests the Statping API using [Postman](https://github.com/hunterlong/statping/blob/master/source/tmpl/postman.json).\n7. If all tests are successful, Travis-CI will compile the binaries using [xgo](https://github.com/karalabe/xgo).\n8. Binaries are code signed using the official [PGP key](https://github.com/hunterlong/statping/wiki/PGP-Signature) and compressed.\n9. [Docker](https://cloud.docker.com/repository/docker/hunterlong/statping/builds) receives a trigger to build for the `latest` tag.\n10. Travis-CI uploads the [latest release](https://github.com/hunterlong/statping/releases) as a tagged version on Github.\n11. Travis-CI updates the [homebrew-statping](https://github.com/hunterlong/homebrew-statping) repo with the latest version.\n\nAnd that's it! Statping is ready to be shipped and installed.\n\n") diff --git a/types/checkin.go b/types/checkin.go deleted file mode 100644 index fff7e431..00000000 --- a/types/checkin.go +++ /dev/null @@ -1,88 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "time" -) - -// Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online -type Checkin struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - ServiceId int64 `gorm:"index;column:service" json:"service_id"` - Name string `gorm:"column:name" json:"name"` - Interval int64 `gorm:"column:check_interval" json:"interval"` - GracePeriod int64 `gorm:"column:grace_period" json:"grace"` - ApiKey string `gorm:"column:api_key" json:"api_key"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - Running chan bool `gorm:"-" json:"-"` - Failing bool `gorm:"-" json:"failing"` - LastHit time.Time `gorm:"-" json:"last_hit"` - Hits []*CheckinHit `gorm:"-" json:"hits"` - Failures []*Failure `gorm:"-" json:"failures"` -} - -// CheckinHit is a successful response from a Checkin -type CheckinHit struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Checkin int64 `gorm:"index;column:checkin" json:"-"` - From string `gorm:"column:from_location" json:"from"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` -} - -// BeforeCreate for checkinHit will set CreatedAt to UTC -func (c *CheckinHit) BeforeCreate() (err error) { - if c.CreatedAt.IsZero() { - c.CreatedAt = time.Now().UTC() - } - return -} - -func (s *Checkin) Expected() time.Duration { - last := s.LastHit - now := time.Now().UTC() - return time.Duration(now.Second() - last.Second()) -} - -func (s *Checkin) Period() time.Duration { - return time.Duration(s.Interval) * time.Second -} - -// Start will create a channel for the checkin checking go routine -func (s *Checkin) Start() { - s.Running = make(chan bool) -} - -// Close will stop the checkin routine -func (s *Checkin) Close() { - if s.IsRunning() { - close(s.Running) - } -} - -// IsRunning returns true if the checkin go routine is running -func (s *Checkin) IsRunning() bool { - if s.Running == nil { - return false - } - select { - case <-s.Running: - return false - default: - return true - } -} diff --git a/types/checkins/database.go b/types/checkins/database.go new file mode 100644 index 00000000..1b517eeb --- /dev/null +++ b/types/checkins/database.go @@ -0,0 +1,53 @@ +package checkins + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/utils" +) + +func DB() database.Database { + return database.DB().Model(&Checkin{}) +} + +func DBhits() database.Database { + return database.DB().Model(&CheckinHit{}) +} + +func Find(id int64) (*Checkin, error) { + var checkin *Checkin + db := DB().Where("id = ?", id).Find(&checkin) + return checkin, db.Error() +} + +func FindByAPI(key string) (*Checkin, error) { + var checkin *Checkin + db := DB().Where("api = ?", key).Find(&checkin) + return checkin, db.Error() +} + +func All() []*Checkin { + var checkins []*Checkin + DB().Find(&checkins) + return checkins +} + +func (c *Checkin) Create() error { + c.ApiKey = utils.RandomString(7) + + c.Start() + go c.CheckinRoutine() + + db := DB().Create(&c) + return db.Error() +} + +func (c *Checkin) Update() error { + db := DB().Update(&c) + return db.Error() +} + +func (c *Checkin) Delete() error { + c.Close() + db := DB().Delete(&c) + return db.Error() +} diff --git a/types/checkins/database_hits.go b/types/checkins/database_hits.go new file mode 100644 index 00000000..4cb63a5f --- /dev/null +++ b/types/checkins/database_hits.go @@ -0,0 +1,29 @@ +package checkins + +func (c *Checkin) LastHit() *CheckinHit { + var hit *CheckinHit + DBhits().Where("checkin = ?", c.Id).Last(&hit) + return hit +} + +func (c *Checkin) Hits() []*CheckinHit { + var hits []*CheckinHit + DBhits().Where("checkin = ?", c.Id).Find(&hits) + c.AllHits = hits + return hits +} + +func (c *CheckinHit) Create() error { + db := DBhits().Create(&c) + return db.Error() +} + +func (c *CheckinHit) Update() error { + db := DBhits().Update(&c) + return db.Error() +} + +func (c *CheckinHit) Delete() error { + db := DBhits().Delete(&c) + return db.Error() +} diff --git a/types/checkins/failures.go b/types/checkins/failures.go new file mode 100644 index 00000000..90f2c5a1 --- /dev/null +++ b/types/checkins/failures.go @@ -0,0 +1,23 @@ +package checkins + +import ( + "github.com/hunterlong/statping/types/failures" + "time" +) + +func (c *Checkin) CreateFailure(f *failures.Failure) error { + f.Checkin = c.Id + return failures.DB().Create(&f).Error() +} + +func (c *Checkin) FailuresColumnID() (string, int64) { + return "checkin", c.Id +} + +func (c *Checkin) Failures() failures.Failurer { + return failures.AllFailures(c) +} + +func (c *Checkin) FailuresSince(t time.Time) failures.Failurer { + return failures.FailuresSince(t, c) +} diff --git a/types/checkins/methods.go b/types/checkins/methods.go new file mode 100644 index 00000000..50fe3861 --- /dev/null +++ b/types/checkins/methods.go @@ -0,0 +1,59 @@ +package checkins + +import ( + "fmt" + "time" +) + +func (c *Checkin) Expected() time.Duration { + last := c.LastHit() + now := time.Now().UTC() + lastDir := now.Sub(last.CreatedAt) + sub := time.Duration(c.Period() - lastDir) + return sub +} + +func (c *Checkin) Period() time.Duration { + duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval)) + return duration +} + +// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response) +func (c *Checkin) Grace() time.Duration { + duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod)) + return duration +} + +// Start will create a channel for the checkin checking go routine +func (c *Checkin) Start() { + c.Running = make(chan bool) +} + +// Close will stop the checkin routine +func (c *Checkin) Close() { + if c.IsRunning() { + close(c.Running) + } +} + +// IsRunning returns true if the checkin go routine is running +func (c *Checkin) IsRunning() bool { + if c.Running == nil { + return false + } + select { + case <-c.Running: + return false + default: + return true + } +} + +// String will return a Checkin API string +func (c *Checkin) String() string { + return c.ApiKey +} + +func (c *Checkin) Link() string { + return fmt.Sprintf("%v/checkin/%v", "DOMAINHERE", c.ApiKey) +} diff --git a/types/checkins/routine.go b/types/checkins/routine.go new file mode 100644 index 00000000..9fe3d9f5 --- /dev/null +++ b/types/checkins/routine.go @@ -0,0 +1,60 @@ +package checkins + +import ( + "fmt" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/utils" + "github.com/prometheus/common/log" + "time" +) + +// RecheckCheckinFailure will check if a Service Checkin has been reported yet +func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) { + between := utils.Now().Sub(utils.Now()).Seconds() + if between > float64(c.Interval) { + fmt.Println("rechecking every 15 seconds!") + time.Sleep(15 * time.Second) + guard <- struct{}{} + c.RecheckCheckinFailure(guard) + } else { + fmt.Println("i recovered!!") + } + <-guard +} + +// Routine for checking if the last Checkin was within its interval +func (c *Checkin) CheckinRoutine() { + lastHit := c.LastHit() + if lastHit == nil { + return + } + reCheck := c.Period() +CheckinLoop: + for { + select { + case <-c.Running: + log.Infoln(fmt.Sprintf("Stopping checkin routine: %v", c.Name)) + c.Failing = false + break CheckinLoop + case <-time.After(reCheck): + log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period()))) + if c.Expected() <= 0 { + issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, lastHit.CreatedAt) + log.Errorln(issue) + + fail := &failures.Failure{ + Issue: issue, + Method: "checkin", + Service: c.ServiceId, + Checkin: c.Id, + PingTime: c.Expected().Seconds(), + CreatedAt: time.Time{}, + } + + c.CreateFailure(fail) + } + reCheck = c.Period() + } + continue + } +} diff --git a/types/checkins/samples.go b/types/checkins/samples.go new file mode 100644 index 00000000..db400404 --- /dev/null +++ b/types/checkins/samples.go @@ -0,0 +1,47 @@ +package checkins + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/utils" + "time" +) + +func (c *Checkin) Samples() []database.DbObject { + checkin1 := &Checkin{ + Name: "Example Checkin 1", + ServiceId: 1, + Interval: 300, + GracePeriod: 300, + ApiKey: utils.RandomString(7), + } + + checkin2 := &Checkin{ + Name: "Example Checkin 2", + ServiceId: 2, + Interval: 900, + GracePeriod: 300, + ApiKey: utils.RandomString(7), + } + + return []database.DbObject{checkin1, checkin2} +} + +func (c *CheckinHit) Samples() []database.DbObject { + checkTime := time.Now().UTC().Add(-24 * time.Hour) + + var hits []database.DbObject + + for i := int64(1); i <= 2; i++ { + checkHit := &CheckinHit{ + Checkin: i, + From: "192.168.0.1", + CreatedAt: checkTime.UTC(), + } + + hits = append(hits, checkHit) + + checkTime = checkTime.Add(10 * time.Minute) + } + + return hits +} diff --git a/types/checkins/struct.go b/types/checkins/struct.go new file mode 100644 index 00000000..e3ee2678 --- /dev/null +++ b/types/checkins/struct.go @@ -0,0 +1,55 @@ +package checkins + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/utils" + "time" +) + +// Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online +type Checkin struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + ServiceId int64 `gorm:"index;column:service" json:"service_id"` + Name string `gorm:"column:name" json:"name"` + Interval int64 `gorm:"column:check_interval" json:"interval"` + GracePeriod int64 `gorm:"column:grace_period" json:"grace"` + ApiKey string `gorm:"column:api_key" json:"api_key"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + Running chan bool `gorm:"-" json:"-"` + Failing bool `gorm:"-" json:"failing"` + LastHitTime time.Time `gorm:"-" json:"last_hit"` + AllHits []*CheckinHit `gorm:"-" json:"hits"` + AllFailures []*failures.Failure `gorm:"-" json:"failures"` +} + +// CheckinHit is a successful response from a Checkin +type CheckinHit struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Checkin int64 `gorm:"index;column:checkin" json:"-"` + From string `gorm:"column:from_location" json:"from"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` +} + +// BeforeCreate for checkinHit will set CreatedAt to UTC +func (c *CheckinHit) BeforeCreate() (err error) { + if c.CreatedAt.IsZero() { + c.CreatedAt = time.Now().UTC() + } + return +} + +func (c *Checkin) BeforeCreate() (err error) { + c.ApiKey = utils.RandomString(7) + if c.CreatedAt.IsZero() { + c.CreatedAt = time.Now().UTC() + c.UpdatedAt = time.Now().UTC() + } + return +} + +func (c *Checkin) BeforeDelete(tx database.Database) (err error) { + return tx.Where("id = ?", c.ServiceId). + Update("group_id", 0).Error() +} diff --git a/types/configs/configs_env.go b/types/configs/configs_env.go new file mode 100644 index 00000000..50b7987b --- /dev/null +++ b/types/configs/configs_env.go @@ -0,0 +1,76 @@ +package configs + +import ( + "github.com/hunterlong/statping/utils" + "github.com/joho/godotenv" + "github.com/pkg/errors" +) + +func loadConfigEnvs() (*DbConfig, error) { + var err error + + loadDotEnvs() + + dbConn := utils.Getenv("DB_CONN", "").(string) + dbHost := utils.Getenv("DB_HOST", "").(string) + dbUser := utils.Getenv("DB_USER", "").(string) + dbPass := utils.Getenv("DB_PASS", "").(string) + dbData := utils.Getenv("DB_DATABASE", "").(string) + dbPort := utils.Getenv("DB_PORT", defaultPort(dbConn)).(int) + name := utils.Getenv("NAME", "Statping").(string) + desc := utils.Getenv("DESCRIPTION", "Statping Monitoring Sample Data").(string) + user := utils.Getenv("ADMIN_USER", "admin").(string) + password := utils.Getenv("ADMIN_PASS", "admin").(string) + domain := utils.Getenv("DOMAIN", "").(string) + sqlFile := utils.Getenv("SQL_FILE", "").(string) + + if dbConn != "" && dbConn != "sqlite" { + if dbHost == "" { + return nil, errors.New("Missing DB_HOST environment variable") + } + if dbPort == 0 { + return nil, errors.New("Missing DB_PORT environment variable") + } + if dbUser == "" { + return nil, errors.New("Missing DB_USER environment variable") + } + if dbPass == "" { + return nil, errors.New("Missing DB_PASS environment variable") + } + if dbData == "" { + return nil, errors.New("Missing DB_DATABASE environment variable") + } + } + + config := &DbConfig{ + DbConn: dbConn, + DbHost: dbHost, + DbUser: dbUser, + DbPass: dbPass, + DbData: dbData, + DbPort: dbPort, + Project: name, + Description: desc, + Domain: domain, + Email: "", + Username: user, + Password: password, + Error: nil, + Location: utils.Directory, + SqlFile: sqlFile, + } + return config, err +} + +// loadDotEnvs attempts to load database configs from a '.env' file in root directory +func loadDotEnvs() { + err := godotenv.Overload(utils.Directory + "/" + ".env") + if err == nil { + log.Warnln("Environment file '.env' found") + envs, _ := godotenv.Read(utils.Directory + "/" + ".env") + for k, e := range envs { + log.Infof("Overwriting %s=%s\n", k, e) + } + log.Warnln("These environment variables will overwrite any existing") + } +} diff --git a/types/configs/configs_file.go b/types/configs/configs_file.go new file mode 100644 index 00000000..c278c4a9 --- /dev/null +++ b/types/configs/configs_file.go @@ -0,0 +1,26 @@ +package configs + +import ( + "github.com/go-yaml/yaml" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" +) + +func loadConfigFile(directory string) (*DbConfig, error) { + var configs *DbConfig + + log.Debugln("Attempting to read config file at: " + directory + "/config.yml") + file, err := utils.OpenFile(directory + "/config.yml") + if err != nil { + core.App.Setup = false + return nil, errors.Wrapf(err, "config.yml file not found at %s/config.yml - starting in setup mode", directory) + } + err = yaml.Unmarshal([]byte(file), &configs) + if err != nil { + return nil, errors.Wrap(err, "yaml file not formatted correctly") + } + log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml") + + return configs, nil +} diff --git a/types/configs/configs_test.go b/types/configs/configs_test.go new file mode 100644 index 00000000..85bedd48 --- /dev/null +++ b/types/configs/configs_test.go @@ -0,0 +1,50 @@ +package configs + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestDbConfig_Save(t *testing.T) { + config := &types.DbConfig{ + DbConn: "sqlite", + Project: "Tester", + Location: dir, + } + + err := SaveConfig(config) + require.Nil(t, err) + assert.Equal(t, "sqlite", config.DbConn) + assert.NotEmpty(t, config.ApiKey) + assert.NotEmpty(t, config.ApiSecret) +} + +func TestLoadDbConfig(t *testing.T) { + Configs, err := LoadConfigFile(dir) + assert.Nil(t, err) + assert.Equal(t, "sqlite", Configs.DbConn) + + configs = Configs +} + +func TestEnvToConfig(t *testing.T) { + os.Setenv("DB_CONN", "sqlite") + os.Setenv("DB_USER", "") + os.Setenv("DB_PASS", "") + os.Setenv("DB_DATABASE", "") + os.Setenv("NAME", "Testing") + os.Setenv("DOMAIN", "http://localhost:8080") + os.Setenv("DESCRIPTION", "Testing Statping") + os.Setenv("ADMIN_USER", "admin") + os.Setenv("ADMIN_PASS", "admin123") + os.Setenv("VERBOSE", "1") + config, err := EnvToConfig() + assert.Nil(t, err) + assert.Equal(t, config.DbConn, "sqlite") + assert.Equal(t, config.Domain, "http://localhost:8080") + assert.Equal(t, config.Description, "Testing Statping") + assert.Equal(t, config.Username, "admin") + assert.Equal(t, config.Password, "admin123") +} diff --git a/types/configs/connection.go b/types/configs/connection.go new file mode 100644 index 00000000..8f5948ad --- /dev/null +++ b/types/configs/connection.go @@ -0,0 +1,110 @@ +package configs + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/users" + "github.com/hunterlong/statping/utils" + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + "os" + "time" +) + +// Connect will attempt to connect to the sqlite, postgres, or mysql database +func Connect(configs *DbConfig, retry bool, location string) error { + postgresSSL := os.Getenv("POSTGRES_SSLMODE") + if database.Available() { + return nil + } + var conn string + var err error + + switch configs.DbConn { + case "sqlite", "sqlite3", "memory": + if configs.DbConn == "memory" { + conn = "sqlite3" + configs.DbConn = ":memory" + } else { + conn = findDbFile(configs) + configs.SqlFile = fmt.Sprintf("%s/%s", utils.Directory, conn) + log.Infof("SQL database file at: %s", configs.SqlFile) + configs.DbConn = "sqlite3" + } + case "mysql": + host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort) + conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", configs.DbUser, configs.DbPass, host, configs.DbData) + case "postgres": + sslMode := "disable" + if postgresSSL != "" { + sslMode = postgresSSL + } + conn = fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=%v", configs.DbHost, configs.DbPort, configs.DbUser, configs.DbData, configs.DbPass, sslMode) + case "mssql": + host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort) + conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", configs.DbUser, configs.DbPass, host, configs.DbData) + } + log.WithFields(utils.ToFields(configs, conn)).Debugln("attempting to connect to database") + + dbSession, err := database.Openw(configs.DbConn, conn) + if err != nil { + log.Debugln(fmt.Sprintf("Database connection error %s", err)) + if retry { + log.Errorln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost)) + return configs.waitForDb(configs) + } else { + return err + } + } + log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database") + + maxOpenConn := utils.Getenv("MAX_OPEN_CONN", 5) + maxIdleConn := utils.Getenv("MAX_IDLE_CONN", 5) + maxLifeConn := utils.Getenv("MAX_LIFE_CONN", 2*time.Minute) + + dbSession.DB().SetMaxOpenConns(maxOpenConn.(int)) + dbSession.DB().SetMaxIdleConns(maxIdleConn.(int)) + dbSession.DB().SetConnMaxLifetime(maxLifeConn.(time.Duration)) + + if dbSession.DB().Ping() == nil { + if utils.VerboseMode >= 4 { + database.LogMode(true).Debug().SetLogger(gorm.Logger{log}) + } + log.Infoln(fmt.Sprintf("Database %v connection was successful.", configs.DbConn)) + } + + return err +} + +// waitForDb will sleep for 5 seconds and try to connect to the database again +func (c *DbConfig) waitForDb(configs *DbConfig) error { + time.Sleep(5 * time.Second) + return c.Connect() +} + +func InitialSetup(configs *DbConfig) error { + var err error + log.Infoln(fmt.Sprintf("Core database does not exist, creating now!")) + if err := configs.DropDatabase(); err != nil { + return errors.Wrap(err, "error dropping database") + } + if err := CreateDatabase(); err != nil { + return errors.Wrap(err, "error creating database") + } + + username := utils.Getenv("ADMIN_USER", "admin").(string) + password := utils.Getenv("ADMIN_PASSWORD", "admin").(string) + + admin := &users.User{ + Username: username, + Password: utils.HashPassword(password), + Email: "info@admin.com", + Admin: null.NewNullBool(true), + } + if err := admin.Create(); err != nil { + return errors.Wrap(err, "error creating admin") + } + + return err +} diff --git a/types/configs/database.go b/types/configs/database.go new file mode 100644 index 00000000..b3e11009 --- /dev/null +++ b/types/configs/database.go @@ -0,0 +1,113 @@ +package configs + +import ( + "github.com/go-yaml/yaml" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/groups" + "github.com/hunterlong/statping/types/hits" + "github.com/hunterlong/statping/types/incidents" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/messages" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" + "github.com/hunterlong/statping/utils" + "os" +) + +type Sampler interface { + Samples() []database.DbObject +} + +func TriggerSamples() error { + return createSamples( + &services.Service{}, + &users.User{}, + &hits.Hit{}, + &failures.Failure{}, + &groups.Group{}, + &checkins.Checkin{}, + &checkins.CheckinHit{}, + &incidents.Incident{}, + &incidents.IncidentUpdate{}, + ) +} + +func createSamples(sm ...Sampler) error { + for _, v := range sm { + for _, sample := range v.Samples() { + if err := sample.Create(); err != nil { + return err + } + } + } + return nil +} + +func (d *DbConfig) Connect() error { + + return nil +} + +func (d *DbConfig) Create() error { + + return nil +} + +// Migrate function +func (d *DbConfig) Update() error { + var err error + config, err := os.Create(utils.Directory + "/config.yml") + if err != nil { + return err + } + defer config.Close() + + data, err := yaml.Marshal(d) + if err != nil { + log.Errorln(err) + return err + } + config.WriteString(string(data)) + return nil +} + +// Save will initially create the config.yml file +func (d *DbConfig) Delete() error { + return os.Remove(d.filename) +} + +// DropDatabase will DROP each table Statping created +func (d *DbConfig) DropDatabase() error { + var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, ¬ifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}, &integrations.Integration{}} + log.Infoln("Dropping Database Tables...") + for _, t := range DbModels { + if err := database.DB().DropTableIfExists(t); err != nil { + return err.Error() + } + log.Infof("Dropped table: %T\n", t) + } + return nil +} + +// CreateDatabase will CREATE TABLES for each of the Statping elements +func CreateDatabase() error { + var err error + + var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, ¬ifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}, &integrations.Integration{}} + + log.Infoln("Creating Database Tables...") + for _, table := range DbModels { + if err := database.DB().CreateTable(table); err.Error() != nil { + return err.Error() + } + } + if err := database.DB().Table("core").CreateTable(&core.Core{}); err.Error() != nil { + return err.Error() + } + log.Infoln("Statping Database Created") + return err +} diff --git a/types/configs/file.go b/types/configs/file.go new file mode 100644 index 00000000..55eb854d --- /dev/null +++ b/types/configs/file.go @@ -0,0 +1,77 @@ +package configs + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" + "os" + "path/filepath" +) + +var log = utils.Log + +func ConnectConfigs(configs *DbConfig) error { + err := Connect(configs, true, utils.Directory) + if err != nil { + return errors.Wrap(err, "error connecting to database") + } + if err := configs.Save(utils.Directory); err != nil { + return errors.Wrap(err, "error saving configuration") + } + + exists := database.DB().HasTable("core") + if !exists { + return InitialSetup(configs) + } + return nil +} + +func LoadConfigs() (*DbConfig, error) { + writeAble, err := utils.DirWritable(utils.Directory) + if err != nil { + return nil, err + } + if !writeAble { + return nil, errors.Errorf("Directory %s is not writable!", utils.Directory) + } + + dbConn := utils.Getenv("DB_CONN", "").(string) + if dbConn != "" { + configs, err := loadConfigEnvs() + if err != nil { + return loadConfigFile(utils.Directory) + } + return configs, nil + } + + return loadConfigFile(utils.Directory) +} + +func findDbFile(configs *DbConfig) string { + if configs == nil { + return findSQLin(utils.Directory) + } + if configs.SqlFile != "" { + return configs.SqlFile + } + return utils.Directory + "/" + SqliteFilename +} + +func findSQLin(path string) string { + filename := SqliteFilename + err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + if filepath.Ext(path) == ".db" { + fmt.Println("DB file is now: ", info.Name()) + filename = info.Name() + } + return nil + }) + if err != nil { + log.Error(err) + } + return filename +} diff --git a/types/configs/methods.go b/types/configs/methods.go new file mode 100644 index 00000000..2e53266b --- /dev/null +++ b/types/configs/methods.go @@ -0,0 +1,34 @@ +package configs + +import ( + "github.com/go-yaml/yaml" + "io/ioutil" + "os" +) + +// Save will initially create the config.yml file +func (d *DbConfig) Save(directory string) error { + data, err := yaml.Marshal(d) + if err != nil { + return err + } + if err := ioutil.WriteFile(directory+"/config.yml", data, os.ModePerm); err != nil { + return err + } + d.filename = directory + "/config.yml" + return nil +} + +// defaultPort accepts a database type and returns its default port +func defaultPort(db string) int { + switch db { + case "mysql": + return 3306 + case "postgres": + return 5432 + case "mssql": + return 1433 + default: + return 0 + } +} diff --git a/types/configs/migration.go b/types/configs/migration.go new file mode 100644 index 00000000..695e3ff8 --- /dev/null +++ b/types/configs/migration.go @@ -0,0 +1,76 @@ +package configs + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/groups" + "github.com/hunterlong/statping/types/hits" + "github.com/hunterlong/statping/types/incidents" + "github.com/hunterlong/statping/types/integrations" + "github.com/hunterlong/statping/types/messages" + "github.com/hunterlong/statping/types/notifications" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" +) + +// InsertNotifierDB inject the Statping database instance to the Notifier package +//func (c *DbConfig) InsertNotifierDB() error { +// if !database.Available() { +// err := c.Connect() +// if err != nil { +// return errors.New("database connection has not been created") +// } +// } +// notifiers.SetDB(database.DB()) +// return nil +//} + +// InsertIntegratorDB inject the Statping database instance to the Integrations package +//func (c *DbConfig) InsertIntegratorDB() error { +// if !database.Available() { +// err := c.Connect() +// if err != nil { +// return errors.Wrap(err,"database connection has not been created") +// } +// } +// integrations.SetDB(database.DB()) +// return nil +//} + +//MigrateDatabase will migrate the database structure to current version. +//This function will NOT remove previous records, tables or columns from the database. +//If this function has an issue, it will ROLLBACK to the previous state. +func MigrateDatabase() error { + + var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, ¬ifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}, &integrations.Integration{}} + + log.Infoln("Migrating Database Tables...") + tx := database.Begin("migration") + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if tx.Error() != nil { + log.Errorln(tx.Error()) + return tx.Error() + } + for _, table := range DbModels { + tx = tx.AutoMigrate(table) + } + if err := tx.Table("core").AutoMigrate(&core.Core{}); err.Error() != nil { + tx.Rollback() + log.Errorln(fmt.Sprintf("Statping Database could not be migrated: %v", tx.Error())) + return tx.Error() + } + + if err := tx.Commit().Error(); err != nil { + return err + } + log.Infoln("Statping Database Migrated") + + return nil +} diff --git a/types/configs/struct.go b/types/configs/struct.go new file mode 100644 index 00000000..585f6351 --- /dev/null +++ b/types/configs/struct.go @@ -0,0 +1,26 @@ +package configs + +const SqliteFilename = "statping.db" + +// DbConfig struct is used for the Db connection and creates the 'config.yml' file +type DbConfig struct { + DbConn string `yaml:"connection" json:"connection"` + DbHost string `yaml:"host" json:"-"` + DbUser string `yaml:"user" json:"-"` + DbPass string `yaml:"password" json:"-"` + DbData string `yaml:"database" json:"-"` + DbPort int `yaml:"port" json:"-"` + ApiKey string `yaml:"api_key" json:"-"` + ApiSecret string `yaml:"api_secret" json:"-"` + Project string `yaml:"-" json:"-"` + Description string `yaml:"-" json:"-"` + Domain string `yaml:"-" json:"-"` + Username string `yaml:"-" json:"-"` + Password string `yaml:"-" json:"-"` + Email string `yaml:"-" json:"-"` + Error error `yaml:"-" json:"-"` + Location string `yaml:"location" json:"-"` + SqlFile string `yaml:"sqlfile,omitempty" json:"-"` + LocalIP string `yaml:"-" json:"-"` + filename string `yaml:"-" json:"-"` +} diff --git a/types/core.go b/types/core.go deleted file mode 100644 index ddf66c98..00000000 --- a/types/core.go +++ /dev/null @@ -1,60 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "time" -) - -// SqliteFilename is the name of the SQLlite Db file -const SqliteFilename = "statping.db" - -// AllNotifiers contains all the Notifiers loaded -type AllNotifiers interface{} - -// Core struct contains all the required fields for Statping. All application settings -// will be saved into 1 row in the 'core' table. You can use the core.CoreApp -// global variable to interact with the attributes to the application, such as services. -type Core struct { - Name string `gorm:"not null;column:name" json:"name"` - Description string `gorm:"not null;column:description" json:"description,omitempty"` - ConfigFile string `gorm:"column:config" json:"-"` - ApiKey string `gorm:"column:api_key" json:"api_key" scope:"admin"` - ApiSecret string `gorm:"column:api_secret" json:"api_secret" scope:"admin"` - Style string `gorm:"not null;column:style" json:"style,omitempty"` - Footer NullString `gorm:"column:footer" json:"footer"` - Domain string `gorm:"not null;column:domain" json:"domain"` - Version string `gorm:"column:version" json:"version"` - Setup bool `gorm:"-" json:"setup"` - MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` - UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"` - Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` - LoggedIn bool `gorm:"-" json:"logged_in"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - Started time.Time `gorm:"-" json:"started_on"` - Plugins []*Info `gorm:"-" json:"-"` - Notifications []AllNotifiers `gorm:"-" json:"-"` - Integrations []Integrator `gorm:"-" json:"-"` -} - -func (Core) TableName() string { - return "core" -} - -type Servicer interface { - Model() *Service -} diff --git a/types/core/core_test.go b/types/core/core_test.go new file mode 100644 index 00000000..7b2d0951 --- /dev/null +++ b/types/core/core_test.go @@ -0,0 +1,24 @@ +package core + +import ( + "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/types/services" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +// NewCore return a new *core.Core struct +func NewCore() *Core { + core := &Core{ + Started: time.Now().UTC(), + } + core.services = make(map[int64]*services.Service) + return core +} + +func TestSelectCore(t *testing.T) { + core, err := Select() + assert.Nil(t, err) + assert.Equal(t, "Statping Sample Data", core.Name) +} diff --git a/types/core/database.go b/types/core/database.go new file mode 100644 index 00000000..6c76a1f8 --- /dev/null +++ b/types/core/database.go @@ -0,0 +1,77 @@ +package core + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" + "os" + "time" +) + +func DB() database.Database { + return database.DB().Table("core").Model(&Core{}) +} + +func Select() (*Core, error) { + // SelectCore will return the CoreApp global variable and the settings/configs for Statping + if !database.Available() { + return nil, errors.New("database has not been initiated yet.") + } + exists := database.DB().HasTable("core") + if !exists { + return nil, errors.New("core database has not been setup yet.") + } + db := DB().First(&App) + if db.Error() != nil { + return nil, db.Error() + } + App.UseCdn = null.NewNullBool(os.Getenv("USE_CDN") == "true") + return App, db.Error() + +} + +func (c *Core) Create() error { + //apiKey := utils.Getenv("API_KEY", utils.NewSHA1Hash(40)) + //apiSecret := utils.Getenv("API_SECRET", utils.NewSHA1Hash(40)) + newCore := &Core{ + Name: c.Name, + Description: c.Description, + ConfigFile: utils.Directory + "/config.yml", + ApiKey: c.ApiKey, + ApiSecret: c.ApiSecret, + Domain: c.Domain, + MigrationId: time.Now().Unix(), + } + db := DB().FirstOrCreate(&newCore) + return db.Error() +} + +func (c *Core) Update() error { + db := DB().Update(&c) + return db.Error() +} + +func (c *Core) Delete() error { + return nil +} + +func Sample() error { + apiKey := utils.Getenv("API_KEY", "samplekey") + apiSecret := utils.Getenv("API_SECRET", "samplesecret") + + core := &Core{ + Name: "Statping Sample Data", + Description: "This data is only used to testing", + ApiKey: apiKey.(string), + ApiSecret: apiSecret.(string), + Domain: "http://localhost:8080", + Version: "test", + CreatedAt: time.Now().UTC(), + UseCdn: null.NewNullBool(false), + Footer: null.NewNullString(""), + } + + db := database.DB().Create(&core) + return db.Error() +} diff --git a/types/core/init.go b/types/core/init.go new file mode 100644 index 00000000..86b9c668 --- /dev/null +++ b/types/core/init.go @@ -0,0 +1,36 @@ +package core + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/notifiers" + "github.com/hunterlong/statping/types/services" +) + +func InitApp() error { + if _, err := Select(); err != nil { + return err + } + //if err := InsertNotifierDB(); err != nil { + // return err + //} + //if err := InsertIntegratorDB(); err != nil { + // return err + //} + if _, err := services.SelectAllServices(true); err != nil { + return err + } + if err := notifiers.AttachNotifiers(); err != nil { + return err + } + //App.Notifications = notifications.AllCommunications + //if err := integrations.AddIntegrations(); err != nil { + // return err + //} + //App.Integrations = integrations.Integrations + + go services.CheckServices() + + database.StartMaintenceRoutine() + App.Setup = true + return nil +} diff --git a/types/core/samples.go b/types/core/samples.go new file mode 100644 index 00000000..89dd3af5 --- /dev/null +++ b/types/core/samples.go @@ -0,0 +1,28 @@ +package core + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/utils" + "time" +) + +func (c *Core) Samples() []database.DbObject { + apiKey := utils.Getenv("API_KEY", "samplekey") + apiSecret := utils.Getenv("API_SECRET", "samplesecret") + + core := &Core{ + Name: "Statping Sample Data", + Description: "This data is only used to testing", + ApiKey: apiKey.(string), + ApiSecret: apiSecret.(string), + Domain: "http://localhost:8080", + Version: "test", + CreatedAt: time.Now().UTC(), + UseCdn: null.NewNullBool(false), + Footer: null.NewNullString(""), + } + + return []database.DbObject{core} + +} diff --git a/types/core/struct.go b/types/core/struct.go new file mode 100644 index 00000000..548ca609 --- /dev/null +++ b/types/core/struct.go @@ -0,0 +1,44 @@ +package core + +import ( + "github.com/hunterlong/statping/types/null" + "time" +) + +var ( + App *Core +) + +// Core struct contains all the required fields for Statping. All application settings +// will be saved into 1 row in the 'core' table. You can use the core.CoreApp +// global variable to interact with the attributes to the application, such as services. +type Core struct { + Name string `gorm:"not null;column:name" json:"name"` + Description string `gorm:"not null;column:description" json:"description,omitempty"` + ConfigFile string `gorm:"column:config" json:"-"` + ApiKey string `gorm:"column:api_key" json:"api_key" scope:"admin"` + ApiSecret string `gorm:"column:api_secret" json:"api_secret" scope:"admin"` + Style string `gorm:"not null;column:style" json:"style,omitempty"` + Footer null.NullString `gorm:"column:footer" json:"footer"` + Domain string `gorm:"not null;column:domain" json:"domain"` + Version string `gorm:"column:version" json:"version"` + Setup bool `gorm:"-" json:"setup"` + MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` + UseCdn null.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"` + Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` + LoggedIn bool `gorm:"-" json:"logged_in"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + Started time.Time `gorm:"-" json:"started_on"` + Notifications []AllNotifiers `gorm:"-" json:"-"` + Integrations []Integrator `gorm:"-" json:"-"` +} + +// AllNotifiers contains all the Notifiers loaded +type AllNotifiers interface{} + +type Integrator interface{} + +func (Core) TableName() string { + return "core" +} diff --git a/types/failures/database.go b/types/failures/database.go new file mode 100644 index 00000000..2f1cb307 --- /dev/null +++ b/types/failures/database.go @@ -0,0 +1,34 @@ +package failures + +import "github.com/hunterlong/statping/database" + +func DB() database.Database { + return database.DB().Model(&Failure{}) +} + +func Find(id int64) (*Failure, error) { + var failure *Failure + db := DB().Where("id = ?", id).Find(&failure) + return failure, db.Error() +} + +func All() []*Failure { + var failures []*Failure + DB().Find(&failures) + return failures +} + +func (f *Failure) Create() error { + db := DB().Create(&f) + return db.Error() +} + +func (f *Failure) Update() error { + db := DB().Update(&f) + return db.Error() +} + +func (f *Failure) Delete() error { + db := DB().Delete(&f) + return db.Error() +} diff --git a/types/failures/interface.go b/types/failures/interface.go new file mode 100644 index 00000000..639104b6 --- /dev/null +++ b/types/failures/interface.go @@ -0,0 +1,54 @@ +package failures + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "time" +) + +type ColumnIDInterfacer interface { + FailuresColumnID() (string, int64) +} + +type Failurer struct { + db database.Database +} + +func (f Failurer) Db() database.Database { + return f.db +} + +func (f Failurer) List() []*Failure { + var fails []*Failure + f.db.Find(&fails) + return fails +} + +func (f Failurer) Count() int { + var amount int + f.db.Count(&amount) + return amount +} + +func (f Failurer) Last(amount int) []*Failure { + var fails []*Failure + f.db.Limit(amount).Find(&fails) + return fails +} + +func (f Failurer) Since(t time.Time) []*Failure { + var fails []*Failure + f.db.Since(t).Find(&fails) + return fails +} + +func AllFailures(obj ColumnIDInterfacer) Failurer { + column, id := obj.FailuresColumnID() + return Failurer{DB().Where(fmt.Sprintf("%s = ?", column), id)} +} + +func FailuresSince(t time.Time, obj ColumnIDInterfacer) Failurer { + column, id := obj.FailuresColumnID() + timestamp := DB().FormatTime(t) + return Failurer{DB().Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)} +} diff --git a/types/failures/samples.go b/types/failures/samples.go new file mode 100644 index 00000000..9815da69 --- /dev/null +++ b/types/failures/samples.go @@ -0,0 +1,31 @@ +package failures + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types" + "github.com/prometheus/common/log" + "time" +) + +func (f *Failure) Samples() []database.DbObject { + + createdAt := time.Now().Add(-1 * types.Month) + + for i := int64(1); i <= 5; i++ { + log.Infoln(fmt.Sprintf("Adding %v Failure records to service", 5500)) + + for fi := 1; fi <= 5500; fi++ { + createdAt = createdAt.Add(2 * time.Minute) + + failure := &Failure{ + Service: i, + Issue: "testing right here", + CreatedAt: createdAt, + } + + failure.Create() + } + } + return nil +} diff --git a/types/failure.go b/types/failures/struct.go similarity index 61% rename from types/failure.go rename to types/failures/struct.go index 0e6ea788..004aafd6 100644 --- a/types/failure.go +++ b/types/failures/struct.go @@ -1,22 +1,10 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . +package failures -package types +import "time" -import ( - "time" +const ( + limitedFailures = 32 + limitedHits = 32 ) // Failure is a failed attempt to check a service. Any a service does not meet the expected requirements, diff --git a/types/group.go b/types/group.go deleted file mode 100644 index 90c44941..00000000 --- a/types/group.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -import "time" - -// Group is the main struct for Groups -type Group struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Name string `gorm:"column:name" json:"name"` - Public NullBool `gorm:"default:true;column:public" json:"public"` - Order int `gorm:"default:0;column:order_id" json:"order_id"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` -} diff --git a/types/groups/database.go b/types/groups/database.go new file mode 100644 index 00000000..5f3beabf --- /dev/null +++ b/types/groups/database.go @@ -0,0 +1,56 @@ +package groups + +import ( + "github.com/hunterlong/statping/database" + "sort" +) + +func Find(id int64) (*Group, error) { + var group *Group + db := database.DB().Model(&Group{}).Where("id = ?", id).Find(&group) + return group, db.Error() +} + +func All() []*Group { + var groups []*Group + database.DB().Model(&Group{}).Find(&groups) + return groups +} + +func (g *Group) Create() error { + db := database.DB().Create(&g) + return db.Error() +} + +func (g *Group) Update() error { + db := database.DB().Update(&g) + return db.Error() +} + +func (g *Group) Delete() error { + db := database.DB().Delete(&g) + return db.Error() +} + +// SelectGroups returns all groups +func SelectGroups(includeAll bool, auth bool) []*Group { + var validGroups []*Group + + all := All() + if includeAll { + sort.Sort(GroupOrder(all)) + return all + } + + for _, g := range all { + if !g.Public.Bool { + if auth { + validGroups = append(validGroups, g) + } + } else { + validGroups = append(validGroups, g) + } + } + sort.Sort(GroupOrder(validGroups)) + return validGroups +} diff --git a/types/groups/methods.go b/types/groups/methods.go new file mode 100644 index 00000000..73f80665 --- /dev/null +++ b/types/groups/methods.go @@ -0,0 +1,20 @@ +package groups + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/services" +) + +func (g *Group) Services() []*services.Service { + var services []*services.Service + database.DB().Where("group = ?", g.Id).Find(&services) + return services +} + +// GroupOrder will reorder the groups based on 'order_id' (Order) +type GroupOrder []*Group + +// Sort interface for resorting the Groups in order +func (c GroupOrder) Len() int { return len(c) } +func (c GroupOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c GroupOrder) Less(i, j int) bool { return c[i].Order < c[j].Order } diff --git a/types/groups/samples.go b/types/groups/samples.go new file mode 100644 index 00000000..56db2f87 --- /dev/null +++ b/types/groups/samples.go @@ -0,0 +1,28 @@ +package groups + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" +) + +func (g *Group) Samples() []database.DbObject { + group1 := &Group{ + Name: "Main Services", + Public: null.NewNullBool(true), + Order: 2, + } + + group2 := &Group{ + Name: "Linked Services", + Public: null.NewNullBool(false), + Order: 1, + } + + group3 := &Group{ + Name: "Empty Group", + Public: null.NewNullBool(false), + Order: 3, + } + + return []database.DbObject{group1, group2, group3} +} diff --git a/types/groups/struct.go b/types/groups/struct.go new file mode 100644 index 00000000..f2d3e239 --- /dev/null +++ b/types/groups/struct.go @@ -0,0 +1,16 @@ +package groups + +import ( + "github.com/hunterlong/statping/types/null" + "time" +) + +// Group is the main struct for Groups +type Group struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Name string `gorm:"column:name" json:"name"` + Public null.NullBool `gorm:"default:true;column:public" json:"public"` + Order int `gorm:"default:0;column:order_id" json:"order_id"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} diff --git a/types/hits/database.go b/types/hits/database.go new file mode 100644 index 00000000..9d5ae80b --- /dev/null +++ b/types/hits/database.go @@ -0,0 +1,34 @@ +package hits + +import "github.com/hunterlong/statping/database" + +func DB() database.Database { + return database.DB().Model(&Hit{}) +} + +func Find(id int64) (*Hit, error) { + var group *Hit + db := DB().Where("id = ?", id).Find(&group) + return group, db.Error() +} + +func All() []*Hit { + var hits []*Hit + DB().Find(&hits) + return hits +} + +func (h *Hit) Create() error { + db := DB().Create(&h) + return db.Error() +} + +func (h *Hit) Update() error { + db := DB().Update(&h) + return db.Error() +} + +func (h *Hit) Delete() error { + db := DB().Delete(&h) + return db.Error() +} diff --git a/types/hits/interface.go b/types/hits/interface.go new file mode 100644 index 00000000..a8a6f696 --- /dev/null +++ b/types/hits/interface.go @@ -0,0 +1,63 @@ +package hits + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "time" +) + +type ColumnIDInterfacer interface { + HitsColumnID() (string, int64) +} + +type Hitters struct { + db database.Database +} + +func (h Hitters) Db() database.Database { + return h.db +} + +func (h Hitters) Since(t time.Time) []*Hit { + var hits []*Hit + h.db.Since(t).Find(&hits) + return hits +} + +func (h Hitters) List() []*Hit { + var hits []*Hit + h.db.Find(&hits) + return hits +} + +func (h Hitters) Last(amount int) []*Hit { + var hits []*Hit + h.db.Limit(amount).Find(&hits) + return hits +} + +func (h Hitters) Count() int { + var count int + h.db.Count(&count) + return count +} + +func (h Hitters) Sum() float64 { + result := struct { + amount float64 + }{0} + + h.db.Select("AVG(latency) as amount").Scan(&result).Debug() + return result.amount +} + +func AllHits(obj ColumnIDInterfacer) Hitters { + column, id := obj.HitsColumnID() + return Hitters{DB().Where(fmt.Sprintf("%s = ?", column), id)} +} + +func HitsSince(t time.Time, obj ColumnIDInterfacer) Hitters { + column, id := obj.HitsColumnID() + timestamp := DB().FormatTime(t) + return Hitters{DB().Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)} +} diff --git a/types/hits/samples.go b/types/hits/samples.go new file mode 100644 index 00000000..8323652f --- /dev/null +++ b/types/hits/samples.go @@ -0,0 +1,30 @@ +package hits + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/utils" + "time" +) + +func (u *Hit) Samples() []database.DbObject { + createdAt := time.Now().Add(-1 * types.Month) + var hits []database.DbObject + + for i := int64(1); i <= 5; i++ { + p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano()) + + for hi := 1; hi <= 5500; hi++ { + latency := p.Noise1D(float64(hi / 10)) + createdAt = createdAt.Add(1 * time.Minute) + hit := &Hit{ + Service: i, + CreatedAt: createdAt.UTC(), + Latency: latency, + } + hits = append(hits, hit) + } + } + + return hits +} diff --git a/types/hits/struct.go b/types/hits/struct.go new file mode 100644 index 00000000..951f1d40 --- /dev/null +++ b/types/hits/struct.go @@ -0,0 +1,20 @@ +package hits + +import "time" + +// Hit struct is a 'successful' ping or web response entry for a service. +type Hit struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Service int64 `gorm:"column:service" json:"-"` + Latency float64 `gorm:"column:latency" json:"latency"` + PingTime float64 `gorm:"column:ping_time" json:"ping_time"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` +} + +// BeforeCreate for Hit will set CreatedAt to UTC +func (h *Hit) BeforeCreate() (err error) { + if h.CreatedAt.IsZero() { + h.CreatedAt = time.Now().UTC() + } + return +} diff --git a/types/incidents/database.go b/types/incidents/database.go new file mode 100644 index 00000000..704875ff --- /dev/null +++ b/types/incidents/database.go @@ -0,0 +1,30 @@ +package incidents + +import "github.com/hunterlong/statping/database" + +func Find(id int64) (*Incident, error) { + var incident *Incident + db := database.DB().Model(&Incident{}).Where("id = ?", id).Find(&incident) + return incident, db.Error() +} + +func All() []*Incident { + var incidents []*Incident + database.DB().Model(&Incident{}).Find(&incidents) + return incidents +} + +func (i *Incident) Create() error { + db := database.DB().Create(&i) + return db.Error() +} + +func (i *Incident) Update() error { + db := database.DB().Update(&i) + return db.Error() +} + +func (i *Incident) Delete() error { + db := database.DB().Delete(&i) + return db.Error() +} diff --git a/types/incidents/database_updates.go b/types/incidents/database_updates.go new file mode 100644 index 00000000..e3e7d3b5 --- /dev/null +++ b/types/incidents/database_updates.go @@ -0,0 +1,25 @@ +package incidents + +import "github.com/hunterlong/statping/database" + +func (i *Incident) Updates() []*IncidentUpdate { + var updates []*IncidentUpdate + database.DB().Model(&IncidentUpdate{}).Where("incident = ?", i.Id).Find(&updates) + i.AllUpdates = updates + return updates +} + +func (i *IncidentUpdate) Create() error { + db := database.DB().Create(&i) + return db.Error() +} + +func (i *IncidentUpdate) Update() error { + db := database.DB().Update(&i) + return db.Error() +} + +func (i *IncidentUpdate) Delete() error { + db := database.DB().Delete(&i) + return db.Error() +} diff --git a/types/incidents/samples.go b/types/incidents/samples.go new file mode 100644 index 00000000..8aa24a4a --- /dev/null +++ b/types/incidents/samples.go @@ -0,0 +1,38 @@ +package incidents + +import ( + "github.com/hunterlong/statping/database" +) + +func (s *Incident) Samples() []database.DbObject { + incident1 := &Incident{ + Title: "Github Downtime", + Description: "This is an example of a incident for a service.", + ServiceId: 2, + } + + i1 := &IncidentUpdate{ + IncidentId: incident1.Id, + Message: "Github's page for Statping seems to be sending a 501 error.", + Type: "Investigating", + } + + i2 := &IncidentUpdate{ + IncidentId: incident1.Id, + Message: "Problem is continuing and we are looking at the issues.", + Type: "Update", + } + + i3 := &IncidentUpdate{ + IncidentId: incident1.Id, + Message: "Github is now back online and everything is working.", + Type: "Resolved", + } + + return []database.DbObject{i1, i2, i3} +} + +func (s *IncidentUpdate) Samples() []database.DbObject { + + return nil +} diff --git a/types/incident.go b/types/incidents/struct.go similarity index 93% rename from types/incident.go rename to types/incidents/struct.go index e6ef2ecb..bdac0aa1 100644 --- a/types/incident.go +++ b/types/incidents/struct.go @@ -1,4 +1,4 @@ -package types +package incidents import "time" @@ -10,7 +10,7 @@ type Incident struct { ServiceId int64 `gorm:"index;column:service" json:"service"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` - Updates []*IncidentUpdate `gorm:"-" json:"updates,omitempty"` + AllUpdates []*IncidentUpdate `gorm:"-" json:"updates,omitempty"` } // IncidentUpdate contains updates based on a Incident diff --git a/types/integrations/database.go b/types/integrations/database.go new file mode 100644 index 00000000..0ee90b7f --- /dev/null +++ b/types/integrations/database.go @@ -0,0 +1,41 @@ +package integrations + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/services" +) + +func DB() database.Database { + return database.DB().Model(&Integration{}) +} + +func Find(name string) (*Integration, error) { + var integration *Integration + db := DB().Where("name = ?", name).Find(&integration) + return integration, db.Error() +} + +func All() []*Integration { + var integrations []*Integration + DB().Find(&integrations) + return integrations +} + +func List(i Integrator) ([]*services.Service, error) { + return i.List() +} + +func (i *Integration) Create() error { + db := DB().Create(&i) + return db.Error() +} + +func (i *Integration) Update() error { + db := DB().Update(&i) + return db.Error() +} + +func (i *Integration) Delete() error { + db := DB().Delete(&i) + return db.Error() +} diff --git a/types/integrations.go b/types/integrations/struct.go similarity index 87% rename from types/integrations.go rename to types/integrations/struct.go index 4791134e..7638903d 100644 --- a/types/integrations.go +++ b/types/integrations/struct.go @@ -1,4 +1,6 @@ -package types +package integrations + +import "github.com/hunterlong/statping/types/services" type Integration struct { ShortName string `gorm:"column:name" json:"name"` @@ -19,5 +21,5 @@ type IntegrationField struct { type Integrator interface { Get() *Integration - List() ([]*Service, error) + List() ([]*services.Service, error) } diff --git a/types/interface.go b/types/interface.go new file mode 100644 index 00000000..ab1254f4 --- /dev/null +++ b/types/interface.go @@ -0,0 +1 @@ +package types diff --git a/types/message.go b/types/message.go deleted file mode 100644 index dc8729e2..00000000 --- a/types/message.go +++ /dev/null @@ -1,45 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "time" -) - -// Message is for creating Announcements, Alerts and other messages for the end users -type Message struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Title string `gorm:"column:title" json:"title"` - Description string `gorm:"column:description" json:"description"` - StartOn time.Time `gorm:"column:start_on" json:"start_on"` - EndOn time.Time `gorm:"column:end_on" json:"end_on"` - ServiceId int64 `gorm:"index;column:service" json:"service"` - NotifyUsers NullBool `gorm:"column:notify_users" json:"notify_users" scope:"user,admin"` - NotifyMethod string `gorm:"column:notify_method" json:"notify_method" scope:"user,admin"` - NotifyBefore NullInt64 `gorm:"column:notify_before" json:"notify_before" scope:"user,admin"` - NotifyBeforeScale string `gorm:"column:notify_before_scale" json:"notify_before_scale" scope:"user,admin"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` -} - -// BeforeCreate for Message will set CreatedAt to UTC -func (u *Message) BeforeCreate() (err error) { - if u.CreatedAt.IsZero() { - u.CreatedAt = time.Now().UTC() - u.UpdatedAt = time.Now().UTC() - } - return -} diff --git a/types/messages/database.go b/types/messages/database.go new file mode 100644 index 00000000..15bd8d85 --- /dev/null +++ b/types/messages/database.go @@ -0,0 +1,30 @@ +package messages + +import "github.com/hunterlong/statping/database" + +func Find(id int64) (*Message, error) { + var user *Message + db := database.DB().Model(&Message{}).Where("id = ?", id).Find(&user) + return user, db.Error() +} + +func All() []*Message { + var messages []*Message + database.DB().Model(&Message{}).Find(&messages) + return messages +} + +func (m *Message) Create() error { + db := database.DB().Create(&m) + return db.Error() +} + +func (m *Message) Update() error { + db := database.DB().Update(&m) + return db.Error() +} + +func (m *Message) Delete() error { + db := database.DB().Delete(&m) + return db.Error() +} diff --git a/types/messages/samples.go b/types/messages/samples.go new file mode 100644 index 00000000..b9284196 --- /dev/null +++ b/types/messages/samples.go @@ -0,0 +1,26 @@ +package messages + +import ( + "github.com/hunterlong/statping/database" + "time" +) + +func (m *Message) Samples() []database.DbObject { + m1 := &Message{ + Title: "Routine Downtime", + Description: "This is an example a upcoming message for a service!", + ServiceId: 1, + StartOn: time.Now().UTC().Add(15 * time.Minute), + EndOn: time.Now().UTC().Add(2 * time.Hour), + } + + m2 := &Message{ + Title: "Server Reboot", + Description: "This is another example a upcoming message for a service!", + ServiceId: 3, + StartOn: time.Now().UTC().Add(15 * time.Minute), + EndOn: time.Now().UTC().Add(2 * time.Hour), + } + + return []database.DbObject{m1, m2} +} diff --git a/types/messages/struct.go b/types/messages/struct.go new file mode 100644 index 00000000..a66b39b9 --- /dev/null +++ b/types/messages/struct.go @@ -0,0 +1,31 @@ +package messages + +import ( + "github.com/hunterlong/statping/types/null" + "time" +) + +// Message is for creating Announcements, Alerts and other messages for the end users +type Message struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Title string `gorm:"column:title" json:"title"` + Description string `gorm:"column:description" json:"description"` + StartOn time.Time `gorm:"column:start_on" json:"start_on"` + EndOn time.Time `gorm:"column:end_on" json:"end_on"` + ServiceId int64 `gorm:"index;column:service" json:"service"` + NotifyUsers null.NullBool `gorm:"column:notify_users" json:"notify_users" scope:"user,admin"` + NotifyMethod string `gorm:"column:notify_method" json:"notify_method" scope:"user,admin"` + NotifyBefore null.NullInt64 `gorm:"column:notify_before" json:"notify_before" scope:"user,admin"` + NotifyBeforeScale string `gorm:"column:notify_before_scale" json:"notify_before_scale" scope:"user,admin"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` +} + +// BeforeCreate for Message will set CreatedAt to UTC +func (m *Message) BeforeCreate() (err error) { + if m.CreatedAt.IsZero() { + m.CreatedAt = time.Now().UTC() + m.UpdatedAt = time.Now().UTC() + } + return +} diff --git a/core/hits.go b/types/notifications/audit.go similarity index 87% rename from core/hits.go rename to types/notifications/audit.go index 3bbe4e83..3adf508b 100644 --- a/core/hits.go +++ b/types/notifications/audit.go @@ -13,12 +13,4 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package core - -import ( - "github.com/hunterlong/statping/types" -) - -type Hit struct { - *types.Hit -} +package notifications diff --git a/types/notifications/database.go b/types/notifications/database.go new file mode 100644 index 00000000..5d8252b5 --- /dev/null +++ b/types/notifications/database.go @@ -0,0 +1,53 @@ +package notifications + +import ( + "errors" + "github.com/hunterlong/statping/database" +) + +func DB() database.Database { + return database.DB().Model(&Notification{}) +} + +func Find(method string) (Notifier, error) { + for _, n := range AllCommunications { + notif := n.Select() + if notif.Method == method { + return n, nil + } + } + return nil, errors.New("notifier not found") +} + +func All() []*Notification { + var notifiers []*Notification + DB().Find(¬ifiers) + return notifiers +} + +func (n *Notification) Create() error { + db := DB().FirstOrCreate(&n) + return db.Error() +} + +func Update(notifier Notifier) error { + n := notifier.Select() + n.ResetQueue() + err := n.Update() + if n.Enabled.Bool { + n.Close() + n.Start() + go Queue(notifier) + } else { + n.Close() + } + return err +} + +func (n *Notification) Update() error { + return Update(n) +} + +func (n *Notification) Delete() error { + return nil +} diff --git a/core/notifier/doc.go b/types/notifications/doc.go similarity index 95% rename from core/notifier/doc.go rename to types/notifications/doc.go index 0e3e5dc3..f72d543a 100644 --- a/core/notifier/doc.go +++ b/types/notifications/doc.go @@ -100,13 +100,13 @@ // these methods will be ran with the service corresponding to it. // // // REQUIRED - BASIC EVENT -// func (n *ExampleNotifier) OnSuccess(s *types.Service) { +// func (n *ExampleNotifier) OnSuccess(s *services.Service) { // msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name) // n.AddQueue(msg) // } // // // REQUIRED - BASIC EVENT -// func (n *ExampleNotifier) OnFailure(s *types.Service, f *types.Failure) { +// func (n *ExampleNotifier) OnFailure(s *services.Service, f *types.Failure) { // msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name) // n.AddQueue(msg) // } @@ -117,4 +117,4 @@ // see more details and examples of how to build your own notifier. // // More info on: https://github.com/hunterlong/statping/wiki/Notifiers -package notifier +package notifications diff --git a/core/notifier/events.go b/types/notifications/events.go similarity index 82% rename from core/notifier/events.go rename to types/notifications/events.go index 4e70c573..65874af7 100644 --- a/core/notifier/events.go +++ b/types/notifications/events.go @@ -13,11 +13,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package notifier +package notifications import ( "fmt" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" ) @@ -34,7 +36,7 @@ func OnSave(method string) { } // OnFailure will be triggered when a service is failing - BasicEvents interface -func OnFailure(s *types.Service, f *types.Failure) { +func OnFailure(s *services.Service, f *failures.Failure) { if !s.AllowNotifications.Bool { return } @@ -63,7 +65,7 @@ sendMessages: } // OnSuccess will be triggered when a service is successful - BasicEvents interface -func OnSuccess(s *types.Service) { +func OnSuccess(s *services.Service) { if !s.AllowNotifications.Bool { return } @@ -85,7 +87,7 @@ func OnSuccess(s *types.Service) { } // OnNewService is triggered when a new service is created - ServiceEvents interface -func OnNewService(s *types.Service) { +func OnNewService(s *services.Service) { for _, comm := range AllCommunications { if utils.IsType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) { log. @@ -97,7 +99,7 @@ func OnNewService(s *types.Service) { } // OnUpdatedService is triggered when a service is updated - ServiceEvents interface -func OnUpdatedService(s *types.Service) { +func OnUpdatedService(s *services.Service) { if !s.AllowNotifications.Bool { return } @@ -110,7 +112,7 @@ func OnUpdatedService(s *types.Service) { } // OnDeletedService is triggered when a service is deleted - ServiceEvents interface -func OnDeletedService(s *types.Service) { +func OnDeletedService(s *services.Service) { if !s.AllowNotifications.Bool { return } @@ -123,7 +125,7 @@ func OnDeletedService(s *types.Service) { } // OnNewUser is triggered when a new user is created - UserEvents interface -func OnNewUser(u *types.User) { +func OnNewUser(u *users.User) { for _, comm := range AllCommunications { if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) { log.Debugln(fmt.Sprintf("Sending new user notification for user %v", u.Username)) @@ -133,7 +135,7 @@ func OnNewUser(u *types.User) { } // OnUpdatedUser is triggered when a new user is updated - UserEvents interface -func OnUpdatedUser(u *types.User) { +func OnUpdatedUser(u *users.User) { for _, comm := range AllCommunications { if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) { log.Debugln(fmt.Sprintf("Sending updated user notification for user %v", u.Username)) @@ -143,7 +145,7 @@ func OnUpdatedUser(u *types.User) { } // OnDeletedUser is triggered when a new user is deleted - UserEvents interface -func OnDeletedUser(u *types.User) { +func OnDeletedUser(u *users.User) { for _, comm := range AllCommunications { if utils.IsType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) { log.Debugln(fmt.Sprintf("Sending deleted user notification for user %v", u.Username)) @@ -152,24 +154,24 @@ func OnDeletedUser(u *types.User) { } } -// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface -func OnUpdatedCore(c *types.Core) { - for _, comm := range AllCommunications { - if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) { - log.Debugln(fmt.Sprintf("Sending updated core notification")) - comm.(CoreEvents).OnUpdatedCore(c) - } - } -} - -// OnStart is triggered when the Statping service has started -func OnStart(c *types.Core) { - for _, comm := range AllCommunications { - if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) { - comm.(CoreEvents).OnUpdatedCore(c) - } - } -} +//// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface +//func OnUpdatedCore(c *core.Core) { +// for _, comm := range AllCommunications { +// if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) { +// log.Debugln(fmt.Sprintf("Sending updated core notification")) +// comm.(CoreEvents).OnUpdatedCore(c) +// } +// } +//} +// +//// OnStart is triggered when the Statping service has started +//func OnStart(c *core.Core) { +// for _, comm := range AllCommunications { +// if utils.IsType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) { +// comm.(CoreEvents).OnUpdatedCore(c) +// } +// } +//} // OnNewNotifier is triggered when a new notifier is loaded func OnNewNotifier(n *Notification) { diff --git a/core/notifier/example_test.go b/types/notifications/example_test.go similarity index 89% rename from core/notifier/example_test.go rename to types/notifications/example_test.go index c7ddd796..5215a0a7 100644 --- a/core/notifier/example_test.go +++ b/types/notifications/example_test.go @@ -13,13 +13,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package notifier +package notifications import ( "errors" "fmt" "github.com/hunterlong/statping/source" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" "time" ) @@ -112,13 +114,13 @@ func (n *ExampleNotifier) OnSave() error { } // OnSuccess is a required basic event for the Notifier interface -func (n *ExampleNotifier) OnSuccess(s *types.Service) { +func (n *ExampleNotifier) OnSuccess(s *services.Service) { msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnFailure is a required basic event for the Notifier interface -func (n *ExampleNotifier) OnFailure(s *types.Service, f *types.Failure) { +func (n *ExampleNotifier) OnFailure(s *services.Service, f *failures.Failure) { msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } @@ -130,52 +132,52 @@ func (n *ExampleNotifier) OnTest() error { } // OnNewService is a option event for new services -func (n *ExampleNotifier) OnNewService(s *types.Service) { +func (n *ExampleNotifier) OnNewService(s *services.Service) { msg := fmt.Sprintf("received a new service trigger for service: %v\n", s.Name) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnUpdatedService is a option event for updated services -func (n *ExampleNotifier) OnUpdatedService(s *types.Service) { +func (n *ExampleNotifier) OnUpdatedService(s *services.Service) { msg := fmt.Sprintf("received a update service trigger for service: %v\n", s.Name) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnDeletedService is a option event for deleted services -func (n *ExampleNotifier) OnDeletedService(s *types.Service) { +func (n *ExampleNotifier) OnDeletedService(s *services.Service) { msg := fmt.Sprintf("received a delete service trigger for service: %v\n", s.Name) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnNewUser is a option event for new users -func (n *ExampleNotifier) OnNewUser(s *types.User) { +func (n *ExampleNotifier) OnNewUser(s *users.User) { msg := fmt.Sprintf("received a new user trigger for user: %v\n", s.Username) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnUpdatedUser is a option event for updated users -func (n *ExampleNotifier) OnUpdatedUser(s *types.User) { +func (n *ExampleNotifier) OnUpdatedUser(s *users.User) { msg := fmt.Sprintf("received a updated user trigger for user: %v\n", s.Username) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } // OnDeletedUser is a option event for deleted users -func (n *ExampleNotifier) OnDeletedUser(s *types.User) { +func (n *ExampleNotifier) OnDeletedUser(s *users.User) { msg := fmt.Sprintf("received a deleted user trigger for user: %v\n", s.Username) n.AddQueue(fmt.Sprintf("service_%v", s.Id), msg) } -// OnUpdatedCore is a option event when the settings are updated -func (n *ExampleNotifier) OnUpdatedCore(s *types.Core) { - msg := fmt.Sprintf("received a updated core trigger for core: %v\n", s.Name) - n.AddQueue("core", msg) -} - -// OnStart is triggered when statup has been started -func (n *ExampleNotifier) OnStart(s *types.Core) { - msg := fmt.Sprintf("received a trigger on Statping boot: %v\n", s.Name) - n.AddQueue(fmt.Sprintf("core"), msg) -} +//// OnUpdatedCore is a option event when the settings are updated +//func (n *ExampleNotifier) OnUpdatedCore(c *core.Core) { +// msg := fmt.Sprintf("received a updated core trigger for core: %v\n", c.Name) +// n.AddQueue("core", msg) +//} +// +//// OnStart is triggered when statup has been started +//func (n *ExampleNotifier) OnStart(c *core.Core) { +// msg := fmt.Sprintf("received a trigger on Statping boot: %v\n", c.Name) +// n.AddQueue(fmt.Sprintf("core"), msg) +//} // OnNewNotifier is triggered when a new notifier has initialized func (n *ExampleNotifier) OnNewNotifier(s *Notification) { diff --git a/core/notifier/interfaces.go b/types/notifications/interfaces.go similarity index 72% rename from core/notifier/interfaces.go rename to types/notifications/interfaces.go index 660600f0..495a0066 100644 --- a/core/notifier/interfaces.go +++ b/types/notifications/interfaces.go @@ -13,9 +13,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package notifier +package notifications -import "github.com/hunterlong/statping/types" +import ( + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" +) // Notifier interface is required to create a new Notifier type Notifier interface { @@ -26,8 +30,8 @@ type Notifier interface { // BasicEvents includes the most minimal events, failing and successful service triggers type BasicEvents interface { - OnSuccess(*types.Service) // OnSuccess is triggered when a service is successful - OnFailure(*types.Service, *types.Failure) // OnFailure is triggered when a service is failing + OnSuccess(*services.Service) // OnSuccess is triggered when a service is successful + OnFailure(*services.Service, *failures.Failure) // OnFailure is triggered when a service is failing } // Tester interface will include a function to Test users settings before saving @@ -37,23 +41,23 @@ type Tester interface { // ServiceEvents are events for Services type ServiceEvents interface { - OnNewService(*types.Service) - OnUpdatedService(*types.Service) - OnDeletedService(*types.Service) + OnNewService(*services.Service) + OnUpdatedService(*services.Service) + OnDeletedService(*services.Service) } // UserEvents are events for Users type UserEvents interface { - OnNewUser(*types.User) - OnUpdatedUser(*types.User) - OnDeletedUser(*types.User) + OnNewUser(*users.User) + OnUpdatedUser(*users.User) + OnDeletedUser(*users.User) } // CoreEvents are events for the main Core app -type CoreEvents interface { - OnUpdatedCore(*types.Core) - OnStart(*types.Core) -} +//type CoreEvents interface { +// OnUpdatedCore(*core.Core) +// OnStart(*core.Core) +//} // NotifierEvents are events for other Notifiers type NotifierEvents interface { diff --git a/types/notifications/methods.go b/types/notifications/methods.go new file mode 100644 index 00000000..09ffe61d --- /dev/null +++ b/types/notifications/methods.go @@ -0,0 +1,148 @@ +package notifications + +import ( + "fmt" + "github.com/hunterlong/statping/utils" + "strings" + "time" +) + +// AfterFind for Notification will set the timezone +func (n *Notification) AfterFind() (err error) { + n.CreatedAt = utils.Now() + n.UpdatedAt = utils.Now() + return +} + +// AddQueue will add any type of interface (json, string, struct, etc) into the Notifiers queue +func (n *Notification) AddQueue(uid string, msg interface{}) { + data := &QueueData{uid, msg} + n.Queue = append(n.Queue, data) + log.WithFields(utils.ToFields(data, n)).Debug(fmt.Sprintf("Notifier '%v' added new item (%v) to the queue. (%v queued)", n.Method, uid, len(n.Queue))) +} + +// CanTest returns true if the notifier implements the OnTest interface +func (n *Notification) CanTest() bool { + return n.testable +} + +// LastSent returns a time.Duration of the last sent notification for the notifier +func (n *Notification) LastSent() time.Duration { + if len(n.logs) == 0 { + return time.Duration(0) + } + last := n.Logs()[0] + since := time.Since(last.Timestamp) + return since +} + +// SentLastHour returns the total amount of notifications sent in last 1 hour +func (n *Notification) SentLastHour() int { + since := utils.Now().Add(-1 * time.Hour) + return n.SentLast(since) +} + +// SentLastMinute returns the total amount of notifications sent in last 1 minute +func (n *Notification) SentLastMinute() int { + since := utils.Now().Add(-1 * time.Minute) + return n.SentLast(since) +} + +// SentLast accept a time.Time and returns the amount of sent notifications within your time to current +func (n *Notification) SentLast(since time.Time) int { + sent := 0 + for _, v := range n.Logs() { + lastTime := time.Time(v.Time).UTC() + if lastTime.After(since) { + sent++ + } + } + return sent +} + +// GetValue returns the database value of a accept DbField value. +func (n *Notification) GetValue(dbField string) string { + dbField = strings.ToLower(dbField) + switch dbField { + case "host": + return n.Host + case "port": + return fmt.Sprintf("%v", n.Port) + case "username": + return n.Username + case "password": + if n.Password != "" { + return "##########" + } + case "var1": + return n.Var1 + case "var2": + return n.Var2 + case "api_key": + return n.ApiKey + case "api_secret": + return n.ApiSecret + case "limits": + return utils.ToString(int(n.Limits)) + } + return "" +} + +// Init accepts the Notifier interface to initialize the notifier +func Init(n Notifier) (*Notification, error) { + err := install(n) + var notify *Notification + if err == nil { + notify, _ = SelectNotification(n) + notify.CreatedAt = time.Now().UTC() + notify.UpdatedAt = time.Now().UTC() + if notify.Delay.Seconds() == 0 { + notify.Delay = time.Duration(1 * time.Second) + } + notify.testable = utils.IsType(n, new(Tester)) + notify.Form = n.Select().Form + } + return notify, err +} + +// ResetQueue will clear the notifiers Queue +func (n *Notification) ResetQueue() { + n.Queue = nil +} + +// ResetQueue will clear the notifiers Queue for a service +func (n *Notification) ResetUniqueQueue(uid string) []*QueueData { + var queue []*QueueData + for _, v := range n.Queue { + if v.Id != uid { + queue = append(queue, v) + } + } + n.Queue = queue + return queue +} + +// start will start the go routine for the notifier queue +func (n *Notification) Start() { + n.Running = make(chan bool) +} + +// close will stop the go routine for queue +func (n *Notification) Close() { + if n.IsRunning() { + close(n.Running) + } +} + +// IsRunning will return true if the notifier is currently running a queue +func (n *Notification) IsRunning() bool { + if n.Running == nil { + return false + } + select { + case <-n.Running: + return false + default: + return true + } +} diff --git a/types/notifications/notifications_test.go b/types/notifications/notifications_test.go new file mode 100644 index 00000000..9965610b --- /dev/null +++ b/types/notifications/notifications_test.go @@ -0,0 +1,14 @@ +package notifications + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInsertNotifierDB(t *testing.T) { + if skipNewDb { + t.SkipNow() + } + err := InsertNotifierDB() + assert.Nil(t, err) +} diff --git a/core/notifier/notifiers.go b/types/notifications/struct.go similarity index 55% rename from core/notifier/notifiers.go rename to types/notifications/struct.go index 3492c630..24f185da 100644 --- a/core/notifier/notifiers.go +++ b/types/notifications/struct.go @@ -13,27 +13,25 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package notifier +package notifications import ( "encoding/json" "errors" "fmt" "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/services" "github.com/hunterlong/statping/utils" "reflect" - "strings" "time" ) var ( - // AllCommunications holds all the loaded notifiers - AllCommunications []types.AllNotifiers // db holds the Statping database connection - db database.Database - timezone float32 - log = utils.Log.WithField("type", "notifier") + db database.Database + log = utils.Log.WithField("type", "notifier") + AllCommunications []Notifier ) // Notification contains all the fields for a Statping Notifier. @@ -48,7 +46,7 @@ type Notification struct { Var2 string `gorm:"not null;column:var2" json:"var2,omitempty"` ApiKey string `gorm:"not null;column:api_key" json:"api_key,omitempty"` ApiSecret string `gorm:"not null;column:api_secret" json:"api_secret,omitempty"` - Enabled types.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"` + Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"` Limits int `gorm:"not null;column:limits" json:"limits"` Removable bool `gorm:"column:removable" json:"removeable"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` @@ -64,6 +62,8 @@ type Notification struct { Queue []*QueueData `gorm:"-" json:"-"` Running chan bool `gorm:"-" json:"-"` testable bool `gorm:"-" json:"testable"` + + Notifier } // QueueData is the struct for the messaging queue with service @@ -92,30 +92,6 @@ type NotificationLog struct { Timestamp time.Time `json:"timestamp"` } -// AfterFind for Notification will set the timezone -func (n *Notification) AfterFind() (err error) { - n.CreatedAt = utils.Now() - n.UpdatedAt = utils.Now() - return -} - -// AddQueue will add any type of interface (json, string, struct, etc) into the Notifiers queue -func (n *Notification) AddQueue(uid string, msg interface{}) { - data := &QueueData{uid, msg} - n.Queue = append(n.Queue, data) - log.WithFields(utils.ToFields(data, n)).Debug(fmt.Sprintf("Notifier '%v' added new item (%v) to the queue. (%v queued)", n.Method, uid, len(n.Queue))) -} - -// CanTest returns true if the notifier implements the OnTest interface -func (n *Notification) CanTest() bool { - return n.testable -} - -// db will return the notifier database column/record -func modelDb(n *Notification) database.Database { - return db.Model(&Notification{}).Where("method = ?", n.Method).Find(n) -} - // SetDB is called by core to inject the database for a notifier to use func SetDB(d database.Database) { db = d @@ -126,24 +102,6 @@ func asNotification(n Notifier) *Notification { return n.Select() } -// AddNotifier accept a Notifier interface to be added into the array -func AddNotifiers(notifiers ...Notifier) error { - for _, n := range notifiers { - if utils.IsType(n, new(Notifier)) { - err := checkNotifierForm(n) - if err != nil { - return err - } - AllCommunications = append(AllCommunications, n) - Init(n) - } else { - return errors.New("notifier does not have the required methods") - } - } - startAllNotifiers() - return nil -} - // normalizeType will accept multiple interfaces and converts it into a string for logging func normalizeType(ty interface{}) string { switch v := ty.(type) { @@ -188,12 +146,6 @@ func reverseLogs(input []*NotificationLog) []*NotificationLog { return append(reverseLogs(input[1:]), input[0]) } -// isInDatabase returns true if the notifier has already been installed -func isInDatabase(n Notifier) bool { - inDb := modelDb(n.Select()).RecordNotFound() - return !inDb -} - // SelectNotification returns the Notification struct from the database func SelectNotification(n Notifier) (*Notification, error) { notifier := n.Select() @@ -201,29 +153,15 @@ func SelectNotification(n Notifier) (*Notification, error) { return notifier, err.Error() } -// Update will update the notification into the database -func Update(n Notifier, notif *Notification) (*Notification, error) { - notif.ResetQueue() - err := db.Model(&Notification{}).Update(notif) - if notif.Enabled.Bool { - notif.close() - notif.start() - go Queue(n) - } else { - notif.close() - } - return notif, err.Error() -} - // insertDatabase will create a new record into the database for the notifier func insertDatabase(n Notifier) (int64, error) { noti := n.Select() noti.Limits = 3 - query := db.Create(noti) - if query.Error() != nil { - return 0, query.Error() + err := noti.Create() + if err != nil { + return 0, err } - return noti.Id, query.Error() + return noti.Id, err } // SelectNotifier returns the Notification struct from the database @@ -241,66 +179,34 @@ func SelectNotifier(method string) (*Notification, Notifier, error) { return nil, nil, errors.New("cannot find notifier") } -// Init accepts the Notifier interface to initialize the notifier -func Init(n Notifier) (*Notification, error) { - err := install(n) - var notify *Notification - if err == nil { - notify, _ = SelectNotification(n) - notify.CreatedAt = time.Now().UTC() - notify.UpdatedAt = time.Now().UTC() - if notify.Delay.Seconds() == 0 { - notify.Delay = time.Duration(1 * time.Second) - } - notify.testable = utils.IsType(n, new(Tester)) - notify.Form = n.Select().Form - } - return notify, err -} - -// startAllNotifiers will start the go routine for each loaded notifier -func startAllNotifiers() { - for _, comm := range AllCommunications { - if utils.IsType(comm, new(Notifier)) { - notify := comm.(Notifier) - if notify.Select().Enabled.Bool { - notify.Select().close() - notify.Select().start() - go Queue(notify) - } - } - } -} - // Queue is the FIFO go routine to send notifications when objects are triggered -func Queue(n Notifier) { - notification := n.Select() - rateLimit := notification.Delay +func Queue(notifer Notifier) { + n := notifer.Select() + rateLimit := n.Delay CheckNotifier: for { select { - case <-notification.Running: + case <-n.Running: break CheckNotifier case <-time.After(rateLimit): - notification = n.Select() - if len(notification.Queue) > 0 { - ok, _ := notification.WithinLimits() + if len(n.Queue) > 0 { + ok, _ := n.WithinLimits() if ok { - msg := notification.Queue[0] - err := n.Send(msg.Data) + msg := n.Queue[0] + err := notifer.Send(msg.Data) if err != nil { - log.WithFields(utils.ToFields(notification, msg)).Error(fmt.Sprintf("Notifier '%v' had an error: %v", notification.Method, err)) + log.WithFields(utils.ToFields(n, msg)).Error(fmt.Sprintf("Notifier '%v' had an error: %v", n.Method, err)) } else { - log.WithFields(utils.ToFields(notification, msg)).Debug(fmt.Sprintf("Notifier '%v' sent outgoing message (%v) %v left in queue.", notification.Method, msg.Id, len(notification.Queue))) + log.WithFields(utils.ToFields(n, msg)).Debug(fmt.Sprintf("Notifier '%v' sent outgoing message (%v) %v left in queue.", n.Method, msg.Id, len(n.Queue))) } - notification.makeLog(msg.Data) - if len(notification.Queue) > 1 { - notification.Queue = notification.Queue[1:] + n.makeLog(msg.Data) + if len(n.Queue) > 1 { + n.Queue = n.Queue[1:] } else { - notification.Queue = nil + n.Queue = nil } - rateLimit = notification.Delay + rateLimit = n.Delay } } } @@ -310,82 +216,18 @@ CheckNotifier: // install will check the database for the notification, if its not inserted it will insert a new record for it func install(n Notifier) error { - inDb := isInDatabase(n) - log.WithField("installed", inDb). - WithFields(utils.ToFields(n)). - Debugln(fmt.Sprintf("Checking if notifier '%v' is installed: %v", n.Select().Method, inDb)) - if !inDb { - _, err := insertDatabase(n) - if err != nil { - log.Errorln(err) - return err - } + _, err := insertDatabase(n) + if err != nil { + log.Errorln(err) + return err } + + log.WithFields(utils.ToFields(n)). + Debugln(fmt.Sprintf("Checking if notifier '%v' is installed", n.Select().Method)) + return nil } -// LastSent returns a time.Duration of the last sent notification for the notifier -func (n *Notification) LastSent() time.Duration { - if len(n.logs) == 0 { - return time.Duration(0) - } - last := n.Logs()[0] - since := time.Since(last.Timestamp) - return since -} - -// SentLastHour returns the total amount of notifications sent in last 1 hour -func (n *Notification) SentLastHour() int { - since := utils.Now().Add(-1 * time.Hour) - return n.SentLast(since) -} - -// SentLastMinute returns the total amount of notifications sent in last 1 minute -func (n *Notification) SentLastMinute() int { - since := utils.Now().Add(-1 * time.Minute) - return n.SentLast(since) -} - -// SentLast accept a time.Time and returns the amount of sent notifications within your time to current -func (n *Notification) SentLast(since time.Time) int { - sent := 0 - for _, v := range n.Logs() { - lastTime := time.Time(v.Time).UTC() - if lastTime.After(since) { - sent++ - } - } - return sent -} - -// GetValue returns the database value of a accept DbField value. -func (n *Notification) GetValue(dbField string) string { - dbField = strings.ToLower(dbField) - switch dbField { - case "host": - return n.Host - case "port": - return fmt.Sprintf("%v", n.Port) - case "username": - return n.Username - case "password": - if n.Password != "" { - return "##########" - } - case "var1": - return n.Var1 - case "var2": - return n.Var2 - case "api_key": - return n.ApiKey - case "api_secret": - return n.ApiSecret - case "limits": - return utils.ToString(int(n.Limits)) - } - return "" -} - // isEnabled returns true if the notifier is enabled func isEnabled(n interface{}) bool { notifier := n.(Notifier).Select() @@ -416,50 +258,8 @@ func (n *Notification) WithinLimits() (bool, error) { return true, nil } -// ResetQueue will clear the notifiers Queue -func (n *Notification) ResetQueue() { - n.Queue = nil -} - -// ResetQueue will clear the notifiers Queue for a service -func (n *Notification) ResetUniqueQueue(uid string) []*QueueData { - var queue []*QueueData - for _, v := range n.Queue { - if v.Id != uid { - queue = append(queue, v) - } - } - n.Queue = queue - return queue -} - -// start will start the go routine for the notifier queue -func (n *Notification) start() { - n.Running = make(chan bool) -} - -// close will stop the go routine for queue -func (n *Notification) close() { - if n.IsRunning() { - close(n.Running) - } -} - -// IsRunning will return true if the notifier is currently running a queue -func (n *Notification) IsRunning() bool { - if n.Running == nil { - return false - } - select { - case <-n.Running: - return false - default: - return true - } -} - // ExampleService can be used for the OnTest() method for notifiers -var ExampleService = &types.Service{ +var ExampleService = &services.Service{ Id: 1, Name: "Interpol - All The Rage Back Home", Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU", @@ -469,7 +269,7 @@ var ExampleService = &types.Service{ Method: "GET", Timeout: 20, LastStatusCode: 404, - Expected: types.NewNullString("test example"), + Expected: null.NewNullString("test example"), LastResponse: "this is an example response", CreatedAt: utils.Now().Add(-24 * time.Hour), } diff --git a/core/notifier/notifiers_test.go b/types/notifications/struct_test.go similarity index 89% rename from core/notifier/notifiers_test.go rename to types/notifications/struct_test.go index 6e2105a5..d9762905 100644 --- a/core/notifier/notifiers_test.go +++ b/types/notifications/struct_test.go @@ -13,12 +13,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package notifier +package notifications import ( "fmt" "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/core" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/types/services" + "github.com/hunterlong/statping/types/users" "github.com/hunterlong/statping/utils" _ "github.com/jinzhu/gorm/dialects/sqlite" "github.com/stretchr/testify/assert" @@ -31,7 +35,7 @@ var ( METHOD = "example" ) -var service = &types.Service{ +var service = &services.Service{ Name: "Interpol - All The Rage Back Home", Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU", ExpectedStatus: 200, @@ -39,19 +43,19 @@ var service = &types.Service{ Type: "http", Method: "GET", Timeout: 20, - AllowNotifications: types.NewNullBool(true), + AllowNotifications: null.NewNullBool(true), } -var failure = &types.Failure{ +var failure = &failures.Failure{ Issue: "testing", } -var user = &types.User{ +var user = &users.User{ Username: "admin", Email: "info@email.com", } -var core = &types.Core{ +var testCore = &core.Core{ Name: "testing notifiers", } @@ -72,11 +76,6 @@ func TestIsBasicType(t *testing.T) { assert.True(t, utils.IsType(example, new(Tester))) } -func TestIsInDatabase(t *testing.T) { - in := isInDatabase(example) - assert.True(t, in) -} - func TestSelectNotification(t *testing.T) { notifier, err := SelectNotification(example) assert.Nil(t, err) @@ -103,7 +102,7 @@ func TestNotification_Update(t *testing.T) { notifier.ApiKey = "USBdu82HDiiuw9327yGYDGw" notifier.ApiSecret = "PQopncow929hUIDHGwiud" notifier.Limits = 10 - _, err = Update(example, notifier) + err = notifier.Update() assert.Nil(t, err) selected, err := SelectNotification(example) @@ -120,11 +119,11 @@ func TestNotification_Update(t *testing.T) { func TestEnableNotification(t *testing.T) { notifier, err := SelectNotification(example) assert.Nil(t, err) - notifier.Enabled = types.NewNullBool(true) - updated, err := Update(example, notifier) + notifier.Enabled = null.NewNullBool(true) + err = notifier.Update() assert.Nil(t, err) - assert.True(t, updated.Enabled.Bool) - assert.True(t, updated.IsRunning()) + assert.True(t, notifier.Enabled.Bool) + assert.True(t, notifier.IsRunning()) } func TestIsEnabled(t *testing.T) { @@ -201,7 +200,7 @@ func TestOnDeletedUser(t *testing.T) { } func TestOnUpdatedCore(t *testing.T) { - OnUpdatedCore(core) + OnUpdatedCore(testCore) assert.Equal(t, 10, len(example.Queue)) } diff --git a/types/null.go b/types/null.go deleted file mode 100644 index f1f80d93..00000000 --- a/types/null.go +++ /dev/null @@ -1,121 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "database/sql" - "encoding/json" -) - -// NewNullString returns a sql.NullString for JSON parsing -func NewNullString(s string) NullString { - return NullString{sql.NullString{s, true}} -} - -// NewNullBool returns a sql.NullBool for JSON parsing -func NewNullBool(s bool) NullBool { - return NullBool{sql.NullBool{s, true}} -} - -// NewNullInt64 returns a sql.NullInt64 for JSON parsing -func NewNullInt64(s int64) NullInt64 { - return NullInt64{sql.NullInt64{s, true}} -} - -// NewNullFloat64 returns a sql.NullFloat64 for JSON parsing -func NewNullFloat64(s float64) NullFloat64 { - return NullFloat64{sql.NullFloat64{s, true}} -} - -// NullInt64 is an alias for sql.NullInt64 data type -type NullInt64 struct { - sql.NullInt64 -} - -// NullBool is an alias for sql.NullBool data type -type NullBool struct { - sql.NullBool -} - -// NullString is an alias for sql.NullString data type -type NullString struct { - sql.NullString -} - -// NullFloat64 is an alias for sql.NullFloat64 data type -type NullFloat64 struct { - sql.NullFloat64 -} - -// MarshalJSON for NullInt64 -func (ni NullInt64) MarshalJSON() ([]byte, error) { - if !ni.Valid { - return []byte("null"), nil - } - return json.Marshal(ni.Int64) -} - -// MarshalJSON for NullFloat64 -func (ni NullFloat64) MarshalJSON() ([]byte, error) { - if !ni.Valid { - return []byte("null"), nil - } - return json.Marshal(ni.Float64) -} - -// MarshalJSON for NullBool -func (nb NullBool) MarshalJSON() ([]byte, error) { - if !nb.Valid { - return []byte("null"), nil - } - return json.Marshal(nb.Bool) -} - -// MarshalJSON for NullString -func (ns NullString) MarshalJSON() ([]byte, error) { - if !ns.Valid { - return []byte("null"), nil - } - return json.Marshal(ns.String) -} - -// Unmarshaler for NullInt64 -func (nf *NullInt64) UnmarshalJSON(b []byte) error { - err := json.Unmarshal(b, &nf.Int64) - nf.Valid = (err == nil) - return err -} - -// Unmarshaler for NullFloat64 -func (nf *NullFloat64) UnmarshalJSON(b []byte) error { - err := json.Unmarshal(b, &nf.Float64) - nf.Valid = (err == nil) - return err -} - -// Unmarshaler for NullBool -func (nf *NullBool) UnmarshalJSON(b []byte) error { - err := json.Unmarshal(b, &nf.Bool) - nf.Valid = (err == nil) - return err -} - -// Unmarshaler for NullString -func (nf *NullString) UnmarshalJSON(b []byte) error { - err := json.Unmarshal(b, &nf.String) - nf.Valid = (err == nil) - return err -} diff --git a/types/null/marshal.go b/types/null/marshal.go new file mode 100644 index 00000000..e5a286d5 --- /dev/null +++ b/types/null/marshal.go @@ -0,0 +1,35 @@ +package null + +import "encoding/json" + +// MarshalJSON for NullInt64 +func (ni NullInt64) MarshalJSON() ([]byte, error) { + if !ni.Valid { + return []byte("null"), nil + } + return json.Marshal(ni.Int64) +} + +// MarshalJSON for NullFloat64 +func (ni NullFloat64) MarshalJSON() ([]byte, error) { + if !ni.Valid { + return []byte("null"), nil + } + return json.Marshal(ni.Float64) +} + +// MarshalJSON for NullBool +func (nb NullBool) MarshalJSON() ([]byte, error) { + if !nb.Valid { + return []byte("null"), nil + } + return json.Marshal(nb.Bool) +} + +// MarshalJSON for NullString +func (ns NullString) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + return json.Marshal(ns.String) +} diff --git a/types/null/types.go b/types/null/types.go new file mode 100644 index 00000000..d9c41231 --- /dev/null +++ b/types/null/types.go @@ -0,0 +1,45 @@ +package null + +import ( + "database/sql" +) + +// NewNullString returns a sql.NullString for JSON parsing +func NewNullString(s string) NullString { + return NullString{sql.NullString{s, true}} +} + +// NewNullBool returns a sql.NullBool for JSON parsing +func NewNullBool(s bool) NullBool { + return NullBool{sql.NullBool{s, true}} +} + +// NewNullInt64 returns a sql.NullInt64 for JSON parsing +func NewNullInt64(s int64) NullInt64 { + return NullInt64{sql.NullInt64{s, true}} +} + +// NewNullFloat64 returns a sql.NullFloat64 for JSON parsing +func NewNullFloat64(s float64) NullFloat64 { + return NullFloat64{sql.NullFloat64{s, true}} +} + +// NullInt64 is an alias for sql.NullInt64 data type +type NullInt64 struct { + sql.NullInt64 +} + +// NullBool is an alias for sql.NullBool data type +type NullBool struct { + sql.NullBool +} + +// NullString is an alias for sql.NullString data type +type NullString struct { + sql.NullString +} + +// NullFloat64 is an alias for sql.NullFloat64 data type +type NullFloat64 struct { + sql.NullFloat64 +} diff --git a/types/null/unmarshal.go b/types/null/unmarshal.go new file mode 100644 index 00000000..7b59ecda --- /dev/null +++ b/types/null/unmarshal.go @@ -0,0 +1,31 @@ +package null + +import "encoding/json" + +// Unmarshaler for NullInt64 +func (nf *NullInt64) UnmarshalJSON(b []byte) error { + err := json.Unmarshal(b, &nf.Int64) + nf.Valid = (err == nil) + return err +} + +// Unmarshaler for NullFloat64 +func (nf *NullFloat64) UnmarshalJSON(b []byte) error { + err := json.Unmarshal(b, &nf.Float64) + nf.Valid = (err == nil) + return err +} + +// Unmarshaler for NullBool +func (nf *NullBool) UnmarshalJSON(b []byte) error { + err := json.Unmarshal(b, &nf.Bool) + nf.Valid = (err == nil) + return err +} + +// Unmarshaler for NullString +func (nf *NullString) UnmarshalJSON(b []byte) error { + err := json.Unmarshal(b, &nf.String) + nf.Valid = (err == nil) + return err +} diff --git a/types/plugin.go b/types/plugin.go deleted file mode 100644 index 0530860b..00000000 --- a/types/plugin.go +++ /dev/null @@ -1,78 +0,0 @@ -package types - -import ( - "github.com/jinzhu/gorm" - "net/http" -) - -type Plugin struct { - Name string - Description string -} - -type PluginObject struct { - Pluginer -} - -type PluginActions interface { - GetInfo() *Info - OnLoad() error -} - -type PluginRepos struct { - Plugins []PluginJSON -} - -type PluginJSON struct { - Name string `json:"name"` - Description string `json:"description"` - Repo string `json:"repo"` - Author string `json:"author"` - Namespace string `json:"namespace"` -} - -type Info struct { - Name string - Description string - Form string -} - -type PluginInfo struct { - Info *Info - Routes []*PluginRoute -} - -type PluginRouting struct { - URL string - Method string - Handler func(http.ResponseWriter, *http.Request) -} - -type Pluginer interface { - Select() *Plugin -} - -type Databaser interface { - StatpingDatabase(*gorm.DB) -} - -type Router interface { - Routes() []*PluginRoute - AddRoute(string, string, http.HandlerFunc) error -} - -type Asseter interface { - Asset(string) ([]byte, error) -} - -type PluginRoute struct { - Url string - Method string - Func http.HandlerFunc -} - -// -//type Notifier interface { -// notifier.Notifier -// notifier.BasicEvents -//} diff --git a/types/safe.go b/types/safe.go deleted file mode 100644 index 18296782..00000000 --- a/types/safe.go +++ /dev/null @@ -1,6 +0,0 @@ -package types - -var ( - SafeService = []string{"domain", "expected_status", "expected", "allow_notifications", "headers", "method", - "port", "timeout", "status_code", "verify_ssl", "post_data", "type", "check_interval", "failures"} -) diff --git a/types/service.go b/types/service.go deleted file mode 100644 index 9ad9fa1b..00000000 --- a/types/service.go +++ /dev/null @@ -1,128 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "crypto/sha1" - "encoding/hex" - "fmt" - "time" -) - -// Service is the main struct for Services -type Service struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Name string `gorm:"column:name" json:"name"` - Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"` - Expected NullString `gorm:"column:expected" json:"expected" scope:"user,admin"` - ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status" scope:"user,admin"` - Interval int `gorm:"default:30;column:check_interval" json:"check_interval" scope:"user,admin"` - Type string `gorm:"column:check_type" json:"type" scope:"user,admin"` - Method string `gorm:"column:method" json:"method" scope:"user,admin"` - PostData NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"` - Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"` - Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"` - Order int `gorm:"default:0;column:order_id" json:"order_id"` - VerifySSL NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"` - Public NullBool `gorm:"default:true;column:public" json:"public"` - GroupId int `gorm:"default:0;column:group_id" json:"group_id"` - Headers NullString `gorm:"column:headers" json:"headers" scope:"user,admin"` - Permalink NullString `gorm:"column:permalink" json:"permalink"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - Online bool `gorm:"-" json:"online"` - Latency float64 `gorm:"-" json:"latency"` - PingTime float64 `gorm:"-" json:"ping_time"` - Online24Hours float32 `gorm:"-" json:"online_24_hours"` - Online7Days float32 `gorm:"-" json:"online_7_days"` - AvgResponse float64 `gorm:"-" json:"avg_response"` - FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"` - Running chan bool `gorm:"-" json:"-"` - Checkpoint time.Time `gorm:"-" json:"-"` - SleepDuration time.Duration `gorm:"-" json:"-"` - LastResponse string `gorm:"-" json:"-"` - AllowNotifications NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"` - UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime - UpdateNotify NullBool `gorm:"default:true;column:notify_all_changes" json:"notify_all_changes" scope:"user,admin"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool` - DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text - SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available - LastStatusCode int `gorm:"-" json:"status_code"` - LastOnline time.Time `gorm:"-" json:"last_success"` - Failures []*Failure `gorm:"-" json:"failures,omitempty" scope:"user,admin"` - Checkins []*Checkin `gorm:"-" json:"checkins,omitempty" scope:"user,admin"` - Stats *Stats `gorm:"-" json:"stats,omitempty"` -} - -type CheckinInterface interface { - Model() *Checkin -} - -type Stater interface { - Fetch() *Stats -} - -type Stats struct { - Failures int `gorm:"-" json:"failures"` - Hits int `gorm:"-" json:"hits"` -} - -// BeforeCreate for Service will set CreatedAt to UTC -func (s *Service) BeforeCreate() (err error) { - if s.CreatedAt.IsZero() { - s.CreatedAt = time.Now().UTC() - s.UpdatedAt = time.Now().UTC() - } - return -} - -func (s *Service) Duration() time.Duration { - return time.Duration(s.Interval) * time.Second -} - -// Start will create a channel for the service checking go routine -func (s *Service) Start() { - if s.IsRunning() { - return - } - s.Running = make(chan bool) -} - -// Close will stop the go routine that is checking if service is online or not -func (s *Service) Close() { - if s.IsRunning() { - close(s.Running) - } -} - -// IsRunning returns true if the service go routine is running -func (s *Service) IsRunning() bool { - if s.Running == nil { - return false - } - select { - case <-s.Running: - return false - default: - return true - } -} - -func (s Service) String() string { - format := fmt.Sprintf("name:%sdomain:%sport:%dtype:%smethod:%s", s.Name, s.Domain, s.Port, s.Type, s.Method) - h := sha1.New() - h.Write([]byte(format)) - return hex.EncodeToString(h.Sum(nil)) -} diff --git a/types/services/checkins.go b/types/services/checkins.go new file mode 100644 index 00000000..3559ef8b --- /dev/null +++ b/types/services/checkins.go @@ -0,0 +1,20 @@ +package services + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/checkins" +) + +// CheckinProcess runs the checkin routine for each checkin attached to service +func CheckinProcess(s *Service) { + for _, c := range s.Checkins() { + c.Start() + go c.CheckinRoutine() + } +} + +func (s *Service) Checkins() []*checkins.Checkin { + var chks []*checkins.Checkin + database.DB().Where("service = ?", s.Id).Find(&chks) + return chks +} diff --git a/types/services/database.go b/types/services/database.go new file mode 100644 index 00000000..479d01d4 --- /dev/null +++ b/types/services/database.go @@ -0,0 +1,79 @@ +package services + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/utils" +) + +var log = utils.Log + +func DB() database.Database { + return database.DB().Model(&Service{}) +} + +func Find(id int64) (*Service, error) { + var service *Service + db := DB().Where("id = ?", id).Find(&service) + return service, db.Error() +} + +func All() []*Service { + var services []*Service + DB().Find(&services) + return services +} + +func (s *Service) Create() error { + + err := s.Create() + if err != nil { + log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err)) + return err + } + allServices[s.Id] = s + + go ServiceCheckQueue(s, true) + reorderServices() + //notifications.OnNewService(s) + + return nil +} + +func (s *Service) Update() error { + db := DB().Update(&s) + + allServices[s.Id] = s + + if !s.AllowNotifications.Bool { + //for _, n := range CoreApp.Notifications { + // notif := n.(notifier.Notifier).Select() + // notif.ResetUniqueQueue(fmt.Sprintf("service_%v", s.Id)) + //} + } + s.Close() + s.Start() + s.SleepDuration = s.Duration() + go ServiceCheckQueue(s, true) + + reorderServices() + //notifier.OnUpdatedService(s.Service) + + return db.Error() +} + +func (s *Service) Delete() error { + db := database.DB().Delete(&s) + + s.Close() + delete(allServices, s.Id) + reorderServices() + //notifier.OnDeletedService(s.Service) + + return db.Error() +} + +func (s *Service) DeleteFailures() error { + query := database.DB().Exec(`DELETE FROM failures WHERE service = ?`, s.Id) + return query.Error() +} diff --git a/types/services/env.go b/types/services/env.go new file mode 100644 index 00000000..168e1f21 --- /dev/null +++ b/types/services/env.go @@ -0,0 +1,59 @@ +package services + +import ( + "bufio" + "github.com/hunterlong/statping/utils" + "github.com/pkg/errors" + "os" +) + +// findServiceByHas will return a service that matches the SHA256 hash of a service +// Service hash example: sha256(name:EXAMPLEdomain:HTTP://DOMAIN.COMport:8080type:HTTPmethod:GET) +func findServiceByHash(hash string) *Service { + for _, service := range All() { + if service.String() == hash { + return service + } + } + return nil +} + +func ServicesFromEnvFile() error { + servicesEnv := utils.Getenv("SERVICES_FILE", "").(string) + if servicesEnv == "" { + return nil + } + + file, err := os.Open(servicesEnv) + if err != nil { + return errors.Wrapf(err, "error opening 'SERVICES_FILE' at: %s", servicesEnv) + } + defer file.Close() + + var serviceLines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + serviceLines = append(serviceLines, scanner.Text()) + } + + if len(serviceLines) == 0 { + return nil + } + + for k, service := range serviceLines { + + svr, err := ValidateService(service) + if err != nil { + return errors.Wrapf(err, "invalid service at index %d in SERVICES_FILE environment variable", k) + } + if findServiceByHash(svr.String()) == nil { + if err := svr.Create(); err != nil { + return errors.Wrapf(err, "could not create service %s", svr.Name) + } + log.Infof("Created new service '%s'", svr.Name) + } + + } + + return nil +} diff --git a/types/services/failures.go b/types/services/failures.go new file mode 100644 index 00000000..4cee05ed --- /dev/null +++ b/types/services/failures.go @@ -0,0 +1,95 @@ +package services + +import ( + "fmt" + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/failures" + "strings" + "time" +) + +func (s *Service) FailuresColumnID() (string, int64) { + return "service", s.Id +} + +func (s *Service) AllFailures() failures.Failurer { + return failures.AllFailures(s) +} + +func (s *Service) LastFailures(amount int) []*failures.Failure { + var fail []*failures.Failure + database.DB().Limit(amount).Find(&fail) + return fail +} + +func (s *Service) FailuresCount() int { + var amount int + failures.DB().Where("service = ?", s.Id).Count(&amount) + return amount +} + +func (s *Service) FailuresSince(t time.Time) []*failures.Failure { + var fails []*failures.Failure + failures.DB().Where("service = ?", s.Id).Find(&fails) + return fails +} + +func (s *Service) DowntimeText() string { + last := s.LastFailures(1) + if len(last) == 0 { + return "" + } + return parseError(last[0]) +} + +// ParseError returns a human readable error for a Failure +func parseError(f *failures.Failure) string { + if f.Method == "checkin" { + return fmt.Sprintf("Checkin is Offline") + } + err := strings.Contains(f.Issue, "connection reset by peer") + if err { + return fmt.Sprintf("Connection Reset") + } + err = strings.Contains(f.Issue, "operation timed out") + if err { + return fmt.Sprintf("HTTP Request Timed Out") + } + err = strings.Contains(f.Issue, "x509: certificate is valid") + if err { + return fmt.Sprintf("SSL Certificate invalid") + } + err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers") + if err { + return fmt.Sprintf("Connection Timed Out") + } + err = strings.Contains(f.Issue, "no such host") + if err { + return fmt.Sprintf("Domain is offline or not found") + } + err = strings.Contains(f.Issue, "HTTP Status Code") + if err { + return fmt.Sprintf("Incorrect HTTP Status Code") + } + err = strings.Contains(f.Issue, "connection refused") + if err { + return fmt.Sprintf("Connection Failed") + } + err = strings.Contains(f.Issue, "can't assign requested address") + if err { + return fmt.Sprintf("Unable to Request Address") + } + err = strings.Contains(f.Issue, "no route to host") + if err { + return fmt.Sprintf("Domain is offline or not found") + } + err = strings.Contains(f.Issue, "i/o timeout") + if err { + return fmt.Sprintf("Connection Timed Out") + } + err = strings.Contains(f.Issue, "Client.Timeout exceeded while reading body") + if err { + return fmt.Sprintf("Timed Out on Response Body") + } + return f.Issue +} diff --git a/types/services/hits.go b/types/services/hits.go new file mode 100644 index 00000000..e418a773 --- /dev/null +++ b/types/services/hits.go @@ -0,0 +1,18 @@ +package services + +import ( + "github.com/hunterlong/statping/types/hits" + "time" +) + +func (s *Service) HitsColumnID() (string, int64) { + return "service", s.Id +} + +func (s *Service) AllHits() hits.Hitters { + return hits.AllHits(s) +} + +func (s *Service) HitsSince(t time.Time) hits.Hitters { + return hits.HitsSince(t, s) +} diff --git a/types/services/methods.go b/types/services/methods.go new file mode 100644 index 00000000..f93004c4 --- /dev/null +++ b/types/services/methods.go @@ -0,0 +1,207 @@ +package services + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + "github.com/hunterlong/statping/types/null" + "github.com/hunterlong/statping/utils" + "net/url" + "sort" + "strconv" + "strings" + "time" +) + +const limitedFailures = 25 + +func (s *Service) Duration() time.Duration { + return time.Duration(s.Interval) * time.Second +} + +// Start will create a channel for the service checking go routine +func (s *Service) Start() { + if s.IsRunning() { + return + } + s.Running = make(chan bool) +} + +// Close will stop the go routine that is checking if service is online or not +func (s *Service) Close() { + if s.IsRunning() { + close(s.Running) + } +} + +// IsRunning returns true if the service go routine is running +func (s *Service) IsRunning() bool { + if s.Running == nil { + return false + } + select { + case <-s.Running: + return false + default: + return true + } +} + +func (s Service) String() string { + format := fmt.Sprintf("name:%sdomain:%sport:%dtype:%smethod:%s", s.Name, s.Domain, s.Port, s.Type, s.Method) + h := sha1.New() + h.Write([]byte(format)) + return hex.EncodeToString(h.Sum(nil)) +} + +// reorderServices will sort the services based on 'order_id' +func reorderServices() { + sort.Sort(ServiceOrder(allServices)) +} + +// SelectAllServices returns a slice of *core.Service to be store on []*core.Services +// should only be called once on startup. +func SelectAllServices(start bool) (map[int64]*Service, error) { + if len(allServices) > 0 { + return allServices, nil + } + + for _, s := range allServices { + if start { + s.Start() + CheckinProcess(s) + } + + fails := s.AllFailures().Last(limitedFailures) + s.Failures = fails + + for _, c := range s.Checkins() { + s.AllCheckins = append(s.AllCheckins, c) + } + + // collect initial service stats + s.UpdateStats() + + allServices[s.Id] = s + } + + reorderServices() + + return allServices, nil +} + +func ValidateService(line string) (*Service, error) { + p, err := url.Parse(line) + if err != nil { + return nil, err + } + newService := new(Service) + + domain := p.Host + newService.Name = niceDomainName(domain, p.Path) + if p.Port() != "" { + newService.Port = int(utils.ToInt(p.Port())) + if p.Scheme != "http" && p.Scheme != "https" { + domain = strings.ReplaceAll(domain, ":"+p.Port(), "") + } + } + newService.Domain = domain + + switch p.Scheme { + case "http", "https": + newService.Type = "http" + newService.Method = "get" + if p.Scheme == "https" { + newService.VerifySSL = null.NewNullBool(true) + } + default: + newService.Type = p.Scheme + } + return newService, nil +} + +func niceDomainName(domain string, paths string) string { + domain = strings.ReplaceAll(domain, "www.", "") + splitPath := strings.Split(paths, "/") + if len(splitPath) == 1 { + return domain + } + var addedName []string + for k, p := range splitPath { + if k > 2 { + break + } + if len(p) > 16 { + addedName = append(addedName, p+"...") + break + } else { + addedName = append(addedName, p) + } + } + return domain + strings.Join(addedName, "/") +} + +func (s *Service) UpdateStats() { + s.Online24Hours = s.OnlineDaysPercent(1) + s.Online7Days = s.OnlineDaysPercent(7) + s.AvgResponse = s.AvgTime() + s.FailuresLast24Hours = len(s.AllFailures().Since(time.Now().UTC().Add(-time.Hour * 24))) + s.Stats = &Stats{ + Failures: s.AllFailures().Count(), + Hits: s.AllHits().Count(), + } +} + +// AvgTime will return the average amount of time for a service to response back successfully +func (s *Service) AvgTime() float64 { + sum := s.AllHits().Sum() + return sum +} + +// AvgUptime will return the average amount of time for a service to response back successfully +func (s *Service) AvgUptime(since time.Time) float64 { + sum := s.AllHits().Sum() + return sum +} + +// OnlineDaysPercent returns the service's uptime percent within last 24 hours +func (s *Service) OnlineDaysPercent(days int) float32 { + ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour) + return s.OnlineSince(ago) +} + +// OnlineSince accepts a time since parameter to return the percent of a service's uptime. +func (s *Service) OnlineSince(ago time.Time) float32 { + failed := s.AllFailures().Since(ago) + if len(failed) == 0 { + s.Online24Hours = 100.00 + return s.Online24Hours + } + total := s.AllHits().Since(ago) + if len(total) == 0 { + s.Online24Hours = 0 + return s.Online24Hours + } + avg := float64(len(failed)) / float64(len(total)) * 100 + avg = 100 - avg + if avg < 0 { + avg = 0 + } + amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10) + s.Online24Hours = float32(amount) + return s.Online24Hours +} + +// Downtime returns the amount of time of a offline service +func (s *Service) Downtime() time.Duration { + hits := s.AllHits().Last(1) + fail := s.AllFailures().Last(1) + if len(fail) == 0 { + return time.Duration(0) + } + if len(fail) == 0 { + return time.Now().UTC().Sub(fail[0].CreatedAt.UTC()) + } + since := fail[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) + return since +} diff --git a/core/checker.go b/types/services/routine.go similarity index 80% rename from core/checker.go rename to types/services/routine.go index 2bfc1d02..cf8360ea 100644 --- a/core/checker.go +++ b/types/services/routine.go @@ -1,26 +1,10 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package core +package services import ( "bytes" "fmt" - "github.com/hunterlong/statping/core/notifier" - "github.com/hunterlong/statping/database" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/hits" "github.com/hunterlong/statping/utils" "github.com/tatsushid/go-fastping" "net" @@ -32,30 +16,19 @@ import ( ) // checkServices will start the checking go routine for each service -func checkServices() { - log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.services))) - for _, s := range CoreApp.services { +func CheckServices() { + log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(allServices))) + for _, s := range allServices { //go CheckinRoutine() time.Sleep(200 * time.Millisecond) // short delay so requests don't run all at the same time. go ServiceCheckQueue(s, true) } } -// Check will run checkHttp for HTTP services and checkTcp for TCP services -// if record param is set to true, it will add a record into the database. -func CheckService(srv *Service, record bool) { - switch srv.Type { - case "http": - CheckHttp(srv, record) - case "tcp", "udp": - CheckTcp(srv, record) - case "icmp": - CheckIcmp(srv, record) - } -} - // CheckQueue is the main go routine for checking a service func ServiceCheckQueue(s *Service, record bool) { + log.Infof("Starting new service '%s', checking every %0.2f seconds.", s.Name, s.Duration().Seconds()) + s.Start() s.Checkpoint = time.Now() s.SleepDuration = (time.Duration(s.Id) * 100) * time.Millisecond CheckLoop: @@ -113,7 +86,7 @@ func isIPv6(address string) bool { } // checkIcmp will send a ICMP ping packet to the service -func CheckIcmp(s *Service, record bool) *types.Service { +func CheckIcmp(s *Service, record bool) *Service { p := fastping.NewPinger() resolveIP := "ip4:icmp" if isIPv6(s.Domain) { @@ -122,7 +95,7 @@ func CheckIcmp(s *Service, record bool) *types.Service { ra, err := net.ResolveIPAddr(resolveIP, s.Domain) if err != nil { recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err)) - return s.Service + return s } p.AddIPAddr(ra) p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) { @@ -132,20 +105,20 @@ func CheckIcmp(s *Service, record bool) *types.Service { err = p.Run() if err != nil { recordFailure(s, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err)) - return s.Service + return s } s.LastResponse = "" - return s.Service + return s } // checkTcp will check a TCP service -func CheckTcp(s *Service, record bool) *types.Service { +func CheckTcp(s *Service, record bool) *Service { dnsLookup, err := dnsCheck(s) if err != nil { if record { recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err)) } - return s.Service + return s } s.PingTime = dnsLookup t1 := time.Now() @@ -161,13 +134,13 @@ func CheckTcp(s *Service, record bool) *types.Service { if record { recordFailure(s, fmt.Sprintf("Dial Error %v", err)) } - return s.Service + return s } if err := conn.Close(); err != nil { if record { recordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err)) } - return s.Service + return s } t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() @@ -175,17 +148,17 @@ func CheckTcp(s *Service, record bool) *types.Service { if record { recordSuccess(s) } - return s.Service + return s } // checkHttp will check a HTTP service -func CheckHttp(s *Service, record bool) *types.Service { +func CheckHttp(s *Service, record bool) *Service { dnsLookup, err := dnsCheck(s) if err != nil { if record { recordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) } - return s.Service + return s } s.PingTime = dnsLookup t1 := time.Now() @@ -210,7 +183,7 @@ func CheckHttp(s *Service, record bool) *types.Service { if record { recordFailure(s, fmt.Sprintf("HTTP Error %v", err)) } - return s.Service + return s } t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() @@ -226,40 +199,40 @@ func CheckHttp(s *Service, record bool) *types.Service { if record { recordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) } - return s.Service + return s } } if s.ExpectedStatus != res.StatusCode { if record { recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) } - return s.Service + return s } if record { recordSuccess(s) } - return s.Service + return s } // recordSuccess will create a new 'hit' record in the database for a successful/online service func recordSuccess(s *Service) { s.LastOnline = time.Now().UTC() - hit := &types.Hit{ + hit := &hits.Hit{ Service: s.Id, Latency: s.Latency, PingTime: s.PingTime, CreatedAt: time.Now().UTC(), } - database.Create(hit) + hit.Create() log.WithFields(utils.ToFields(hit, s)).Infoln(fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000)) - notifier.OnSuccess(s.Service) + //notifier.OnSuccess(s) s.Online = true s.SuccessNotified = true } // recordFailure will create a new 'Failure' record in the database for a offline service func recordFailure(s *Service, issue string) { - fail := &types.Failure{ + fail := &failures.Failure{ Service: s.Id, Issue: issue, PingTime: s.PingTime, @@ -268,9 +241,23 @@ func recordFailure(s *Service, issue string) { } log.WithFields(utils.ToFields(fail, s)). Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000)) - database.Create(fail) + + fail.Create() s.Online = false s.SuccessNotified = false s.DownText = s.DowntimeText() - notifier.OnFailure(s.Service, fail) + //notifier.OnFailure(s, fail) +} + +// Check will run checkHttp for HTTP services and checkTcp for TCP services +// if record param is set to true, it will add a record into the database. +func CheckService(srv *Service, record bool) { + switch srv.Type { + case "http": + CheckHttp(srv, record) + case "tcp", "udp": + CheckTcp(srv, record) + case "icmp": + CheckIcmp(srv, record) + } } diff --git a/types/services/samples.go b/types/services/samples.go new file mode 100644 index 00000000..b6ea7d82 --- /dev/null +++ b/types/services/samples.go @@ -0,0 +1,82 @@ +package services + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" + "time" +) + +func (s *Service) Samples() []database.DbObject { + createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC() + s1 := &Service{ + Name: "Google", + Domain: "https://google.com", + ExpectedStatus: 200, + Interval: 10, + Type: "http", + Method: "GET", + Timeout: 10, + Order: 1, + GroupId: 1, + Permalink: null.NewNullString("google"), + VerifySSL: null.NewNullBool(true), + CreatedAt: createdOn, + } + s2 := &Service{ + Name: "Statping Github", + Domain: "https://github.com/hunterlong/statping", + ExpectedStatus: 200, + Interval: 30, + Type: "http", + Method: "GET", + Timeout: 20, + Order: 2, + Permalink: null.NewNullString("statping_github"), + VerifySSL: null.NewNullBool(true), + CreatedAt: createdOn, + } + s3 := &Service{ + Name: "JSON Users Test", + Domain: "https://jsonplaceholder.typicode.com/users", + ExpectedStatus: 200, + Interval: 60, + Type: "http", + Method: "GET", + Timeout: 30, + Order: 3, + Public: null.NewNullBool(true), + VerifySSL: null.NewNullBool(true), + GroupId: 2, + CreatedAt: createdOn, + } + s4 := &Service{ + Name: "JSON API Tester", + Domain: "https://jsonplaceholder.typicode.com/posts", + ExpectedStatus: 201, + Expected: null.NewNullString(`(title)": "((\\"|[statping])*)"`), + Interval: 30, + Type: "http", + Method: "POST", + PostData: null.NewNullString(`{ "title": "statping", "body": "bar", "userId": 19999 }`), + Timeout: 30, + Order: 4, + Public: null.NewNullBool(true), + VerifySSL: null.NewNullBool(true), + GroupId: 2, + CreatedAt: createdOn, + } + s5 := &Service{ + Name: "Google DNS", + Domain: "8.8.8.8", + Interval: 20, + Type: "tcp", + Port: 53, + Timeout: 120, + Order: 5, + Public: null.NewNullBool(true), + GroupId: 1, + CreatedAt: createdOn, + } + + return []database.DbObject{s1, s2, s3, s4, s5} +} diff --git a/core/services_checkin_test.go b/types/services/services_checkin_test.go similarity index 99% rename from core/services_checkin_test.go rename to types/services/services_checkin_test.go index 177a7ab1..df9c6410 100644 --- a/core/services_checkin_test.go +++ b/types/services/services_checkin_test.go @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package core +package services import ( "github.com/hunterlong/statping/database" diff --git a/core/services_test.go b/types/services/services_test.go similarity index 98% rename from core/services_test.go rename to types/services/services_test.go index 6ae80862..0502cef1 100644 --- a/core/services_test.go +++ b/types/services/services_test.go @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package core +package services import ( "github.com/hunterlong/statping/database" @@ -146,7 +146,7 @@ func TestServiceAvgUptime(t *testing.T) { } func TestCreateService(t *testing.T) { - s := &types.Service{ + s := &services.Service{ Name: "That'll do 🐢", Domain: "https://www.youtube.com/watch?v=rjQtzV9IZ0Q", ExpectedStatus: 200, @@ -169,7 +169,7 @@ func TestViewNewService(t *testing.T) { } func TestCreateFailingHTTPService(t *testing.T) { - s := &types.Service{ + s := &services.Service{ Name: "Bad URL", Domain: "http://localhost/iamnothere", ExpectedStatus: 200, @@ -196,7 +196,7 @@ func TestServiceFailedCheck(t *testing.T) { } func TestCreateFailingTCPService(t *testing.T) { - s := &types.Service{ + s := &services.Service{ Name: "Bad TCP", Domain: "localhost", Port: 5050, diff --git a/types/services/struct.go b/types/services/struct.go new file mode 100644 index 00000000..0150caf2 --- /dev/null +++ b/types/services/struct.go @@ -0,0 +1,94 @@ +package services + +import ( + "github.com/hunterlong/statping/types/checkins" + "github.com/hunterlong/statping/types/failures" + "github.com/hunterlong/statping/types/null" + "time" +) + +var ( + allServices map[int64]*Service +) + +func init() { + allServices = make(map[int64]*Service) +} + +func Services() map[int64]*Service { + return allServices +} + +// Service is the main struct for Services +type Service struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Name string `gorm:"column:name" json:"name"` + Domain string `gorm:"column:domain" json:"domain" private:"true" scope:"user,admin"` + Expected null.NullString `gorm:"column:expected" json:"expected" scope:"user,admin"` + ExpectedStatus int `gorm:"default:200;column:expected_status" json:"expected_status" scope:"user,admin"` + Interval int `gorm:"default:30;column:check_interval" json:"check_interval" scope:"user,admin"` + Type string `gorm:"column:check_type" json:"type" scope:"user,admin"` + Method string `gorm:"column:method" json:"method" scope:"user,admin"` + PostData null.NullString `gorm:"column:post_data" json:"post_data" scope:"user,admin"` + Port int `gorm:"not null;column:port" json:"port" scope:"user,admin"` + Timeout int `gorm:"default:30;column:timeout" json:"timeout" scope:"user,admin"` + Order int `gorm:"default:0;column:order_id" json:"order_id"` + VerifySSL null.NullBool `gorm:"default:false;column:verify_ssl" json:"verify_ssl" scope:"user,admin"` + Public null.NullBool `gorm:"default:true;column:public" json:"public"` + GroupId int `gorm:"default:0;column:group_id" json:"group_id"` + Headers null.NullString `gorm:"column:headers" json:"headers" scope:"user,admin"` + Permalink null.NullString `gorm:"column:permalink" json:"permalink"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + Online bool `gorm:"-" json:"online"` + Latency float64 `gorm:"-" json:"latency"` + PingTime float64 `gorm:"-" json:"ping_time"` + Online24Hours float32 `gorm:"-" json:"online_24_hours"` + Online7Days float32 `gorm:"-" json:"online_7_days"` + AvgResponse float64 `gorm:"-" json:"avg_response"` + FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"` + Running chan bool `gorm:"-" json:"-"` + Checkpoint time.Time `gorm:"-" json:"-"` + SleepDuration time.Duration `gorm:"-" json:"-"` + LastResponse string `gorm:"-" json:"-"` + AllowNotifications null.NullBool `gorm:"default:true;column:allow_notifications" json:"allow_notifications" scope:"user,admin"` + UserNotified bool `gorm:"-" json:"-"` // True if the User was already notified about a Downtime + UpdateNotify null.NullBool `gorm:"default:true;column:notify_all_changes" json:"notify_all_changes" scope:"user,admin"` // This Variable is a simple copy of `core.CoreApp.UpdateNotify.Bool` + DownText string `gorm:"-" json:"-"` // Contains the current generated Downtime Text + SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available + LastStatusCode int `gorm:"-" json:"status_code"` + LastOnline time.Time `gorm:"-" json:"last_success"` + Failures []*failures.Failure `gorm:"-" json:"failures,omitempty" scope:"user,admin"` + AllCheckins []*checkins.Checkin `gorm:"-" json:"checkins,omitempty" scope:"user,admin"` + Stats *Stats `gorm:"-" json:"stats,omitempty"` +} + +type Stats struct { + Failures int `gorm:"-" json:"failures"` + Hits int `gorm:"-" json:"hits"` +} + +// BeforeCreate for Service will set CreatedAt to UTC +func (s *Service) BeforeCreate() (err error) { + if s.CreatedAt.IsZero() { + s.CreatedAt = time.Now().UTC() + s.UpdatedAt = time.Now().UTC() + } + return +} + +// ServiceOrder will reorder the services based on 'order_id' (Order) +type ServiceOrder map[int64]*Service + +// Sort interface for resroting the Services in order +func (c ServiceOrder) Len() int { return len(c) } +func (c ServiceOrder) Swap(i, j int) { c[int64(i)], c[int64(j)] = c[int64(j)], c[int64(i)] } +func (c ServiceOrder) Less(i, j int) bool { + if c[int64(i)] == nil { + return false + } + if c[int64(j)] == nil { + return false + } + return c[int64(i)].Order < c[int64(j)].Order +} diff --git a/types/types.go b/types/types.go deleted file mode 100644 index 35cfd552..00000000 --- a/types/types.go +++ /dev/null @@ -1,59 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "time" -) - -// Hit struct is a 'successful' ping or web response entry for a service. -type Hit struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Service int64 `gorm:"column:service" json:"-"` - Latency float64 `gorm:"column:latency" json:"latency"` - PingTime float64 `gorm:"column:ping_time" json:"ping_time"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` -} - -// BeforeCreate for Hit will set CreatedAt to UTC -func (h *Hit) BeforeCreate() (err error) { - if h.CreatedAt.IsZero() { - h.CreatedAt = time.Now().UTC() - } - return -} - -// DbConfig struct is used for the Db connection and creates the 'config.yml' file -type DbConfig struct { - DbConn string `yaml:"connection" json:"connection"` - DbHost string `yaml:"host" json:"-"` - DbUser string `yaml:"user" json:"-"` - DbPass string `yaml:"password" json:"-"` - DbData string `yaml:"database" json:"-"` - DbPort int `yaml:"port" json:"-"` - ApiKey string `yaml:"api_key" json:"-"` - ApiSecret string `yaml:"api_secret" json:"-"` - Project string `yaml:"-" json:"-"` - Description string `yaml:"-" json:"-"` - Domain string `yaml:"-" json:"-"` - Username string `yaml:"-" json:"-"` - Password string `yaml:"-" json:"-"` - Email string `yaml:"-" json:"-"` - Error error `yaml:"-" json:"-"` - Location string `yaml:"location" json:"-"` - SqlFile string `yaml:"sqlfile,omitempty" json:"-"` - LocalIP string `yaml:"-" json:"-"` -} diff --git a/types/user.go b/types/user.go deleted file mode 100644 index a3ac1a61..00000000 --- a/types/user.go +++ /dev/null @@ -1,50 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package types - -import ( - "time" -) - -// User is the main struct for Users -type User struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Username string `gorm:"type:varchar(100);unique;column:username;" json:"username,omitempty"` - Password string `gorm:"column:password" json:"password,omitempty"` - Email string `gorm:"type:varchar(100);unique;column:email" json:"email,omitempty"` - ApiKey string `gorm:"column:api_key" json:"api_key,omitempty"` - ApiSecret string `gorm:"column:api_secret" json:"api_secret,omitempty"` - Admin NullBool `gorm:"column:administrator" json:"admin,omitempty"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - DatabaseInter `gorm:"-" json:"-"` -} - -// UserInterface interfaces the Db functions -type DatabaseInter interface { - Create() (int64, error) - Update() error - Delete() error -} - -// BeforeCreate for User will set CreatedAt to UTC -func (u *User) BeforeCreate() (err error) { - if u.CreatedAt.IsZero() { - u.CreatedAt = time.Now().UTC() - u.UpdatedAt = time.Now().UTC() - } - return -} diff --git a/types/users/auth.go b/types/users/auth.go new file mode 100644 index 00000000..74bef960 --- /dev/null +++ b/types/users/auth.go @@ -0,0 +1,35 @@ +package users + +import ( + "fmt" + "github.com/prometheus/common/log" + "golang.org/x/crypto/bcrypt" + "time" +) + +// AuthUser will return the User and a boolean if authentication was correct. +// AuthUser accepts username, and password as a string +func AuthUser(username, password string) (*User, bool) { + user, err := FindByUsername(username) + if err != nil { + log.Warnln(fmt.Errorf("user %v not found", username)) + return nil, false + } + + fmt.Println(username, password) + + fmt.Println(username, user.Password) + + if CheckHash(password, user.Password) { + user.UpdatedAt = time.Now().UTC() + user.Update() + return user, true + } + return nil, false +} + +// CheckHash returns true if the password matches with a hashed bcrypt password +func CheckHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} diff --git a/types/users/database.go b/types/users/database.go new file mode 100644 index 00000000..c9c98e14 --- /dev/null +++ b/types/users/database.go @@ -0,0 +1,47 @@ +package users + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/utils" + "time" +) + +func Find(id int64) (*User, error) { + var user *User + db := database.DB().Model(&User{}).Where("id = ?", id).Find(&user) + return user, db.Error() +} + +func FindByUsername(username string) (*User, error) { + var user *User + db := database.DB().Model(&User{}).Where("username = ?", username).Find(&user) + return user, db.Error() +} + +func All() []*User { + var users []*User + database.DB().Model(&User{}).Find(&users) + return users +} + +func (u *User) Create() error { + u.CreatedAt = time.Now().UTC() + u.Password = utils.HashPassword(u.Password) + u.ApiKey = utils.NewSHA1Hash(5) + u.ApiSecret = utils.NewSHA1Hash(10) + + db := database.DB().Create(&u) + return db.Error() +} + +func (u *User) Update() error { + u.ApiKey = utils.NewSHA1Hash(5) + u.ApiSecret = utils.NewSHA1Hash(10) + db := database.DB().Update(&u) + return db.Error() +} + +func (u *User) Delete() error { + db := database.DB().Delete(&u) + return db.Error() +} diff --git a/types/users/sample.go b/types/users/sample.go new file mode 100644 index 00000000..da335c89 --- /dev/null +++ b/types/users/sample.go @@ -0,0 +1,31 @@ +package users + +import ( + "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types/null" +) + +func (u *User) Samples() []database.DbObject { + + var samples []database.DbObject + + u2 := &User{ + Username: "testadmin", + Password: "password123", + Email: "info@betatude.com", + Admin: null.NewNullBool(true), + } + + samples = append(samples, u2) + + u3 := &User{ + Username: "testadmin2", + Password: "password123", + Email: "info@adminhere.com", + Admin: null.NewNullBool(true), + } + + samples = append(samples, u3) + + return samples +} diff --git a/types/users/struct.go b/types/users/struct.go new file mode 100644 index 00000000..50e97b3c --- /dev/null +++ b/types/users/struct.go @@ -0,0 +1,28 @@ +package users + +import ( + "github.com/hunterlong/statping/types/null" + "time" +) + +// User is the main struct for Users +type User struct { + Id int64 `gorm:"primary_key;column:id" json:"id"` + Username string `gorm:"type:varchar(100);unique;column:username;" json:"username,omitempty"` + Password string `gorm:"column:password" json:"password,omitempty"` + Email string `gorm:"type:varchar(100);unique;column:email" json:"email,omitempty"` + ApiKey string `gorm:"column:api_key" json:"api_key,omitempty"` + ApiSecret string `gorm:"column:api_secret" json:"api_secret,omitempty"` + Admin null.NullBool `gorm:"column:administrator" json:"admin,omitempty"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} + +// BeforeCreate for User will set CreatedAt to UTC +func (u *User) BeforeCreate() (err error) { + if u.CreatedAt.IsZero() { + u.CreatedAt = time.Now().UTC() + u.UpdatedAt = time.Now().UTC() + } + return +} diff --git a/core/users_test.go b/types/users/users_test.go similarity index 99% rename from core/users_test.go rename to types/users/users_test.go index acd8f9d6..06010c51 100644 --- a/core/users_test.go +++ b/types/users/users_test.go @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package core +package users import ( "github.com/hunterlong/statping/database" diff --git a/utils/log.go b/utils/log.go index c78744b1..0abf1c61 100644 --- a/utils/log.go +++ b/utils/log.go @@ -18,7 +18,7 @@ package utils import ( "fmt" "github.com/fatih/structs" - "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/types/null" Logger "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" "io" @@ -56,7 +56,7 @@ func (t *hook) Levels() []Logger.Level { // ToFields accepts any amount of interfaces to create a new mapping for log.Fields. You will need to // turn on verbose mode by starting Statping with "-v". This function will convert a struct of to the // base struct name, and each field into it's own mapping, for example: -// type "*types.Service", on string field "Name" converts to "service_name=value". There is also an +// type "*services.Service", on string field "Name" converts to "service_name=value". There is also an // additional field called "_pointer" that will return the pointer hex value. func ToFields(d ...interface{}) map[string]interface{} { if !Log.IsLevelEnabled(Logger.DebugLevel) { @@ -83,13 +83,13 @@ func ToFields(d ...interface{}) map[string]interface{} { // replaceVal accepts an interface to be converted into human readable type func replaceVal(d interface{}) interface{} { switch v := d.(type) { - case types.NullBool: + case null.NullBool: return v.Bool - case types.NullString: + case null.NullString: return v.String - case types.NullFloat64: + case null.NullFloat64: return v.Float64 - case types.NullInt64: + case null.NullInt64: return v.Int64 case string: if len(v) > 500 { diff --git a/utils/utils.go b/utils/utils.go index dba2cb7a..a884aa98 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -18,18 +18,15 @@ package utils import ( "context" "crypto/tls" - "encoding/json" "errors" "fmt" "github.com/ararog/timeago" - "github.com/hunterlong/statping/types" "io" "io/ioutil" "math" "math/rand" "net" "net/http" - "net/url" "os" "os/exec" "reflect" @@ -86,14 +83,6 @@ func Getenv(key string, defaultValue interface{}) interface{} { return d } return ok - - case []*types.Service: - var services []*types.Service - if err := json.Unmarshal([]byte(val), services); err != nil { - Log.Error("Incorrect formatting with SERVICE environment variable") - return []*types.Service{} - } - return services default: return val } @@ -442,57 +431,6 @@ func HttpRequest(url, method string, content interface{}, headers []string, body return contents, resp, err } -func ValidateService(line string) (*types.Service, error) { - p, err := url.Parse(line) - if err != nil { - return nil, err - } - newService := new(types.Service) - - domain := p.Host - newService.Name = niceDomainName(domain, p.Path) - if p.Port() != "" { - newService.Port = int(ToInt(p.Port())) - if p.Scheme != "http" && p.Scheme != "https" { - domain = strings.ReplaceAll(domain, ":"+p.Port(), "") - } - } - newService.Domain = domain - - switch p.Scheme { - case "http", "https": - newService.Type = "http" - newService.Method = "get" - if p.Scheme == "https" { - newService.VerifySSL = types.NewNullBool(true) - } - default: - newService.Type = p.Scheme - } - return newService, nil -} - -func niceDomainName(domain string, paths string) string { - domain = strings.ReplaceAll(domain, "www.", "") - splitPath := strings.Split(paths, "/") - if len(splitPath) == 1 { - return domain - } - var addedName []string - for k, p := range splitPath { - if k > 2 { - break - } - if len(p) > 16 { - addedName = append(addedName, p+"...") - break - } else { - addedName = append(addedName, p) - } - } - return domain + strings.Join(addedName, "/") -} - const ( B = 0x100 N = 0x1000