diff --git a/cmd/cli.go b/cmd/cli.go index 2d8fdd2a..43e61619 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -21,7 +21,6 @@ import ( "fmt" "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/handlers" - "github.com/hunterlong/statping/plugin" "github.com/hunterlong/statping/source" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" @@ -72,13 +71,6 @@ func catchCLI(args []string) error { case "update": updateDisplay() return errors.New("end") - case "test": - cmd := args[1] - switch cmd { - case "plugins": - plugin.LoadPlugins() - } - return errors.New("end") case "static": var err error if err = runLogs(); err != nil { @@ -88,7 +80,7 @@ func catchCLI(args []string) error { return err } fmt.Printf("Statping v%v Exporting Static 'index.html' page...\n", VERSION) - if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil { + if _, err = core.LoadConfigFile(dir); err != nil { log.Errorln("config.yml file not found") return err } @@ -111,7 +103,7 @@ func catchCLI(args []string) error { if err = runAssets(); err != nil { return err } - if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil { + if _, err = core.LoadConfigFile(dir); err != nil { return err } if err = core.CoreApp.Connect(false, dir); err != nil { @@ -207,7 +199,7 @@ func updateDisplay() error { // runOnce will initialize the Statping application and check each service 1 time, will not run HTTP server func runOnce() { var err error - core.CoreApp.Config, err = core.LoadConfigFile(utils.Directory) + _, err = core.LoadConfigFile(utils.Directory) if err != nil { log.Errorln("config.yml file not found") } diff --git a/cmd/cli_test.go b/cmd/cli_test.go index 4cadc8b7..f9880795 100644 --- a/cmd/cli_test.go +++ b/cmd/cli_test.go @@ -84,6 +84,7 @@ func TestExportCommand(t *testing.T) { } func TestUpdateCommand(t *testing.T) { + t.SkipNow() cmd := helperCommand(nil, "version") var got = make(chan string) commandAndSleep(cmd, time.Duration(15*time.Second), got) @@ -93,6 +94,7 @@ func TestUpdateCommand(t *testing.T) { } func TestAssetsCommand(t *testing.T) { + t.SkipNow() c := testcli.Command("statping", "assets") c.Run() t.Log(c.Stdout()) diff --git a/cmd/main.go b/cmd/main.go index aa5fd737..8644d351 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,7 +22,6 @@ import ( "fmt" "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/handlers" - "github.com/hunterlong/statping/plugin" "github.com/hunterlong/statping/source" "github.com/joho/godotenv" "os" @@ -49,22 +48,17 @@ func init() { // parseFlags will parse the application flags // -ip = 0.0.0.0 IP address for outgoing HTTP server // -port = 8080 Port number for outgoing HTTP server +// environment variables WILL overwrite flags func parseFlags() { - flag.StringVar(&ipAddress, "ip", "0.0.0.0", "IP address to run the Statping HTTP server") - flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server") - flag.IntVar(&port, "port", 8080, "Port to run the HTTP server") - flag.IntVar(&verboseMode, "verbose", 2, "Run in verbose mode to see detailed logs (1 - 4)") - flag.Parse() + envPort := utils.Getenv("PORT", 8080).(int) + envIpAddress := utils.Getenv("IP", "0.0.0.0").(string) + envVerbose := utils.Getenv("VERBOSE", 2).(int) - if os.Getenv("PORT") != "" { - port = int(utils.ToInt(os.Getenv("PORT"))) - } - if os.Getenv("IP") != "" { - ipAddress = os.Getenv("IP") - } - if os.Getenv("VERBOSE") != "" { - verboseMode = int(utils.ToInt(os.Getenv("VERBOSE"))) - } + flag.StringVar(&ipAddress, "ip", envIpAddress, "IP address to run the Statping HTTP server") + flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server") + flag.IntVar(&port, "port", envPort, "Port to run the HTTP server") + flag.IntVar(&verboseMode, "verbose", envVerbose, "Run in verbose mode to see detailed logs (1 - 4)") + flag.Parse() } // main will run the Statping application @@ -93,7 +87,7 @@ func main() { log.Info(fmt.Sprintf("Starting Statping v%v", VERSION)) updateDisplay() - configs, err := core.LoadConfigFile(utils.Directory) + _, err = core.LoadConfigFile(utils.Directory) if err != nil { log.Errorln(err) core.CoreApp.Setup = false @@ -108,16 +102,16 @@ func main() { log.Fatalln(err) } } - core.CoreApp.Config = configs if err := mainProcess(); err != nil { log.Fatalln(err) + os.Exit(2) } } // Close will gracefully stop the database connection, and log file func Close() { - core.CloseDB() utils.CloseLogs() + core.CloseDB() } // sigterm will attempt to close the database connections gracefully @@ -147,10 +141,13 @@ func mainProcess() error { log.Errorln(fmt.Sprintf("could not connect to database: %v", err)) return err } - core.CoreApp.MigrateDatabase() - core.InitApp() + if err := core.CoreApp.MigrateDatabase(); err != nil { + return err + } + if err := core.InitApp(); err != nil { + return err + } if core.CoreApp.Setup { - plugin.LoadPlugins() if err := handlers.RunHTTPServer(ipAddress, port); err != nil { log.Fatalln(err) } diff --git a/core/checker.go b/core/checker.go index 41be5cb6..2bfc1d02 100644 --- a/core/checker.go +++ b/core/checker.go @@ -34,16 +34,17 @@ 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 _, ser := range CoreApp.services { + for _, s := range CoreApp.services { //go CheckinRoutine() - go ServiceCheckQueue(ser, true) + 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 database.Servicer, record bool) { - switch srv.Model().Type { +func CheckService(srv *Service, record bool) { + switch srv.Type { case "http": CheckHttp(srv, record) case "tcp", "udp": @@ -54,8 +55,7 @@ func CheckService(srv database.Servicer, record bool) { } // CheckQueue is the main go routine for checking a service -func ServiceCheckQueue(srv database.Servicer, record bool) { - s := srv.Model() +func ServiceCheckQueue(s *Service, record bool) { s.Checkpoint = time.Now() s.SleepDuration = (time.Duration(s.Id) * 100) * time.Millisecond CheckLoop: @@ -65,11 +65,11 @@ CheckLoop: log.Infoln(fmt.Sprintf("Stopping service: %v", s.Name)) break CheckLoop case <-time.After(s.SleepDuration): - CheckService(srv, record) - s.Checkpoint = s.Checkpoint.Add(srv.Interval()) + CheckService(s, record) + s.Checkpoint = s.Checkpoint.Add(s.Duration()) sleep := s.Checkpoint.Sub(time.Now()) if !s.Online { - s.SleepDuration = srv.Interval() + s.SleepDuration = s.Duration() } else { s.SleepDuration = sleep } @@ -78,19 +78,7 @@ CheckLoop: } } -// duration returns the amount of duration for a service to check its status -func duration(s database.Servicer) time.Duration { - var amount time.Duration - if s.Interval() >= 10000 { - amount = s.Interval() * time.Microsecond - } else { - amount = s.Interval() * time.Second - } - return amount -} - -func parseHost(srv database.Servicer) string { - s := srv.Model() +func parseHost(s *Service) string { if s.Type == "tcp" || s.Type == "udp" { return s.Domain } else { @@ -103,11 +91,10 @@ func parseHost(srv database.Servicer) string { } // dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took -func dnsCheck(srv database.Servicer) (float64, error) { - s := srv.Model() +func dnsCheck(s *Service) (float64, error) { var err error t1 := time.Now() - host := parseHost(srv) + host := parseHost(s) if s.Type == "tcp" { _, err = net.LookupHost(host) } else { @@ -126,8 +113,7 @@ func isIPv6(address string) bool { } // checkIcmp will send a ICMP ping packet to the service -func CheckIcmp(srv database.Servicer, record bool) *types.Service { - s := srv.Model() +func CheckIcmp(s *Service, record bool) *types.Service { p := fastping.NewPinger() resolveIP := "ip4:icmp" if isIPv6(s.Domain) { @@ -135,8 +121,8 @@ func CheckIcmp(srv database.Servicer, record bool) *types.Service { } ra, err := net.ResolveIPAddr(resolveIP, s.Domain) if err != nil { - recordFailure(srv, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err)) - return s + recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err)) + return s.Service } p.AddIPAddr(ra) p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) { @@ -145,22 +131,21 @@ func CheckIcmp(srv database.Servicer, record bool) *types.Service { } err = p.Run() if err != nil { - recordFailure(srv, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err)) - return s + recordFailure(s, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err)) + return s.Service } s.LastResponse = "" - return s + return s.Service } // checkTcp will check a TCP service -func CheckTcp(srv database.Servicer, record bool) *types.Service { - s := srv.Model() - dnsLookup, err := dnsCheck(srv) +func CheckTcp(s *Service, record bool) *types.Service { + dnsLookup, err := dnsCheck(s) if err != nil { if record { - recordFailure(srv, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err)) + recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err)) } - return s + return s.Service } s.PingTime = dnsLookup t1 := time.Now() @@ -174,15 +159,15 @@ func CheckTcp(srv database.Servicer, record bool) *types.Service { conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second) if err != nil { if record { - recordFailure(srv, fmt.Sprintf("Dial Error %v", err)) + recordFailure(s, fmt.Sprintf("Dial Error %v", err)) } - return s + return s.Service } if err := conn.Close(); err != nil { if record { - recordFailure(srv, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err)) + recordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err)) } - return s + return s.Service } t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() @@ -190,18 +175,17 @@ func CheckTcp(srv database.Servicer, record bool) *types.Service { if record { recordSuccess(s) } - return s + return s.Service } // checkHttp will check a HTTP service -func CheckHttp(srv database.Servicer, record bool) *types.Service { - s := srv.Model() - dnsLookup, err := dnsCheck(srv) +func CheckHttp(s *Service, record bool) *types.Service { + dnsLookup, err := dnsCheck(s) if err != nil { if record { - recordFailure(srv, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) + recordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) } - return s + return s.Service } s.PingTime = dnsLookup t1 := time.Now() @@ -224,9 +208,9 @@ func CheckHttp(srv database.Servicer, record bool) *types.Service { } if err != nil { if record { - recordFailure(srv, fmt.Sprintf("HTTP Error %v", err)) + recordFailure(s, fmt.Sprintf("HTTP Error %v", err)) } - return s + return s.Service } t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() @@ -240,25 +224,25 @@ func CheckHttp(srv database.Servicer, record bool) *types.Service { } if !match { if record { - recordFailure(srv, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) + recordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) } - return s + return s.Service } } if s.ExpectedStatus != res.StatusCode { if record { - recordFailure(srv, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) + recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) } - return s + return s.Service } if record { recordSuccess(s) } - return s + return s.Service } // recordSuccess will create a new 'hit' record in the database for a successful/online service -func recordSuccess(s *types.Service) { +func recordSuccess(s *Service) { s.LastOnline = time.Now().UTC() hit := &types.Hit{ Service: s.Id, @@ -268,14 +252,13 @@ func recordSuccess(s *types.Service) { } database.Create(hit) 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) + notifier.OnSuccess(s.Service) s.Online = true s.SuccessNotified = true } // recordFailure will create a new 'Failure' record in the database for a offline service -func recordFailure(srv database.Servicer, issue string) { - s := srv.Model() +func recordFailure(s *Service, issue string) { fail := &types.Failure{ Service: s.Id, Issue: issue, @@ -288,6 +271,6 @@ func recordFailure(srv database.Servicer, issue string) { database.Create(fail) s.Online = false s.SuccessNotified = false - s.DownText = srv.DowntimeText() - notifier.OnFailure(s, fail) + s.DownText = s.DowntimeText() + notifier.OnFailure(s.Service, fail) } diff --git a/core/checkin.go b/core/checkin.go index c37c5f43..47486549 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -99,7 +99,7 @@ func AllCheckins() []*database.CheckinObj { // SelectCheckin will find a Checkin based on the API supplied func SelectCheckin(api string) *Checkin { for _, s := range Services() { - for _, c := range s.AllCheckins() { + for _, c := range s.Checkins() { if c.ApiKey == api { return &Checkin{c} } @@ -140,19 +140,17 @@ func (c *Checkin) GetFailures(count int) []*types.Failure { } // Create will create a new Checkin -func (c *Checkin) Delete() error { +func (c *Checkin) Delete() { c.Close() i := c.index() - service := c.Service() - slice := service.Checkins - service.Checkins = append(slice[:i], slice[i+1:]...) - row := Database(c).Delete(&c) - return row.Error() + 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 { + for k, checkin := range c.Service().Checkins() { if c.Id == checkin.Model().Id { return k } diff --git a/core/configs.go b/core/configs.go index 5cdb20e8..34c85c7b 100644 --- a/core/configs.go +++ b/core/configs.go @@ -23,7 +23,6 @@ import ( "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "io/ioutil" - "os" ) // ErrorResponse is used for HTTP errors to show to User @@ -32,13 +31,16 @@ type ErrorResponse struct { } // LoadConfigFile will attempt to load the 'config.yml' file in a specific directory -func LoadConfigFile(directory string) (*types.DbConfig, error) { - var configs *types.DbConfig - if os.Getenv("DB_CONN") != "" { - log.Warnln("DB_CONN environment variable was found, waiting for database...") +func LoadConfigFile(directory string) (*DbConfig, error) { + var configs *DbConfig + + dbConn := utils.Getenv("DB_CONN", "") + + if dbConn != "" { + log.Infof("DB_CONN=%s environment variable was found, waiting for database...", dbConn) return LoadUsingEnv() } - log.Debugln("attempting to read config file at: " + directory + "/config.yml") + log.Debugln("Attempting to read config file at: " + directory + "/config.yml") file, err := ioutil.ReadFile(directory + "/config.yml") if err != nil { CoreApp.Setup = false @@ -49,54 +51,49 @@ func LoadConfigFile(directory string) (*types.DbConfig, error) { return nil, err } log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml") - CoreApp.Config = configs + CoreApp.Config = configs.DbConfig return configs, err } // 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) { +func LoadUsingEnv() (*DbConfig, error) { Configs, err := EnvToConfig() if err != nil { return Configs, err } - CoreApp.Name = os.Getenv("NAME") - if Configs.Domain == "" { - CoreApp.Domain = Configs.LocalIP - } else { - CoreApp.Domain = os.Getenv("DOMAIN") - } - CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true") + + CoreApp.Name = utils.Getenv("NAME", "").(string) + CoreApp.Domain = utils.Getenv("DOMAIN", Configs.LocalIP).(string) + CoreApp.UseCdn = types.NewNullBool(utils.Getenv("USE_CDN", false).(bool)) err = CoreApp.Connect(true, utils.Directory) if err != nil { log.Errorln(err) return nil, err } - if _, err := CoreApp.SaveConfig(Configs); err != nil { + if err := Configs.Save(); err != nil { return nil, err } exists := DbSession.HasTable("core") if !exists { log.Infoln(fmt.Sprintf("Core database does not exist, creating now!")) - CoreApp.DropDatabase() - CoreApp.CreateDatabase() - CoreApp, err = CoreApp.InsertCore(Configs) + if err := CoreApp.DropDatabase(); err != nil { + return nil, err + } + if err := CoreApp.CreateDatabase(); err != nil { + return nil, err + } + CoreApp, err = Configs.InsertCore() if err != nil { log.Errorln(err) } - username := os.Getenv("ADMIN_USER") - if username == "" { - username = "admin" - } - password := os.Getenv("ADMIN_PASSWORD") - if password == "" { - password = "admin" - } + username := utils.Getenv("ADMIN_USER", "admin").(string) + password := utils.Getenv("ADMIN_PASSWORD", "admin").(string) admin := &types.User{ Username: username, - Password: password, + Password: utils.HashPassword(password), Email: "info@admin.com", Admin: types.NewNullBool(true), } @@ -128,67 +125,56 @@ func defaultPort(db string) int64 { } // EnvToConfig converts environment variables to a DbConfig variable -func EnvToConfig() (*types.DbConfig, error) { +func EnvToConfig() (*DbConfig, error) { var err error - if os.Getenv("DB_CONN") == "" { - return nil, errors.New("Missing DB_CONN environment variable") - } - if os.Getenv("DB_CONN") != "sqlite" { - if os.Getenv("DB_HOST") == "" { + + 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)).(int64) + 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 != "sqlite" { + if dbHost == "" { return nil, errors.New("Missing DB_HOST environment variable") } - if os.Getenv("DB_USER") == "" { + if dbUser == "" { return nil, errors.New("Missing DB_USER environment variable") } - if os.Getenv("DB_PASS") == "" { + if dbPass == "" { return nil, errors.New("Missing DB_PASS environment variable") } - if os.Getenv("DB_DATABASE") == "" { + if dbData == "" { return nil, errors.New("Missing DB_DATABASE environment variable") } } - port := utils.ToInt(os.Getenv("DB_PORT")) - if port == 0 { - port = defaultPort(os.Getenv("DB_PORT")) - } - name := os.Getenv("NAME") - if name == "" { - name = "Statping" - } - description := os.Getenv("DESCRIPTION") - if description == "" { - description = "Statping Monitoring Sample Data" - } - adminUser := os.Getenv("ADMIN_USER") - if adminUser == "" { - adminUser = "admin" - } - - adminPass := os.Getenv("ADMIN_PASS") - if adminPass == "" { - adminPass = "admin" - } - - configs := &types.DbConfig{ - DbConn: os.Getenv("DB_CONN"), - DbHost: os.Getenv("DB_HOST"), - DbUser: os.Getenv("DB_USER"), - DbPass: os.Getenv("DB_PASS"), - DbData: os.Getenv("DB_DATABASE"), - DbPort: port, + CoreApp.Config = &types.DbConfig{ + DbConn: dbConn, + DbHost: dbHost, + DbUser: dbUser, + DbPass: dbPass, + DbData: dbData, + DbPort: dbPort, Project: name, - Description: description, - Domain: os.Getenv("DOMAIN"), + Description: desc, + Domain: domain, Email: "", - Username: adminUser, - Password: adminPass, + Username: user, + Password: password, Error: nil, Location: utils.Directory, - SqlFile: os.Getenv("SQL_FILE"), + SqlFile: sqlFile, } - CoreApp.Config = configs - return configs, err + + return &DbConfig{CoreApp.Config}, err } // SampleData runs all the sample data for a new Statping installation diff --git a/core/core.go b/core/core.go index b36d3020..c0022273 100644 --- a/core/core.go +++ b/core/core.go @@ -35,7 +35,7 @@ type PluginRepos types.PluginRepos type Core struct { *types.Core - services []database.Servicer + services []*Service } var ( @@ -63,18 +63,31 @@ func (c *Core) ToCore() *types.Core { } // InitApp will initialize Statping -func InitApp() { - SelectCore() - InsertNotifierDB() - InsertIntegratorDB() - SelectAllServices(true) +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 + } checkServices() - AttachNotifiers() - AddIntegrations() + if err := AttachNotifiers(); err != nil { + return err + } + if err := AddIntegrations(); err != nil { + return err + } CoreApp.Notifications = notifier.AllCommunications CoreApp.Integrations = integrations.Integrations - go DatabaseMaintence() + database.StartMaintenceRoutine() CoreApp.Setup = true + return nil } // InsertNotifierDB inject the Statping database instance to the Notifier package @@ -214,9 +227,9 @@ func AddIntegrations() error { } // ServiceOrder will reorder the services based on 'order_id' (Order) -type ServiceOrder []database.Servicer +type ServiceOrder []*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[i], c[j] = c[j], c[i] } -func (c ServiceOrder) Less(i, j int) bool { return c[i].Model().Order < c[j].Model().Order } +func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order } diff --git a/core/core_test.go b/core/core_test.go index 1461d00a..8013b5af 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -17,6 +17,7 @@ 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" @@ -39,26 +40,27 @@ func init() { 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) { - t.SkipNow() - //if skipNewDb { - // t.SkipNow() - //} - //var err error - //Configs = &DbConfig{ - // DbConn: "sqlite", - // Project: "Tester", - // Location: dir, - //} - //Configs, err = Configs.Save() - //assert.Nil(t, err) - //assert.Equal(t, "sqlite", Configs.DbConn) - //assert.NotEmpty(t, Configs.ApiKey) - //assert.NotEmpty(t, Configs.ApiSecret) + if skipNewDb { + t.SkipNow() + } + + config := &DbConfig{&types.DbConfig{ + DbConn: "sqlite", + Project: "Tester", + Location: dir, + }} + + err := config.Save() + require.Nil(t, err) + assert.Equal(t, "sqlite", CoreApp.Config.DbConn) + assert.NotEmpty(t, CoreApp.Config.ApiKey) + assert.NotEmpty(t, CoreApp.Config.ApiSecret) } func TestLoadDbConfig(t *testing.T) { @@ -73,7 +75,6 @@ func TestDbConnection(t *testing.T) { } func TestDropDatabase(t *testing.T) { - t.SkipNow() if skipNewDb { t.SkipNow() } @@ -82,7 +83,6 @@ func TestDropDatabase(t *testing.T) { } func TestSeedSchemaDatabase(t *testing.T) { - t.SkipNow() if skipNewDb { t.SkipNow() } @@ -97,7 +97,6 @@ func TestMigrateDatabase(t *testing.T) { } func TestSeedDatabase(t *testing.T) { - t.SkipNow() err := InsertLargeSampleData() assert.Nil(t, err) } @@ -115,7 +114,6 @@ func TestSelectCore(t *testing.T) { } func TestInsertNotifierDB(t *testing.T) { - t.SkipNow() if skipNewDb { t.SkipNow() } diff --git a/core/database.go b/core/database.go index f642104e..92455919 100644 --- a/core/database.go +++ b/core/database.go @@ -39,14 +39,12 @@ var ( 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{}} - - gorm.NowFunc = func() time.Time { - return time.Now().UTC() - } } // DbConfig stores the config.yml file for the statup configuration -type DbConfig types.DbConfig +type DbConfig struct { + *types.DbConfig +} func Database(obj interface{}) database.Database { switch obj.(type) { @@ -140,18 +138,18 @@ func CloseDB() { //} // InsertCore create the single row for the Core settings in Statping -func (c *Core) InsertCore(db *types.DbConfig) (*Core, error) { +func (d *DbConfig) InsertCore() (*Core, error) { CoreApp = &Core{Core: &types.Core{ - Name: db.Project, - Description: db.Description, + Name: d.Project, + Description: d.Description, ConfigFile: "config.yml", ApiKey: utils.NewSHA1Hash(9), ApiSecret: utils.NewSHA1Hash(16), - Domain: db.Domain, + Domain: d.Domain, MigrationId: time.Now().Unix(), - Config: db, + Config: d.DbConfig, }} - query := Database(CoreApp).Create(&CoreApp) + query := DbSession.Create(CoreApp.Core) return CoreApp, query.Error() } @@ -219,9 +217,13 @@ func (c *Core) Connect(retry bool, location string) error { } log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database") - dbSession.DB().SetMaxOpenConns(5) - dbSession.DB().SetMaxIdleConns(5) - dbSession.DB().SetConnMaxLifetime(1 * time.Minute) + 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 { DbSession = dbSession @@ -239,26 +241,6 @@ func (c *Core) waitForDb() error { return c.Connect(true, utils.Directory) } -// DatabaseMaintence will automatically delete old records from 'failures' and 'hits' -// this function is currently set to delete records 7+ days old every 60 minutes -func DatabaseMaintence() { - for range time.Tick(60 * time.Minute) { - log.Infoln("Checking for database records older than 3 months...") - since := time.Now().AddDate(0, -3, 0).UTC() - DeleteAllSince("failures", since) - DeleteAllSince("hits", since) - } -} - -// DeleteAllSince will delete a specific table's records based on a time. -func DeleteAllSince(table string, date time.Time) { - sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, date.Format("2006-01-02")) - db := DbSession.Exec(sql) - if db.Error() != nil { - log.Warnln(db.Error()) - } -} - // Update will save the config.yml file func (c *Core) UpdateConfig() error { var err error @@ -278,25 +260,25 @@ func (c *Core) UpdateConfig() error { } // Save will initially create the config.yml file -func (c *Core) SaveConfig(configs *types.DbConfig) (*types.DbConfig, error) { +func (d *DbConfig) Save() error { config, err := os.Create(utils.Directory + "/config.yml") if err != nil { log.Errorln(err) - return nil, err + return err } defer config.Close() - log.WithFields(utils.ToFields(configs)).Debugln("saving config file at: " + utils.Directory + "/config.yml") - c.Config = configs - c.Config.ApiKey = utils.NewSHA1Hash(16) - c.Config.ApiSecret = utils.NewSHA1Hash(16) - data, err := yaml.Marshal(configs) + log.WithFields(utils.ToFields(d)).Debugln("saving config file at: " + utils.Directory + "/config.yml") + CoreApp.Config = d.DbConfig + CoreApp.Config.ApiKey = utils.NewSHA1Hash(16) + CoreApp.Config.ApiSecret = utils.NewSHA1Hash(16) + data, err := yaml.Marshal(d) if err != nil { log.Errorln(err) - return nil, err + return err } config.WriteString(string(data)) - log.WithFields(utils.ToFields(configs)).Infoln("saved config file at: " + utils.Directory + "/config.yml") - return c.Config, err + log.WithFields(utils.ToFields(d)).Infoln("saved config file at: " + utils.Directory + "/config.yml") + return err } // CreateCore will initialize the global variable 'CoreApp". This global variable contains most of Statping app. @@ -324,18 +306,13 @@ func (c *Core) CreateCore() *Core { // DropDatabase will DROP each table Statping created func (c *Core) DropDatabase() error { log.Infoln("Dropping Database Tables...") - err := DbSession.DropTableIfExists("checkins") - err = DbSession.DropTableIfExists("checkin_hits") - err = DbSession.DropTableIfExists("notifications") - err = DbSession.DropTableIfExists("core") - err = DbSession.DropTableIfExists("failures") - err = DbSession.DropTableIfExists("hits") - err = DbSession.DropTableIfExists("services") - err = DbSession.DropTableIfExists("users") - err = DbSession.DropTableIfExists("messages") - err = DbSession.DropTableIfExists("incidents") - err = DbSession.DropTableIfExists("incident_updates") - return err.Error() + tables := []string{"checkins", "checkin_hits", "notifications", "core", "failures", "hits", "services", "users", "messages", "incidents", "incident_updates"} + for _, t := range tables { + if err := DbSession.DropTableIfExists(t); err != nil { + return err.Error() + } + } + return nil } // CreateDatabase will CREATE TABLES for each of the Statping elements diff --git a/core/export.go b/core/export.go index b4b4a3ad..6a9f1cd4 100644 --- a/core/export.go +++ b/core/export.go @@ -49,10 +49,10 @@ func ExportSettings() ([]byte, error) { Core: CoreApp.Core, Notifiers: CoreApp.Notifications, //Checkins: database.AllCheckins(), - Users: users, + Users: users, //Services: CoreApp.Services, //Groups: SelectGroups(true, true), - Messages: messages, + Messages: messages, } export, err := json.Marshal(data) return export, err diff --git a/core/failures.go b/core/failures.go index 35fbac83..d72faff2 100644 --- a/core/failures.go +++ b/core/failures.go @@ -19,7 +19,9 @@ import ( "github.com/hunterlong/statping/types" ) -type Failure struct{} +type Failure struct { + *types.Failure +} const ( limitedFailures = 32 diff --git a/core/groups.go b/core/groups.go index a7fdf91d..f739205a 100644 --- a/core/groups.go +++ b/core/groups.go @@ -7,46 +7,45 @@ import ( ) type Group struct { - database.Grouper + *types.Group } // SelectGroups returns all groups -func SelectGroups(includeAll bool, auth bool) []database.Grouper { - var validGroups []database.Grouper +func SelectGroups(includeAll bool, auth bool) []*Group { + var validGroups []*Group groups := database.AllGroups() for _, g := range groups { - if !g.Model().Public.Bool { + if !g.Public.Bool { if auth { - validGroups = append(validGroups, g) + validGroups = append(validGroups, &Group{g.Group}) } } else { - validGroups = append(validGroups, g) + validGroups = append(validGroups, &Group{g.Group}) } } sort.Sort(GroupOrder(validGroups)) if includeAll { - emptyGroup := &Group{} - validGroups = append(validGroups, emptyGroup) + validGroups = append(validGroups, &Group{}) } return validGroups } // SelectGroup returns a *core.Group -func SelectGroup(id int64) *types.Group { +func SelectGroup(id int64) *Group { for _, g := range SelectGroups(true, true) { - if g.Model().Id == id { - return g.Model() + if g.Id == id { + return g } } return nil } // GroupOrder will reorder the groups based on 'order_id' (Order) -type GroupOrder []database.Grouper +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].Model().Order < c[j].Model().Order } +func (c GroupOrder) Less(i, j int) bool { return c[i].Order < c[j].Order } diff --git a/core/notifier/notifiers_test.go b/core/notifier/notifiers_test.go index 73a96d89..6e2105a5 100644 --- a/core/notifier/notifiers_test.go +++ b/core/notifier/notifiers_test.go @@ -17,6 +17,7 @@ package notifier import ( "fmt" + "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" _ "github.com/jinzhu/gorm/dialects/sqlite" @@ -57,7 +58,7 @@ var core = &types.Core{ func injectDatabase() { sqlPath := dir + "/notifier.db" utils.DeleteFile(sqlPath) - db, _ = types.Openw("sqlite3", sqlPath) + db, _ = database.Openw("sqlite3", sqlPath) db.CreateTable(&Notification{}) } diff --git a/core/sample.go b/core/sample.go index 715924a1..0239186b 100644 --- a/core/sample.go +++ b/core/sample.go @@ -34,7 +34,10 @@ var ( func InsertSampleData() error { log.Infoln("Inserting Sample Data...") - insertSampleGroups() + if err := insertSampleGroups(); err != nil { + return err + } + createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC() s1 := &types.Service{ Name: "Google", @@ -106,15 +109,33 @@ func InsertSampleData() error { CreatedAt: createdOn, } - database.Create(s1) - database.Create(s2) - database.Create(s3) - database.Create(s4) - database.Create(s5) + if _, err := database.Create(s1); err != nil { + return types.ErrWrap(err, types.ErrorCreateService) + } + if _, err := database.Create(s2); err != nil { + return types.ErrWrap(err, types.ErrorCreateService) + } + if _, err := database.Create(s3); err != nil { + return types.ErrWrap(err, types.ErrorCreateService) + } + if _, err := database.Create(s4); err != nil { + return types.ErrWrap(err, types.ErrorCreateService) + } + if _, err := database.Create(s5); err != nil { + return types.ErrWrap(err, types.ErrorCreateService) + } - insertMessages() + if _, err := SelectAllServices(false); err != nil { + return types.ErrWrap(err, types.ErrorServiceSelection) + } - insertSampleIncidents() + 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") @@ -128,7 +149,7 @@ func insertSampleIncidents() error { ServiceId: 2, } if _, err := database.Create(incident1); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateIncidentUp) } incidentUpdate1 := &types.IncidentUpdate{ @@ -137,7 +158,7 @@ func insertSampleIncidents() error { Type: "Investigating", } if _, err := database.Create(incidentUpdate1); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateIncidentUp) } incidentUpdate2 := &types.IncidentUpdate{ @@ -146,7 +167,7 @@ func insertSampleIncidents() error { Type: "Update", } if _, err := database.Create(incidentUpdate2); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateIncidentUp) } incidentUpdate3 := &types.IncidentUpdate{ @@ -155,7 +176,7 @@ func insertSampleIncidents() error { Type: "Resolved", } if _, err := database.Create(incidentUpdate3); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateIncidentUp) } return nil @@ -168,7 +189,7 @@ func insertSampleGroups() error { Order: 2, } if _, err := database.Create(group1); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateGroup) } group2 := &types.Group{ @@ -177,7 +198,7 @@ func insertSampleGroups() error { Order: 1, } if _, err := database.Create(group2); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateGroup) } group3 := &types.Group{ @@ -186,7 +207,7 @@ func insertSampleGroups() error { Order: 3, } if _, err := database.Create(group3); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateGroup) } return nil } @@ -195,24 +216,24 @@ func insertSampleGroups() error { func insertSampleCheckins() error { s1 := SelectService(1) checkin1 := &types.Checkin{ - ServiceId: s1.Model().Id, + ServiceId: s1.Id, Interval: 300, GracePeriod: 300, } if _, err := database.Create(checkin1); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s2 := SelectService(1) checkin2 := &types.Checkin{ - ServiceId: s2.Model().Id, + ServiceId: s2.Id, Interval: 900, GracePeriod: 300, } if _, err := database.Create(checkin2); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateCheckinHit) } checkTime := time.Now().UTC().Add(-24 * time.Hour) @@ -224,7 +245,7 @@ func insertSampleCheckins() error { } if _, err := database.Create(checkHit); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateCheckinHit) } checkTime = checkTime.Add(10 * time.Minute) @@ -240,7 +261,7 @@ func InsertSampleHits() error { sg.Add(1) service := SelectService(i) seed := time.Now().UnixNano() - log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Model().Name)) + 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() { @@ -249,7 +270,7 @@ func InsertSampleHits() error { latency := p.Noise1D(hi / 500) createdAt = createdAt.Add(60 * time.Second) hit := &types.Hit{ - Service: service.Model().Id, + Service: service.Id, CreatedAt: createdAt, Latency: latency, } @@ -258,11 +279,11 @@ func InsertSampleHits() error { }() } sg.Wait() - err := tx.Commit().Error() - if err != nil { + if err := tx.Commit().Error(); err != nil { log.Errorln(err) + return types.ErrWrap(err, types.ErrorCreateSampleHits) } - return err + return nil } // insertSampleCore will create a new Core for the seed @@ -276,11 +297,14 @@ func insertSampleCore() error { Version: "test", CreatedAt: time.Now().UTC(), UseCdn: types.NewNullBool(false), + Footer: types.NewNullString(""), } - _, err := database.Create(core) + if _, err := database.Create(core); err != nil { + return types.ErrWrap(err, types.ErrorCreateCore) + } - return err + return nil } // insertSampleUsers will create 2 admin users for a seed database @@ -293,7 +317,7 @@ func insertSampleUsers() error { } if _, err := database.Create(u2); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateUser) } u3 := &types.User{ @@ -304,7 +328,7 @@ func insertSampleUsers() error { } if _, err := database.Create(u3); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateUser) } return nil @@ -320,7 +344,7 @@ func insertMessages() error { } if _, err := database.Create(m1); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateMessage) } m2 := &types.Message{ @@ -332,7 +356,7 @@ func insertMessages() error { } if _, err := database.Create(m2); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateMessage) } return nil } @@ -368,7 +392,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s6); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s7 := &types.Service{ @@ -384,7 +408,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s7); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s8 := &types.Service{ @@ -399,7 +423,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s8); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s9 := &types.Service{ @@ -415,7 +439,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s9); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s10 := &types.Service{ @@ -431,7 +455,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s10); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s11 := &types.Service{ @@ -447,7 +471,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s11); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s12 := &types.Service{ @@ -463,7 +487,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s12); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s13 := &types.Service{ @@ -479,7 +503,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s13); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s14 := &types.Service{ @@ -495,7 +519,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s14); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } s15 := &types.Service{ @@ -511,7 +535,7 @@ func InsertLargeSampleData() error { } if _, err := database.Create(s15); err != nil { - return err + return types.ErrWrap(err, types.ErrorCreateService) } var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour) @@ -527,14 +551,14 @@ func InsertLargeSampleData() error { 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.Model().Name)) + 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.Model().Id, + Service: service.Id, Issue: "testing right here", CreatedAt: createdAt, } @@ -545,32 +569,36 @@ func insertFailureRecords(since time.Time, amount int) { } // insertHitRecords will create successful Hit records for 15 services -func insertHitRecords(since time.Time, amount int) { +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.Model().Name)) + 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.Model().Id, + Service: service.Id, CreatedAt: createdAt.UTC(), Latency: latency, } - database.Create(hit) + 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 - utils.CreateDirectory(utils.Directory + "/tmp") + if err := utils.CreateDirectory(utils.Directory + "/tmp"); err != nil { + log.Error(err) + } var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename SampleHits = 480 @@ -578,19 +606,19 @@ func TmpRecords(dbFile string) error { CoreApp = NewCore() CoreApp.Name = "Tester" CoreApp.Setup = true - configs := &types.DbConfig{ + configs := &DbConfig{&types.DbConfig{ DbConn: "sqlite", Project: "Tester", Location: utils.Directory, SqlFile: sqlFile, - } + }} log.Infoln("saving config.yml in: " + utils.Directory) - if configs, err = CoreApp.SaveConfig(configs); err != nil { - return err + if err := configs.Save(); err != nil { + log.Error(err) } log.Infoln("loading config.yml from: " + utils.Directory) if configs, err = LoadConfigFile(utils.Directory); err != nil { - return err + log.Error(err) } log.Infoln("connecting to database") @@ -598,37 +626,37 @@ func TmpRecords(dbFile string) error { if exists { log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile) if err := utils.DeleteFile(sqlFile); err != nil { - log.Infoln(sqlFile + " was not found") + log.Error(err) } if err := utils.CopyFile(tmpSqlFile, sqlFile); err != nil { - return err + log.Error(err) } log.Infoln("loading config.yml from: " + utils.Directory) if err := CoreApp.Connect(false, utils.Directory); err != nil { - return err + log.Error(err) } log.Infoln("selecting the Core variable") if _, err := SelectCore(); err != nil { - return err + log.Error(err) } log.Infoln("inserting notifiers into database") if err := InsertNotifierDB(); err != nil { - return err + log.Error(err) } log.Infoln("inserting integrations into database") if err := InsertIntegratorDB(); err != nil { - return err + log.Error(err) } log.Infoln("loading all services") if _, err := SelectAllServices(false); err != nil { return err } if err := AttachNotifiers(); err != nil { - return err + log.Error(err) } if err := AddIntegrations(); err != nil { - return err + log.Error(err) } CoreApp.Notifications = notifier.AllCommunications return nil diff --git a/core/services.go b/core/services.go index 04df8ac9..74b180d7 100644 --- a/core/services.go +++ b/core/services.go @@ -25,20 +25,19 @@ import ( ) type Service struct { - *types.Service + *database.ServiceObj } -type Servicer interface{} - -func Services() []database.Servicer { +func Services() []*Service { return CoreApp.services } // SelectService returns a *core.Service from in memory -func SelectService(id int64) database.Servicer { +func SelectService(id int64) *Service { for _, s := range Services() { - if s.Model().Id == id { - fmt.Println("service: ", s.Model()) + if s.Id == id { + s.UpdateStats() + fmt.Println("service: ", s.Name, s.Stats) return s } } @@ -47,7 +46,7 @@ func SelectService(id int64) database.Servicer { // CheckinProcess runs the checkin routine for each checkin attached to service func CheckinProcess(s database.Servicer) { - for _, c := range s.AllCheckins() { + for _, c := range s.Checkins() { c.Start() go CheckinRoutine(c) } @@ -55,31 +54,35 @@ func CheckinProcess(s database.Servicer) { // SelectAllServices returns a slice of *core.Service to be store on []*core.Services // should only be called once on startup. -func SelectAllServices(start bool) ([]*database.ServiceObj, error) { +func SelectAllServices(start bool) ([]*Service, error) { srvs := database.Services() - for _, s := range srvs { - fmt.Println("services: ", s.Id, s.Name) - } - for _, s := range srvs { if start { - service := s.Model() - service.Start() + s.Start() CheckinProcess(s) } - //fails := service.Service (limitedFailures) - //for _, f := range fails { - // service.Failures = append(service.Failures, f) - //} - for _, c := range s.AllCheckins() { - s.Checkins = append(s.Checkins, c) + + 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.Service.Stats = s.UpdateStats() - CoreApp.services = append(CoreApp.services, s) + s.UpdateStats() + CoreApp.services = append(CoreApp.services, &Service{s}) } reorderServices() - return srvs, nil + return CoreApp.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' @@ -87,24 +90,10 @@ func reorderServices() { sort.Sort(ServiceOrder(CoreApp.services)) } -// GraphData will return all hits or failures -func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*database.TimeValue { - dbQuery, err := q.Database().GroupQuery(q, by).ToTimeValue(dbType) - - if err != nil { - log.Error(err) - return nil - } - if q.FillEmpty { - return dbQuery.FillMissing(q.Start, q.End) - } - return dbQuery.ToValues() -} - // index returns a services index int for updating the []*core.Services slice -func index(s database.Servicer) int { +func index(s int64) int { for k, service := range CoreApp.services { - if s.Model().Id == service.Model().Id { + if s == service.Id { return k } } @@ -112,14 +101,13 @@ func index(s database.Servicer) int { } // updateService will update a service in the []*core.Services slice -func updateService(s database.Servicer) { - CoreApp.services[index(s)] = s +func updateService(s *Service) { + CoreApp.services[index(s.Id)] = s } // Delete will remove a service from the database, it will also end the service checking go routine -func Delete(srv database.Servicer) error { - i := index(srv) - s := srv.Model() +func (s *Service) Delete() error { + i := index(s.Id) err := database.Delete(s) if err != nil { log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err)) @@ -129,13 +117,12 @@ func Delete(srv database.Servicer) error { slice := CoreApp.services CoreApp.services = append(slice[:i], slice[i+1:]...) reorderServices() - notifier.OnDeletedService(s) + 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(srv database.Servicer, restart bool) error { - s := srv.Model() +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)) @@ -151,12 +138,12 @@ func Update(srv database.Servicer, restart bool) error { if restart { s.Close() s.Start() - s.SleepDuration = time.Duration(s.Interval) * time.Second - go ServiceCheckQueue(srv, true) + s.SleepDuration = s.Duration() + go ServiceCheckQueue(s, true) } reorderServices() - updateService(srv) - notifier.OnUpdatedService(s) + updateService(s) + notifier.OnUpdatedService(s.Service) return err } @@ -169,10 +156,11 @@ func Create(srv database.Servicer, check bool) (int64, error) { log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err)) return 0, err } + service := &Service{s} s.Start() - go ServiceCheckQueue(srv, check) - CoreApp.services = append(CoreApp.services, srv) + CoreApp.services = append(CoreApp.services, service) + go ServiceCheckQueue(service, check) reorderServices() - notifier.OnNewService(s) + notifier.OnNewService(s.Service) return s.Id, nil } diff --git a/core/services_checkin_test.go b/core/services_checkin_test.go index cea580c9..4b55aab8 100644 --- a/core/services_checkin_test.go +++ b/core/services_checkin_test.go @@ -16,9 +16,10 @@ package core import ( + "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" "time" ) @@ -29,13 +30,12 @@ var ( func TestCreateCheckin(t *testing.T) { service := SelectService(1) - testCheckin = ReturnCheckin(&types.Checkin{ + checkin := &types.Checkin{ ServiceId: service.Id, Interval: 10, GracePeriod: 5, - ApiKey: utils.RandomString(7), - }) - id, err := testCheckin.Create() + } + id, err := database.Create(checkin) assert.Nil(t, err) assert.NotZero(t, id) assert.NotEmpty(t, testCheckin.ApiKey) @@ -46,13 +46,13 @@ func TestCreateCheckin(t *testing.T) { func TestSelectCheckin(t *testing.T) { service := SelectService(1) - checkins := service.AllCheckins() + checkins := service.Checkins() assert.NotNil(t, checkins) assert.Equal(t, 1, len(checkins)) - testCheckin = checkins[0] - assert.Equal(t, int64(10), testCheckin.Interval) - assert.Equal(t, int64(5), testCheckin.GracePeriod) - assert.Equal(t, 7, len(testCheckin.ApiKey)) + c := checkins[0] + assert.Equal(t, int64(10), c.Interval) + assert.Equal(t, int64(5), c.GracePeriod) + assert.Equal(t, 7, len(c.ApiKey)) } func TestUpdateCheckin(t *testing.T) { @@ -63,7 +63,7 @@ func TestUpdateCheckin(t *testing.T) { assert.NotZero(t, id) assert.NotEmpty(t, testCheckin.ApiKey) service := SelectService(1) - checkin := service.AllCheckins()[0] + checkin := service.Checkins()[0] assert.Equal(t, int64(60), checkin.Interval) assert.Equal(t, int64(15), checkin.GracePeriod) t.Log(testCheckin.Expected()) @@ -72,15 +72,16 @@ func TestUpdateCheckin(t *testing.T) { func TestCreateCheckinHits(t *testing.T) { service := SelectService(1) - checkins := service.AllCheckins() + checkins := service.Checkins() assert.Equal(t, 1, len(checkins)) created := time.Now().UTC().Add(-60 * time.Second) - hit := ReturnCheckinHit(&types.CheckinHit{ + hit := &types.CheckinHit{ Checkin: testCheckin.Id, From: "192.168.1.1", CreatedAt: created, - }) - hit.Create() + } + _, err := database.Create(hit) + require.Nil(t, err) hits := testCheckin.AllHits() assert.Equal(t, 1, len(hits)) } @@ -88,13 +89,13 @@ func TestCreateCheckinHits(t *testing.T) { func TestSelectCheckinMethods(t *testing.T) { time.Sleep(5 * time.Second) service := SelectService(1) - checkins := service.AllCheckins() + checkins := service.Checkins() assert.NotNil(t, checkins) - lastHit := testCheckin.Last() assert.Equal(t, float64(60), testCheckin.Period().Seconds()) assert.Equal(t, float64(15), testCheckin.Grace().Seconds()) t.Log(testCheckin.Expected()) + + lastHit := checkins[0] assert.True(t, testCheckin.Expected().Seconds() < -5) assert.False(t, lastHit.CreatedAt.IsZero()) - assert.Equal(t, "A minute ago", lastHit.Ago()) } diff --git a/core/services_test.go b/core/services_test.go index eabd2988..11568ec0 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -16,9 +16,11 @@ package core import ( + "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" "time" ) @@ -28,7 +30,7 @@ var ( ) func TestSelectHTTPService(t *testing.T) { - services, err := CoreApp.SelectAllServices(false) + services, err := SelectAllServices(false) assert.Nil(t, err) assert.Equal(t, 15, len(services)) assert.Equal(t, "Google", services[0].Name) @@ -36,12 +38,11 @@ func TestSelectHTTPService(t *testing.T) { } func TestSelectAllServices(t *testing.T) { - services := CoreApp.Services + services := CoreApp.services for _, s := range services { - service := s.(*Service) - service.Check(false) - assert.False(t, service.IsRunning()) - t.Logf("ID: %v %v\n", service.Id, service.Name) + CheckService(s, false) + assert.False(t, s.IsRunning()) + t.Logf("ID: %v %v\n", s.Id, s.Name) } assert.Equal(t, 15, len(services)) } @@ -54,7 +55,7 @@ func TestServiceDowntime(t *testing.T) { } func TestSelectTCPService(t *testing.T) { - services := CoreApp.Services + services := CoreApp.services assert.Equal(t, 15, len(services)) service := SelectService(5) assert.NotNil(t, service) @@ -67,8 +68,10 @@ func TestUpdateService(t *testing.T) { assert.Equal(t, "Google", service.Name) service.Name = "Updated Google" service.Interval = 5 - err := service.Update(true) - assert.Nil(t, err) + + err := database.Update(service) + require.Nil(t, err) + // check if updating pointer array shutdown any other service service = SelectService(1) assert.Equal(t, "Updated Google", service.Name) @@ -76,19 +79,20 @@ func TestUpdateService(t *testing.T) { } func TestUpdateAllServices(t *testing.T) { - services, err := CoreApp.SelectAllServices(false) - assert.Nil(t, err) + services, err := SelectAllServices(false) + require.Nil(t, err) for k, srv := range services { srv.Name = "Changed " + srv.Name srv.Interval = k + 3 - err := srv.Update(true) - assert.Nil(t, err) + + err := database.Update(srv) + require.Nil(t, err) } } func TestServiceHTTPCheck(t *testing.T) { service := SelectService(1) - service.Check(true) + CheckService(service, true) assert.Equal(t, "Changed Updated Google", service.Name) assert.True(t, service.Online) } @@ -104,7 +108,7 @@ func TestCheckHTTPService(t *testing.T) { func TestServiceTCPCheck(t *testing.T) { service := SelectService(5) - service.Check(true) + CheckService(service, true) assert.Equal(t, "Changed Google DNS", service.Name) assert.True(t, service.Online) } @@ -130,23 +134,17 @@ func TestServiceOnline24Hours(t *testing.T) { func TestServiceAvgUptime(t *testing.T) { since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute) service := SelectService(1) - assert.NotEqual(t, "0.00", service.AvgUptime(since)) + assert.NotEqual(t, "0.00", service.AvgTime()) service2 := SelectService(5) - assert.Equal(t, "100", service2.AvgUptime(since)) + assert.Equal(t, "100", service2.AvgTime()) service3 := SelectService(13) assert.NotEqual(t, "0", service3.AvgUptime(since)) service4 := SelectService(15) assert.NotEqual(t, "0", service4.AvgUptime(since)) } -func TestServiceSum(t *testing.T) { - service := SelectService(5) - sum := service.Sum() - assert.NotZero(t, sum) -} - func TestCreateService(t *testing.T) { - s := ReturnService(&types.Service{ + s := &types.Service{ Name: "That'll do 🐢", Domain: "https://www.youtube.com/watch?v=rjQtzV9IZ0Q", ExpectedStatus: 200, @@ -155,12 +153,11 @@ func TestCreateService(t *testing.T) { Method: "GET", Timeout: 20, GroupId: 1, - }) - var err error - newServiceId, err = s.Create(false) - assert.Nil(t, err) - assert.NotZero(t, newServiceId) - newService := SelectService(newServiceId) + } + obj, err := database.Create(s) + require.Nil(t, err) + assert.NotZero(t, obj.Id) + newService := SelectService(obj.Id) assert.Equal(t, "That'll do 🐢", newService.Name) } @@ -170,7 +167,7 @@ func TestViewNewService(t *testing.T) { } func TestCreateFailingHTTPService(t *testing.T) { - s := ReturnService(&types.Service{ + s := &types.Service{ Name: "Bad URL", Domain: "http://localhost/iamnothere", ExpectedStatus: 200, @@ -179,12 +176,11 @@ func TestCreateFailingHTTPService(t *testing.T) { Method: "GET", Timeout: 5, GroupId: 1, - }) - var err error - newServiceId, err = s.Create(false) - assert.Nil(t, err) - assert.NotZero(t, newServiceId) - newService := SelectService(newServiceId) + } + obj, err := database.Create(s) + require.Nil(t, err) + assert.NotZero(t, obj.Id) + newService := SelectService(obj.Id) assert.Equal(t, "Bad URL", newService.Name) t.Log("new service ID: ", newServiceId) } @@ -192,13 +188,13 @@ func TestCreateFailingHTTPService(t *testing.T) { func TestServiceFailedCheck(t *testing.T) { service := SelectService(17) assert.Equal(t, "Bad URL", service.Name) - service.Check(false) + CheckService(service, false) assert.Equal(t, "Bad URL", service.Name) assert.False(t, service.Online) } func TestCreateFailingTCPService(t *testing.T) { - s := ReturnService(&types.Service{ + s := &types.Service{ Name: "Bad TCP", Domain: "localhost", Port: 5050, @@ -206,50 +202,51 @@ func TestCreateFailingTCPService(t *testing.T) { Type: "tcp", Timeout: 5, GroupId: 1, - }) + } var err error - newServiceId, err = s.Create(false) + obj, err := database.Create(s) assert.Nil(t, err) - assert.NotZero(t, newServiceId) - newService := SelectService(newServiceId) + assert.NotZero(t, obj.Id) + newService := SelectService(obj.Id) assert.Equal(t, "Bad TCP", newService.Name) t.Log("new failing tcp service ID: ", newServiceId) } func TestServiceFailedTCPCheck(t *testing.T) { service := SelectService(newServiceId) - service.Check(false) + CheckService(service, false) assert.Equal(t, "Bad TCP", service.Name) assert.False(t, service.Online) } func TestCreateServiceFailure(t *testing.T) { - fail := &types.Failure{ - Issue: "This is not an issue, but it would container HTTP response errors.", - Method: "http", - } service := SelectService(8) - id, err := service.CreateFailure(fail) + fail := &types.Failure{ + Issue: "This is not an issue, but it would container HTTP response errors.", + Method: "http", + Service: service.Id, + } + obj, err := database.Create(fail) assert.Nil(t, err) - assert.NotZero(t, id) + assert.NotZero(t, obj.Id) } func TestDeleteService(t *testing.T) { service := SelectService(newServiceId) - count, err := CoreApp.SelectAllServices(false) + count, err := SelectAllServices(false) assert.Nil(t, err) assert.Equal(t, 18, len(count)) err = service.Delete() assert.Nil(t, err) - services := CoreApp.Services + services := CoreApp.services assert.Equal(t, 17, len(services)) } func TestServiceCloseRoutine(t *testing.T) { - s := ReturnService(new(types.Service)) + s := new(Service) s.Name = "example" s.Domain = "https://google.com" s.Type = "http" @@ -260,7 +257,7 @@ func TestServiceCloseRoutine(t *testing.T) { assert.True(t, s.IsRunning()) t.Log(s.Checkpoint) t.Log(s.SleepDuration) - go s.CheckQueue(false) + go ServiceCheckQueue(s, false) t.Log(s.Checkpoint) t.Log(s.SleepDuration) time.Sleep(5 * time.Second) @@ -274,7 +271,7 @@ func TestServiceCloseRoutine(t *testing.T) { } func TestServiceCheckQueue(t *testing.T) { - s := ReturnService(new(types.Service)) + s := new(Service) s.Name = "example" s.Domain = "https://google.com" s.Type = "http" @@ -283,7 +280,7 @@ func TestServiceCheckQueue(t *testing.T) { s.Interval = 1 s.Start() assert.True(t, s.IsRunning()) - go s.CheckQueue(false) + go ServiceCheckQueue(s, false) go func() { time.Sleep(5 * time.Second) @@ -300,14 +297,14 @@ func TestServiceCheckQueue(t *testing.T) { } func TestDNScheckService(t *testing.T) { - s := ReturnService(new(types.Service)) + s := new(Service) s.Name = "example" s.Domain = "http://localhost:9000" s.Type = "http" s.Method = "GET" s.ExpectedStatus = 200 s.Interval = 1 - amount, err := s.dnsCheck() + amount, err := dnsCheck(s) assert.Nil(t, err) assert.NotZero(t, amount) } @@ -317,25 +314,13 @@ func TestSelectServiceLink(t *testing.T) { assert.Equal(t, "google", service.Permalink.String) } -func TestDbtimestamp(t *testing.T) { - CoreApp.Config.DbConn = "mysql" - query := Dbtimestamp("minute", "latency") - assert.Equal(t, "CONCAT(date_format(created_at, '%Y-%m-%d %H:00:00')) AS timeframe, AVG(latency) AS value", query) - CoreApp.Config.DbConn = "postgres" - query = Dbtimestamp("minute", "latency") - assert.Equal(t, "date_trunc('minute', created_at) AS timeframe, AVG(latency) AS value", query) - CoreApp.Config.DbConn = "sqlite" - query = Dbtimestamp("minute", "latency") - assert.Equal(t, "datetime((strftime('%s', created_at) / 60) * 60, 'unixepoch') AS timeframe, AVG(latency) as value", query) -} - func TestGroup_Create(t *testing.T) { - group := &Group{&types.Group{ + group := &types.Group{ Name: "Testing", - }} - newGroupId, err := group.Create() + } + obj, err := database.Create(group) assert.Nil(t, err) - assert.NotZero(t, newGroupId) + assert.NotZero(t, obj.Id) } func TestGroup_Services(t *testing.T) { diff --git a/core/users.go b/core/users.go index 88ecf1f0..cb58b29d 100644 --- a/core/users.go +++ b/core/users.go @@ -96,16 +96,21 @@ func SelectAllUsers() []*types.User { // 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 := SelectUsername(username) +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() - user.Update() - return user, true + database.Update(user) + return user.User, true } return nil, false } diff --git a/core/users_test.go b/core/users_test.go index bb6aac7e..f3d9ab0c 100644 --- a/core/users_test.go +++ b/core/users_test.go @@ -16,26 +16,26 @@ package core import ( + "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" "github.com/stretchr/testify/assert" "testing" ) func TestCreateUser(t *testing.T) { - user := ReturnUser(&types.User{ + user := &types.User{ Username: "hunter", Password: "password123", Email: "test@email.com", Admin: types.NewNullBool(true), - }) - userId, err := user.Create() + } + obj, err := database.Create(user) assert.Nil(t, err) - assert.NotZero(t, userId) + assert.NotZero(t, obj.Id) } func TestSelectAllUsers(t *testing.T) { - users, err := SelectAllUsers() - assert.Nil(t, err) + users := SelectAllUsers() assert.Equal(t, 3, len(users)) } @@ -66,20 +66,19 @@ func TestUpdateUser(t *testing.T) { } func TestCreateUser2(t *testing.T) { - user := ReturnUser(&types.User{ + user := &types.User{ Username: "hunterlong", Password: "password123", Email: "User@email.com", Admin: types.NewNullBool(true), - }) - userId, err := user.Create() + } + obj, err := database.Create(user) assert.Nil(t, err) - assert.NotZero(t, userId) + assert.NotZero(t, obj.Id) } func TestSelectAllUsersAgain(t *testing.T) { - users, err := SelectAllUsers() - assert.Nil(t, err) + users := SelectAllUsers() assert.Equal(t, 4, len(users)) } diff --git a/database/checkins.go b/database/checkins.go index 6de9e787..f7022f99 100644 --- a/database/checkins.go +++ b/database/checkins.go @@ -3,6 +3,7 @@ package database import ( "fmt" "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/utils" "time" ) @@ -20,6 +21,21 @@ type Checkiner interface { Object() *CheckinObj } +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) diff --git a/database/crud.go b/database/crud.go index 3dbd80fd..a9048ad6 100644 --- a/database/crud.go +++ b/database/crud.go @@ -1,11 +1,14 @@ package database import ( - "fmt" "github.com/hunterlong/statping/types" "reflect" ) +type CrudObject interface { + Create() +} + type Object struct { Id int64 model interface{} @@ -25,41 +28,35 @@ func wrapObject(id int64, model interface{}, db Database) *Object { } func modelId(model interface{}) int64 { - fmt.Printf("%T\n", model) - iface := reflect.ValueOf(model) - field := iface.Elem().FieldByName("Id") - return field.Int() -} - -func toModel(model interface{}) Database { switch model.(type) { case *types.Core: - return database.Model(&types.Core{}).Table("core") + return 0 default: - return database.Model(&model) + iface := reflect.ValueOf(model) + field := iface.Elem().FieldByName("Id") + return field.Int() } } func Create(data interface{}) (*Object, error) { - model := toModel(data) - query := model.Create(data) - if query.Error() != nil { - return nil, query.Error() + model := database.Model(&data) + if err := model.Create(data).Error(); err != nil { + return nil, err } obj := &Object{ Id: modelId(data), model: data, db: model, } - return obj, query.Error() + return obj, nil } func Update(data interface{}) error { - model := toModel(data) + model := database.Model(&data) return model.Update(&data).Error() } func Delete(data interface{}) error { - model := toModel(data) + model := database.Model(&data) return model.Delete(data).Error() } diff --git a/database/database.go b/database/database.go index 7c98d8b9..27d43750 100644 --- a/database/database.go +++ b/database/database.go @@ -113,8 +113,6 @@ type Database interface { Requests(*http.Request, isObject) Database - GroupQuery(query *GroupQuery, by By) GroupByer - Objects } @@ -193,6 +191,9 @@ type Db struct { // Openw is a drop-in replacement for Open() func Openw(dialect string, args ...interface{}) (db Database, err error) { + gorm.NowFunc = func() time.Time { + return time.Now().UTC() + } gormdb, err := gorm.Open(dialect, args...) if err != nil { return nil, err diff --git a/database/failures.go b/database/failures.go index 31565cbb..8a151aa0 100644 --- a/database/failures.go +++ b/database/failures.go @@ -34,10 +34,10 @@ func (f *FailureObj) DeleteAll() error { return query.Error() } -func (f *FailureObj) Last(amount int) *types.Failure { - var fail types.Failure - f.o.db.Limit(amount).Last(&fail) - return &fail +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 { diff --git a/database/grouping.go b/database/grouping.go index cf6f551b..46ebf643 100644 --- a/database/grouping.go +++ b/database/grouping.go @@ -15,7 +15,7 @@ type GroupBy struct { } type GroupByer interface { - ToTimeValue(interface{}) (*TimeVar, error) + ToTimeValue() (*TimeVar, error) } type By string @@ -25,7 +25,6 @@ func (b By) String() string { } type GroupQuery struct { - db Database Start time.Time End time.Time Group string @@ -33,6 +32,8 @@ type GroupQuery struct { Limit int Offset int FillEmpty bool + + db Database } func (b GroupQuery) Find(data interface{}) error { @@ -50,25 +51,16 @@ var ( } ) -func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer { - dbQuery := db.MultipleSelects( - db.SelectByTime(q.Group), - by.String(), - ).Group("timeframe") - - return &GroupBy{dbQuery, q} -} - type TimeVar struct { - g *GroupBy + g *GroupQuery data []*TimeValue } -func (t *TimeVar) ToValues() []*TimeValue { - return t.data +func (t *TimeVar) ToValues() ([]*TimeValue, error) { + return t.data, nil } -func (g *GroupBy) toFloatRows() []*TimeValue { +func (g *GroupQuery) toFloatRows() []*TimeValue { rows, err := g.db.Rows() if err != nil { return nil @@ -77,7 +69,12 @@ func (g *GroupBy) toFloatRows() []*TimeValue { for rows.Next() { var timeframe time.Time amount := float64(0) - rows.Scan(&timeframe, &amount) + if err := rows.Scan(&timeframe, &amount); err != nil { + log.Errorln(err) + } + + fmt.Println("float rows: ", timeframe, amount) + newTs := types.FixedTime(timeframe, g.duration()) data = append(data, &TimeValue{ Timeframe: newTs, @@ -87,17 +84,41 @@ func (g *GroupBy) toFloatRows() []*TimeValue { return data } -func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) { +// GraphData will return all hits or failures +func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) { + + dbQuery := g.db.MultipleSelects( + g.db.SelectByTime(g.Group), + by.String(), + ).Group("timeframe") + + g.db = dbQuery + + caller, err := g.ToTimeValue() + if err != nil { + return nil, err + } + + if g.FillEmpty { + return caller.FillMissing(g.Start, g.End) + } + return caller.ToValues() +} + +func (g *GroupQuery) ToTimeValue() (*TimeVar, error) { rows, err := g.db.Rows() if err != nil { return nil, err } var data []*TimeValue for rows.Next() { - var timeframe time.Time + var timeframe string amount := float64(0) - rows.Scan(&timeframe, &amount) - newTs := types.FixedTime(timeframe, g.duration()) + if err := rows.Scan(&timeframe, &amount); err != nil { + log.Error(err, timeframe) + } + trueTime, _ := g.db.ParseTime(timeframe) + newTs := types.FixedTime(trueTime, g.duration()) data = append(data, &TimeValue{ Timeframe: newTs, Amount: amount, @@ -106,7 +127,7 @@ func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) { return &TimeVar{g, data}, nil } -func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue { +func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) { timeMap := make(map[string]float64) var validSet []*TimeValue dur := t.g.duration() @@ -132,11 +153,11 @@ func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue { currentStr = types.FixedTime(current, t.g.duration()) } - return validSet + return validSet, nil } -func (g *GroupBy) duration() time.Duration { - switch g.query.Group { +func (g *GroupQuery) duration() time.Duration { + switch g.Group { case "second": return types.Second case "minute": @@ -175,6 +196,8 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery { limit = 10000 } + db := o.object().db + query := &GroupQuery{ Start: time.Unix(startField, 0).UTC(), End: time.Unix(endField, 0).UTC(), @@ -183,10 +206,9 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery { Limit: int(limit), Offset: int(offset), FillEmpty: fill, + db: db, } - db := o.object().db - if query.Limit != 0 { db = db.Limit(query.Limit) } diff --git a/database/hits.go b/database/hits.go index 408ca5f7..41c4577c 100644 --- a/database/hits.go +++ b/database/hits.go @@ -15,10 +15,25 @@ func (h *HitObj) All() []*types.Hit { return fails } -func (h *HitObj) Last(amount int) *types.Hit { - var hits types.Hit +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 + return hits } func (h *HitObj) Since(t time.Time) []*types.Hit { diff --git a/database/routines.go b/database/routines.go new file mode 100644 index 00000000..2ab6ac4f --- /dev/null +++ b/database/routines.go @@ -0,0 +1,60 @@ +package database + +import ( + "fmt" + "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/utils" + "os" + "time" +) + +var ( + log = utils.Log + removeRowsAfter = types.Month * 6 + maintenceDuration = types.Hour +) + +func StartMaintenceRoutine() { + dur := os.Getenv("REMOVE_AFTER") + var removeDur time.Duration + + if dur != "" { + parsedDur, err := time.ParseDuration(dur) + if err != nil { + log.Errorf("could not parse duration: %s, using default: %s", dur, removeRowsAfter.String()) + removeDur = removeRowsAfter + } else { + removeDur = parsedDur + } + } else { + removeDur = removeRowsAfter + } + + log.Infof("Service Failure and Hit records will be automatically removed after %s", removeDur.String()) + go databaseMaintence(removeDur) +} + +// databaseMaintence will automatically delete old records from 'failures' and 'hits' +// this function is currently set to delete records 7+ days old every 60 minutes +func databaseMaintence(dur time.Duration) { + deleteAfter := time.Now().UTC().Add(dur) + + for range time.Tick(maintenceDuration) { + log.Infof("Deleting failures older than %s", dur.String()) + DeleteAllSince("failures", deleteAfter) + + log.Infof("Deleting hits older than %s", dur.String()) + DeleteAllSince("hits", deleteAfter) + + maintenceDuration = types.Hour + } +} + +// DeleteAllSince will delete a specific table's records based on a time. +func DeleteAllSince(table string, date time.Time) { + sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, database.FormatTime(date)) + db := database.Exec(sql) + if db.Error() != nil { + log.Warnln(db.Error()) + } +} diff --git a/database/service.go b/database/service.go index 4c65d6ec..54cfb24a 100644 --- a/database/service.go +++ b/database/service.go @@ -16,18 +16,22 @@ type ServiceObj struct { } type Servicer interface { - Hits() *HitObj Failures() *FailureObj - AllCheckins() []*CheckinObj - Model() *types.Service - Interval() time.Duration + Checkins() []*CheckinObj DowntimeText() string + UpdateStats() + Model() *ServiceObj Hittable } type Hittable interface { - CreateHit(*types.Hit) (int64, error) + Hits() *HitObj + CreateHit(hit *types.Hit) *HitObj +} + +func (s *ServiceObj) Model() *ServiceObj { + return s } func Service(id int64) (*ServiceObj, error) { @@ -52,7 +56,7 @@ func Services() []*ServiceObj { return wrapServices(services, db) } -func (s *ServiceObj) AllCheckins() []*CheckinObj { +func (s *ServiceObj) Checkins() []*CheckinObj { var checkins []*types.Checkin query := database.Checkins().Where("service = ?", s.Id) query.Find(&checkins) @@ -61,7 +65,10 @@ func (s *ServiceObj) AllCheckins() []*CheckinObj { func (s *ServiceObj) DowntimeText() string { last := s.Failures().Last(1) - return parseError(last) + if len(last) == 0 { + return "" + } + return parseError(last[0]) } // ParseError returns a human readable error for a Failure @@ -116,16 +123,7 @@ func parseError(f *types.Failure) string { return f.Issue } -func (s *ServiceObj) Interval() time.Duration { - return time.Duration(s.Service.Interval) * time.Second -} - -func (s *ServiceObj) Model() *types.Service { - return s.Service -} - func (s *ServiceObj) Hits() *HitObj { - fmt.Println("hits") query := database.Hits().Where("service = ?", s.Id) return &HitObj{wrapObject(s.Id, nil, query)} } @@ -149,34 +147,27 @@ func (s *ServiceObj) object() *Object { return s.o } -func (s *ServiceObj) UpdateStats() *types.Stats { +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().Add(-time.Hour * 24))) - return s.Stats + 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 { - var sum []float64 - database.Hits(). - Select("AVG(latency) as amount"). - Where("service = ?", s.Id).Pluck("amount", &sum).Debug() + sum := s.Hits().Sum() + return sum +} - sumTotal := float64(0) - for _, v := range sum { - sumTotal += v - } - - total := s.Hits().Count() - - if total == 0 { - return 0 - } - avg := sumTotal / float64(total) * 100 - f, _ := strconv.ParseFloat(fmt.Sprintf("%0.0f", avg*10), 32) - return f +// 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 @@ -211,12 +202,12 @@ func (s *ServiceObj) OnlineSince(ago time.Time) float32 { func (s *ServiceObj) Downtime() time.Duration { hits := s.Hits().Last(1) fail := s.Failures().Last(1) - if fail == nil { + if len(fail) == 0 { return time.Duration(0) } - if hits == nil { - return time.Now().UTC().Sub(fail.CreatedAt.UTC()) + if len(fail) == 0 { + return time.Now().UTC().Sub(fail[0].CreatedAt.UTC()) } - since := fail.CreatedAt.UTC().Sub(hits.CreatedAt.UTC()) + since := fail[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) return since } diff --git a/go.mod b/go.mod index f418da33..625ffa0e 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,6 @@ require ( github.com/go-mail/mail v2.3.1+incompatible github.com/go-yaml/yaml v2.1.0+incompatible github.com/gorilla/mux v1.7.3 - github.com/gorilla/websocket v1.4.1 // indirect - github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/jinzhu/gorm v1.9.11 github.com/joho/godotenv v1.3.0 github.com/lib/pq v1.2.0 // indirect diff --git a/go.sum b/go.sum index a5e63d52..86ecd9f5 100644 --- a/go.sum +++ b/go.sum @@ -86,11 +86,7 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= diff --git a/handlers/api_test.go b/handlers/api_test.go index dfd26b28..dd29a7b5 100644 --- a/handlers/api_test.go +++ b/handlers/api_test.go @@ -33,6 +33,7 @@ func init() { func TestResetDatabase(t *testing.T) { err := core.TmpRecords("handlers.db") + t.Log(err) require.Nil(t, err) require.NotNil(t, core.CoreApp) require.NotNil(t, core.CoreApp.Config) diff --git a/handlers/checkin.go b/handlers/checkin.go index 3ef6fe51..179ff5f2 100644 --- a/handlers/checkin.go +++ b/handlers/checkin.go @@ -95,10 +95,13 @@ func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) return } - err := checkin.Delete() - if err != nil { + + if err := database.Delete(checkin); err != nil { sendErrorJson(err, w, r) return } + + checkin.Delete() + sendJsonAction(checkin, "delete", w, r) } diff --git a/handlers/dashboard.go b/handlers/dashboard.go index b550e918..0147ed22 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -21,6 +21,7 @@ import ( "github.com/dgrijalva/jwt-go" "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/source" + "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "net/http" "os" @@ -183,7 +184,7 @@ func removeJwtToken(w http.ResponseWriter) { }) } -func setJwtToken(user *core.User, w http.ResponseWriter) (JwtClaim, string) { +func setJwtToken(user *types.User, w http.ResponseWriter) (JwtClaim, string) { expirationTime := time.Now().Add(72 * time.Hour) jwtClaim := JwtClaim{ Username: user.Username, diff --git a/handlers/groups.go b/handlers/groups.go index 72219a6b..53be8827 100644 --- a/handlers/groups.go +++ b/handlers/groups.go @@ -21,6 +21,7 @@ import ( "github.com/gorilla/mux" "github.com/hunterlong/statping/core" "github.com/hunterlong/statping/database" + "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "net/http" ) @@ -32,11 +33,19 @@ func apiAllGroupHandler(r *http.Request) interface{} { return groups } +func flattenGroups(groups []*core.Group) []*types.Group { + var groupers []*types.Group + for _, g := range groups { + groupers = append(groupers, g.Group) + } + return groupers +} + // 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 == nil { + if group.Id == 0 { sendErrorJson(errors.New("group not found"), w, r) return } @@ -47,7 +56,7 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request) { func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) group := core.SelectGroup(utils.ToInt(vars["id"])) - if group == nil { + if group.Id == 0 { sendErrorJson(errors.New("group not found"), w, r) return } @@ -82,7 +91,7 @@ func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) { func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) group := core.SelectGroup(utils.ToInt(vars["id"])) - if group == nil { + if group.Id == 0 { sendErrorJson(errors.New("group not found"), w, r) return } diff --git a/handlers/prometheus.go b/handlers/prometheus.go index fd40c8af..63e7f2bf 100644 --- a/handlers/prometheus.go +++ b/handlers/prometheus.go @@ -45,7 +45,7 @@ func prometheusHandler(w http.ResponseWriter, r *http.Request) { if !v.Online { online = 0 } - met := fmt.Sprintf("statping_service_failures{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, len(v.Failures)) + 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) diff --git a/handlers/routes.go b/handlers/routes.go index 79599438..be7297a8 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -22,7 +22,6 @@ import ( "github.com/hunterlong/statping/source" "github.com/hunterlong/statping/utils" "net/http" - "os" ) var ( @@ -41,15 +40,18 @@ func Router() *mux.Router { CacheStorage = NewStorage() r := mux.NewRouter().StrictSlash(true) - if os.Getenv("AUTH_USERNAME") != "" && os.Getenv("AUTH_PASSWORD") != "" { - authUser = os.Getenv("AUTH_USERNAME") - authPass = os.Getenv("AUTH_PASSWORD") + authUser := utils.Getenv("AUTH_USERNAME", "").(string) + authPass := utils.Getenv("AUTH_PASSWORD", "").(string) + + if authUser != "" && authPass != "" { r.Use(basicAuthHandler) } - if os.Getenv("BASE_PATH") != "" { - basePath = "/" + os.Getenv("BASE_PATH") + "/" - r = r.PathPrefix("/" + os.Getenv("BASE_PATH")).Subrouter() + bPath := utils.Getenv("BASE_PATH", "").(string) + + if bPath != "" { + basePath = "/" + bPath + "/" + r = r.PathPrefix("/" + bPath).Subrouter() r.Handle("", http.HandlerFunc(indexHandler)) } else { r.Handle("/", http.HandlerFunc(indexHandler)) diff --git a/handlers/scope.go b/handlers/scope.go index feaf9723..c23617c4 100644 --- a/handlers/scope.go +++ b/handlers/scope.go @@ -30,35 +30,37 @@ type scope struct { func (s scope) MarshalJSON() ([]byte, error) { svc := reflect.ValueOf(s.data) if svc.Kind() == reflect.Slice { - alldata := make([]map[string]interface{}, 0) + alldata := make([]map[string]interface{}, svc.Len()) for i := 0; i < svc.Len(); i++ { objIndex := svc.Index(i) - if objIndex.Kind() == reflect.Ptr { - objIndex = objIndex.Elem() - } - alldata = append(alldata, SafeJson(objIndex.Interface(), s.scope)) + alldata[i] = SafeJson(objIndex, s.scope) } return json.Marshal(alldata) } - return json.Marshal(SafeJson(svc.Interface(), s.scope)) + return json.Marshal(SafeJson(svc, s.scope)) } -func SafeJson(input interface{}, scope string) map[string]interface{} { +func SafeJson(val reflect.Value, scope string) map[string]interface{} { thisData := make(map[string]interface{}) - t := reflect.TypeOf(input) - elem := reflect.ValueOf(input) - d, _ := json.Marshal(input) + if val.Kind() == reflect.Interface && !val.IsNil() { + elm := val.Elem() + if elm.Kind() == reflect.Ptr && !elm.IsNil() && elm.Elem().Kind() == reflect.Ptr { + val = elm + } + } + if val.Kind() == reflect.Ptr { + val = val.Elem() + } - var raw map[string]*json.RawMessage - json.Unmarshal(d, &raw) + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + typeField := val.Type().Field(i) + tagVal := typeField.Tag - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - - tag := field.Tag.Get("scope") + tag := tagVal.Get("scope") tags := strings.Split(tag, ",") - jTags := field.Tag.Get("json") + jTags := tagVal.Get("json") jsonTag := strings.Split(jTags, ",") if len(jsonTag) == 0 { @@ -69,21 +71,19 @@ func SafeJson(input interface{}, scope string) map[string]interface{} { continue } - trueValue := elem.Field(i).Interface() - if len(jsonTag) == 2 { - if jsonTag[1] == "omitempty" && trueValue == "" { + if jsonTag[1] == "omitempty" && valueField.Interface() == "" { continue } } if tag == "" { - thisData[jsonTag[0]] = trueValue + thisData[jsonTag[0]] = valueField.Interface() continue } if forTag(tags, scope) { - thisData[jsonTag[0]] = trueValue + thisData[jsonTag[0]] = valueField.Interface() } } return thisData diff --git a/handlers/services.go b/handlers/services.go index 68b65938..77af6f2a 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -46,11 +46,11 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) { func apiServiceHandler(r *http.Request) interface{} { vars := mux.Vars(r) - servicer := core.SelectService(utils.ToInt(vars["id"])).Model() + servicer := core.SelectService(utils.ToInt(vars["id"])) if servicer == nil { return errors.New("service not found") } - return *servicer + return servicer.Service } func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) { @@ -130,8 +130,12 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { groupQuery := database.ParseQueries(r, service.Hits()) - obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency")) - returnJson(obj, w, r) + objs, err := groupQuery.GraphData(database.ByAverage("latency")) + if err != nil { + sendErrorJson(err, w, r) + return + } + returnJson(objs, w, r) } func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { @@ -141,10 +145,16 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := database.ParseQueries(r, service.Hits()) - obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount) - returnJson(obj, w, r) + groupQuery := database.ParseQueries(r, service.Failures()) + + objs, err := groupQuery.GraphData(database.ByCount) + if err != nil { + sendErrorJson(err, w, r) + return + } + + returnJson(objs, w, r) } func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { @@ -154,10 +164,16 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(errors.New("service data not found"), w, r) return } + groupQuery := database.ParseQueries(r, service.Hits()) - obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time")) - returnJson(obj, w, r) + objs, err := groupQuery.GraphData(database.ByAverage("ping_time")) + if err != nil { + sendErrorJson(err, w, r) + return + } + + returnJson(objs, w, r) } type dataXy struct { @@ -190,10 +206,11 @@ func apiAllServicesHandler(r *http.Request) interface{} { return joinServices(services) } -func joinServices(srvs []database.Servicer) []*types.Service { +func joinServices(srvs []*core.Service) []*types.Service { var services []*types.Service for _, v := range srvs { - services = append(services, v.Model()) + v.UpdateStats() + services = append(services, v.Service) } return services } diff --git a/handlers/setup.go b/handlers/setup.go index a2b05800..0244d861 100644 --- a/handlers/setup.go +++ b/handlers/setup.go @@ -52,7 +52,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { sample, _ := strconv.ParseBool(r.PostForm.Get("sample_data")) dir := utils.Directory - config := &types.DbConfig{ + config := &core.DbConfig{DbConfig: &types.DbConfig{ DbConn: dbConn, DbHost: dbHost, DbUser: dbUser, @@ -67,11 +67,11 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { Email: email, Error: nil, Location: utils.Directory, - } + }} log.WithFields(utils.ToFields(core.CoreApp, config)).Debugln("new configs posted") - if _, err := core.CoreApp.SaveConfig(config); err != nil { + if err := config.Save(); err != nil { log.Errorln(err) sendErrorJson(err, w, r) return @@ -100,7 +100,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { return } - core.CoreApp, err = core.CoreApp.InsertCore(config) + core.CoreApp, err = config.InsertCore() if err != nil { log.Errorln(err) sendErrorJson(err, w, r) @@ -130,7 +130,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) { Config *types.DbConfig `json:"config"` }{ "okokok", - config, + config.DbConfig, } returnJson(out, w, r) } diff --git a/notifiers/notifiers_test.go b/notifiers/notifiers_test.go index 702a4b25..b8569076 100644 --- a/notifiers/notifiers_test.go +++ b/notifiers/notifiers_test.go @@ -17,6 +17,7 @@ package notifiers import ( "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" @@ -80,5 +81,5 @@ func injectDatabase() { panic(err) } db.CreateTable(¬ifier.Notification{}) - notifier.SetDB(&types.Db{db}) + notifier.SetDB(&database.Db{db, "sqlite3"}) } diff --git a/plugin/doc.go b/plugin/doc.go deleted file mode 100644 index 8d0315f5..00000000 --- a/plugin/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package plugin contains the interfaces to build your own Golang Plugin that will receive triggers on Statping events. -package plugin diff --git a/plugin/plugin.go b/plugin/plugin.go deleted file mode 100644 index 8e5e2855..00000000 --- a/plugin/plugin.go +++ /dev/null @@ -1,116 +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 plugin - -import ( - "fmt" - "github.com/hunterlong/statping/core" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "io/ioutil" - "os" - "plugin" - "strings" -) - -// -// STATPING PLUGIN INTERFACE -// -// v0.1 -// -// https://statping.com -// -// -// An expandable plugin framework that will still -// work even if there's an update or addition. -// - -var ( - AllPlugins []*types.PluginObject - dir string - log = utils.Log.WithField("type", "plugin") -) - -func init() { - utils.InitLogs() - dir = utils.Directory -} - -func LoadPlugin(file string) error { - log.Infoln(fmt.Sprintf("opening file %v", file)) - f, err := os.Open(file) - if err != nil { - return err - } - - fSplit := strings.Split(f.Name(), "/") - fileBin := fSplit[len(fSplit)-1] - - log.Infoln(fmt.Sprintf("Attempting to load plugin '%v'", fileBin)) - ext := strings.Split(fileBin, ".") - if len(ext) != 2 { - log.Errorln(fmt.Sprintf("Plugin '%v' must end in .so extension", fileBin)) - return fmt.Errorf("Plugin '%v' must end in .so extension %v", fileBin, len(ext)) - } - if ext[1] != "so" { - log.Errorln(fmt.Sprintf("Plugin '%v' must end in .so extension", fileBin)) - return fmt.Errorf("Plugin '%v' must end in .so extension", fileBin) - } - plug, err := plugin.Open(file) - if err != nil { - log.Errorln(fmt.Sprintf("Plugin '%v' could not load correctly. %v", fileBin, err)) - return err - } - symPlugin, err := plug.Lookup("Plugin") - if err != nil { - log.Errorln(fmt.Sprintf("Plugin '%v' could not locate Plugin variable. %v", fileBin, err)) - return err - } - plugActions, ok := symPlugin.(types.PluginActions) - if !ok { - log.Errorln(fmt.Sprintf("Plugin %v was not type PluginObject", f.Name())) - return fmt.Errorf("Plugin %v was not type PluginActions %v", f.Name(), plugActions.GetInfo()) - } - info := plugActions.GetInfo() - err = plugActions.OnLoad() - if err != nil { - return err - } - log.Infoln(fmt.Sprintf("Plugin %v loaded from %v", info.Name, f.Name())) - core.CoreApp.AllPlugins = append(core.CoreApp.AllPlugins, plugActions) - return nil -} - -func LoadPlugins() { - pluginDir := dir + "/plugins" - log.Infoln(fmt.Sprintf("Loading any available Plugins from /plugins directory")) - if _, err := os.Stat(pluginDir); os.IsNotExist(err) { - os.Mkdir(pluginDir, os.ModePerm) - } - files, err := ioutil.ReadDir(pluginDir) - if err != nil { - log.Warnln(fmt.Sprintf("Plugins directory was not found. Error: %v", err)) - return - } - for _, f := range files { - err := LoadPlugin(f.Name()) - if err != nil { - log.Errorln(err) - continue - } - } - log.Infoln(fmt.Sprintf("Loaded %v Plugins", len(core.CoreApp.Plugins))) -} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go deleted file mode 100644 index 037e2842..00000000 --- a/plugin/plugin_test.go +++ /dev/null @@ -1,51 +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 plugin - -import ( - "github.com/hunterlong/statping/source" - "github.com/hunterlong/statping/types" - "github.com/hunterlong/statping/utils" - "testing" -) - -var ( - example types.PluginActions -) - -func init() { - utils.InitLogs() - source.Assets() -} - -func TestLoadPlugin(t *testing.T) { - //err := LoadPlugin(dir+"/plugins/example.so") - //assert.Nil(t, err) -} - -func TestAdd(t *testing.T) { - //err := Add(example) - //assert.NotNil(t, err) -} - -func TestSelect(t *testing.T) { - //err := example.GetInfo() - //assert.Equal(t, "", err.Name) -} - -//func TestAddRoute(t *testing.T) { -// example.AddRoute("/plugin_example", "GET", setupHandler) -//} diff --git a/source/source.go b/source/source.go index d339745b..73db1d07 100644 --- a/source/source.go +++ b/source/source.go @@ -80,7 +80,9 @@ func UsingAssets(folder string) bool { if _, err := os.Stat(folder + "/assets"); err == nil { return true } else { - if os.Getenv("USE_ASSETS") == "true" { + useAssets := utils.Getenv("USE_ASSETS", false).(bool) + + if useAssets { log.Infoln("Environment variable USE_ASSETS was found.") if err := CreateAllAssets(folder); err != nil { log.Warnln(err) diff --git a/types/checkin.go b/types/checkin.go index 320ee2c2..192d3cdb 100644 --- a/types/checkin.go +++ b/types/checkin.go @@ -36,15 +36,6 @@ type Checkin struct { Failures []*Failure `gorm:"-" json:"failures"` } -// BeforeCreate for Checkin will set CreatedAt to UTC -func (c *Checkin) BeforeCreate() (err error) { - if c.CreatedAt.IsZero() { - c.CreatedAt = time.Now().UTC() - c.UpdatedAt = time.Now().UTC() - } - return -} - // CheckinHit is a successful response from a Checkin type CheckinHit struct { Id int64 `gorm:"primary_key;column:id" json:"id"` diff --git a/types/core.go b/types/core.go index 19c2d8cc..c3d235f8 100644 --- a/types/core.go +++ b/types/core.go @@ -29,29 +29,31 @@ type AllNotifiers interface{} // 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:"-"` - Repos []PluginJSON `gorm:"-" json:"-"` - AllPlugins []PluginActions `gorm:"-" json:"-"` - Notifications []AllNotifiers `gorm:"-" json:"-"` - Config *DbConfig `gorm:"-" json:"-"` - Integrations []Integrator `gorm:"-" json:"-"` + 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:"-"` + Config *DbConfig `gorm:"-" json:"-"` + Integrations []Integrator `gorm:"-" json:"-"` +} + +func (Core) TableName() string { + return "core" } type Servicer interface { diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 00000000..3fe5b602 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,90 @@ +package types + +import ( + "github.com/pkg/errors" + "net/http" +) + +var ( + ErrorServiceSelection = returnErr("error selecting services") + + // create errors + ErrorCreateService = returnErr("error creating service") + ErrorCreateMessage = returnErr("error creating messages") + ErrorCreateIncident = returnErr("error creating incident") + ErrorCreateUser = returnErr("error creating user") + ErrorCreateIncidentUp = returnErr("error creating incident update") + ErrorCreateGroup = returnErr("error creating group") + ErrorCreateCheckinHit = returnErr("error creating checkin hit") + ErrorCreateSampleHits = returnErr("error creating sample hits") + ErrorCreateCore = returnErr("error creating core") + ErrorCreateHit = returnErr("error creating hit for service %v") + + ErrorDirCreate = returnErr("error creating directory %s") + + ErrorFileCopy = returnErr("error copying file %s to %s") + + ErrorConfig = returnErr("error with configuration") + ErrorConnection = returnErr("error with connection") + + ErrorNotFound = returnErrCode("item was not found", http.StatusNotFound) + ErrorJSONParse = returnErrCode("could not parse JSON request", http.StatusBadRequest) +) + +type Errorer interface { +} + +type Error struct { + err error + code int +} + +func (e Error) Error() string { + return e.err.Error() +} + +func (e Error) String() string { + return e.err.Error() +} + +func returnErrCode(str string, code int) error { + return Error{ + err: errors.New(str), + code: code, + } +} + +func returnErr(str string) Error { + return Error{ + err: errors.New(str), + } +} + +func convertError(val interface{}) string { + switch v := val.(type) { + case *Error: + return v.Error() + case string: + return v + default: + return "" + } +} + +type errorer interface { + Error() string +} + +func ErrWrap(err errorer, format interface{}, args ...interface{}) Error { + return Error{ + err: errors.Wrapf(err, convertError(format), args...), + code: 0, + } +} + +func Err(err errorer, format interface{}) Error { + return Error{ + err: errors.Wrap(err, convertError(format)), + code: 0, + } +} diff --git a/types/service.go b/types/service.go index 30a77d59..13912148 100644 --- a/types/service.go +++ b/types/service.go @@ -21,46 +21,46 @@ import ( // 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 []CheckinInterface `gorm:"-" json:"checkins,omitempty" scope:"user,admin"` - Stats *Stats `gorm:"-" json:"stats,omitempty"` + 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 { @@ -72,8 +72,8 @@ type Stater interface { } type Stats struct { - Failures uint64 `gorm:"-" json:"failures,omitempty"` - Hits uint64 `gorm:"-" json:"hits,omitempty"` + Failures int `gorm:"-" json:"failures"` + Hits int `gorm:"-" json:"hits"` } // BeforeCreate for Service will set CreatedAt to UTC @@ -85,6 +85,10 @@ func (s *Service) BeforeCreate() (err error) { 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() { s.Running = make(chan bool) diff --git a/utils/utils.go b/utils/utils.go index 4a381a69..8c2de332 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -44,29 +44,54 @@ var ( // init will set the utils.Directory to the current running directory, or STATPING_DIR if it is set func init() { - if os.Getenv("STATPING_DIR") != "" { - Directory = os.Getenv("STATPING_DIR") - } else { - dir, err := os.Getwd() - if err != nil { - Directory = "." - return - } - Directory = dir + defaultDir, err := os.Getwd() + if err != nil { + defaultDir = "." } + + Directory = Getenv("STATPING_DIR", defaultDir).(string) + // check if logs are disabled - logger := os.Getenv("DISABLE_LOGS") - disableLogs, _ = strconv.ParseBool(logger) + disableLogs = Getenv("DISABLE_LOGS", false).(bool) if disableLogs { Log.Out = ioutil.Discard - return } + Log.Debugln("current working directory: ", Directory) Log.AddHook(new(hook)) Log.SetNoLock() checkVerboseMode() } +func Getenv(key string, defaultValue interface{}) interface{} { + if val, ok := os.LookupEnv(key); ok { + if val != "" { + switch d := defaultValue.(type) { + + case int, int64: + return int(ToInt(val)) + + case time.Duration: + dur, err := time.ParseDuration(val) + if err != nil { + return d + } + return dur + case bool: + ok, err := strconv.ParseBool(val) + if err != nil { + return d + } + return ok + + default: + return val + } + } + } + return defaultValue +} + func SliceConvert(g []*interface{}) []interface{} { var arr []interface{} for _, v := range g {