diff --git a/.gitignore b/.gitignore index da7e1961..68fa2d42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea -rice-box.go +./rice-box.go config.yml statup.db plugins/*.so diff --git a/.travis.yml b/.travis.yml index fcc62ea1..2bba6816 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ services: env: global: - - VERSION=0.29.9 + - VERSION=0.30 - DB_HOST=localhost - DB_USER=travis - DB_PASS= diff --git a/cli.go b/cli.go index cef480c8..38918577 100644 --- a/cli.go +++ b/cli.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "github.com/hunterlong/statup/core" - "github.com/hunterlong/statup/plugin" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "github.com/joho/godotenv" "io/ioutil" @@ -106,7 +106,7 @@ func RunOnce() { utils.Log(4, err) } for _, s := range core.CoreApp.Services { - out := s.Check() + out := core.ServiceCheck(s.ToService()) fmt.Printf(" Service %v | URL: %v | Latency: %0.0fms | Online: %v\n", out.Name, out.Domain, (out.Latency * 1000), out.Online) } } @@ -127,7 +127,7 @@ func HelpEcho() { fmt.Println("Give Statup a Star at https://github.com/hunterlong/statup") } -func TestPlugin(plug plugin.PluginActions) { +func TestPlugin(plug types.PluginActions) { defer utils.DeleteFile("./.plugin_test.db") RenderBoxes() @@ -148,41 +148,41 @@ func TestPlugin(plug plugin.PluginActions) { core.OnLoad(core.DbSession) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnSuccess(Service)'") - core.OnSuccess(core.SelectService(1)) + core.OnSuccess(core.SelectService(1).ToService()) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnFailure(Service, FailureData)'") fakeFailD := core.FailureData{ Issue: "No issue, just testing this plugin. This would include HTTP failure information though", } - core.OnFailure(core.SelectService(1), fakeFailD) + core.OnFailure(core.SelectService(1).ToService(), fakeFailD) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnSettingsSaved(Core)'") fmt.Println(BRAKER) - core.OnSettingsSaved(core.CoreApp) + core.OnSettingsSaved(core.CoreApp.ToCore()) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnNewService(Service)'") - core.OnNewService(core.SelectService(2)) + core.OnNewService(core.SelectService(2).ToService()) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnNewUser(User)'") user, _ := core.SelectUser(1) core.OnNewUser(user) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnUpdateService(Service)'") - srv := core.SelectService(2) + srv := core.SelectService(2).ToService() srv.Type = "http" srv.Domain = "https://yahoo.com" core.OnUpdateService(srv) fmt.Println("\n" + BRAKER) fmt.Println(POINT + "Sending 'OnDeletedService(Service)'") - core.OnDeletedService(core.SelectService(1)) + core.OnDeletedService(core.SelectService(1).ToService()) fmt.Println("\n" + BRAKER) } -func FakeSeed(plug plugin.PluginActions) { +func FakeSeed(plug types.PluginActions) { var err error core.CoreApp = core.NewCore() - core.CoreApp.AllPlugins = []plugin.PluginActions{plug} + core.CoreApp.AllPlugins = []types.PluginActions{plug} fmt.Printf("\n" + BRAKER) @@ -212,21 +212,21 @@ func FakeSeed(plug plugin.PluginActions) { core.CoreApp.ApiSecret = "0x0x0x0x0" core.CoreApp.ApiKey = "abcdefg12345" - fakeSrv := &core.Service{ + fakeSrv := &types.Service{ Name: "Test Plugin Service", Domain: "https://google.com", Method: "GET", } - fakeSrv.Create() + core.CreateService(fakeSrv) - fakeSrv2 := &core.Service{ + fakeSrv2 := &types.Service{ Name: "Awesome Plugin Service", Domain: "https://netflix.com", Method: "GET", } - fakeSrv2.Create() + core.CreateService(fakeSrv2) - fakeUser := &core.User{ + fakeUser := &types.User{ Id: 6334, Username: "Bulbasaur", Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy", @@ -234,35 +234,37 @@ func FakeSeed(plug plugin.PluginActions) { Admin: true, CreatedAt: time.Now(), } - fakeUser.Create() + core.CreateUser(fakeUser) - fakeUser = &core.User{ + fakeUser = &types.User{ Id: 6335, Username: "Billy", Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy", Email: "info@awesome.com", CreatedAt: time.Now(), } - fakeUser.Create() + core.CreateUser(fakeUser) for i := 0; i <= 50; i++ { dd := core.HitData{ Latency: rand.Float64(), } - fakeSrv.CreateHit(dd) + core.CreateServiceHit(fakeSrv, dd) + dd = core.HitData{ Latency: rand.Float64(), } - fakeSrv2.CreateHit(dd) + core.CreateServiceHit(fakeSrv2, dd) + fail := core.FailureData{ Issue: "This is not an issue, but it would container HTTP response errors.", } - fakeSrv.CreateFailure(fail) + core.CreateServiceFailure(fakeSrv, fail) fail = core.FailureData{ Issue: "HTTP Status Code 521 did not match 200", } - fakeSrv2.CreateFailure(fail) + core.CreateServiceFailure(fakeSrv, fail) } fmt.Println("Seeding example data is complete, running Plugin Tests") diff --git a/core/assets.go b/core/assets.go index fa7d1c99..fbaa6597 100644 --- a/core/assets.go +++ b/core/assets.go @@ -95,8 +95,8 @@ func CreateAllAssets() { utils.Log(1, "Inserting scss, css, emails, and javascript files into assets..") CopyToPublic(ScssBox, "scss", "base.scss") CopyToPublic(ScssBox, "scss", "variables.scss") - CopyToPublic(EmailBox, "emails", "message.html") - CopyToPublic(EmailBox, "emails", "failure.html") + //CopyToPublic(EmailBox, "emails", "message.html") + //CopyToPublic(EmailBox, "emails", "failure.html") CopyToPublic(CssBox, "css", "bootstrap.min.css") CopyToPublic(JsBox, "js", "bootstrap.min.js") CopyToPublic(JsBox, "js", "Chart.bundle.min.js") diff --git a/core/checker.go b/core/checker.go index 976516e7..68ad41c6 100644 --- a/core/checker.go +++ b/core/checker.go @@ -18,21 +18,21 @@ type FailureData types.FailureData func CheckServices() { CoreApp.Services, _ = SelectAllServices() utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services))) - for _, v := range CoreApp.Services { - obj := v + for _, ser := range CoreApp.Services { + s := ser.ToService() //go obj.StartCheckins() - obj.stopRoutine = make(chan struct{}) - go obj.CheckQueue() + s.StopRoutine = make(chan struct{}) + go CheckQueue(s) } } -func (s *Service) CheckQueue() { +func CheckQueue(s *types.Service) { for { select { - case <-s.stopRoutine: + case <-s.StopRoutine: return default: - s.Check() + ServiceCheck(s) if s.Interval < 1 { s.Interval = 1 } @@ -43,7 +43,7 @@ func (s *Service) CheckQueue() { } } -func (s *Service) DNSCheck() (float64, error) { +func DNSCheck(s *types.Service) (float64, error) { t1 := time.Now() url, err := url.Parse(s.Domain) if err != nil { @@ -58,13 +58,13 @@ func (s *Service) DNSCheck() (float64, error) { return subTime, err } -func (s *Service) Check() *Service { - dnsLookup, err := s.DNSCheck() +func ServiceCheck(s *types.Service) *types.Service { + dnsLookup, err := DNSCheck(s) if err != nil { - s.Failure(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 } - s.dnsLookup = dnsLookup + s.DnsLookup = dnsLookup t1 := time.Now() client := http.Client{ Timeout: 30 * time.Second, @@ -77,14 +77,14 @@ func (s *Service) Check() *Service { response, err = client.Get(s.Domain) } if err != nil { - s.Failure(fmt.Sprintf("HTTP Error %v", err)) + RecordFailure(s, fmt.Sprintf("HTTP Error %v", err)) return s } response.Header.Set("User-Agent", "StatupMonitor") t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() if err != nil { - s.Failure(fmt.Sprintf("HTTP Error %v", err)) + RecordFailure(s, fmt.Sprintf("HTTP Error %v", err)) return s } defer response.Body.Close() @@ -100,20 +100,20 @@ func (s *Service) Check() *Service { if !match { s.LastResponse = string(contents) s.LastStatusCode = response.StatusCode - s.Failure(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 } } if s.ExpectedStatus != response.StatusCode { s.LastResponse = string(contents) s.LastStatusCode = response.StatusCode - s.Failure(fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus)) + RecordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus)) return s } s.LastResponse = string(contents) s.LastStatusCode = response.StatusCode s.Online = true - s.Record(response) + RecordSuccess(s, response) return s } @@ -121,23 +121,23 @@ type HitData struct { Latency float64 } -func (s *Service) Record(response *http.Response) { +func RecordSuccess(s *types.Service, response *http.Response) { s.Online = true s.LastOnline = time.Now() data := HitData{ Latency: s.Latency, } - s.CreateHit(data) + CreateServiceHit(s, data) OnSuccess(s) } -func (s *Service) Failure(issue string) { +func RecordFailure(s *types.Service, issue string) { s.Online = false data := FailureData{ Issue: issue, } utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue)) - s.CreateFailure(data) + CreateServiceFailure(s, data) //SendFailureEmail(s) OnFailure(s, data) } diff --git a/core/checkin.go b/core/checkin.go index 0aa6ef70..8b0721ec 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -14,9 +14,9 @@ func (c *Checkin) String() string { return c.Api } -func FindCheckin(api string) *Checkin { - for _, s := range CoreApp.Services { - for _, c := range s.Checkins { +func FindCheckin(api string) *types.Checkin { + for _, ser := range CoreApp.Services { + for _, c := range ser.ToService().Checkins { if c.Api == api { return c } @@ -25,8 +25,8 @@ func FindCheckin(api string) *Checkin { return nil } -func (s *Service) SelectAllCheckins() []*Checkin { - var checkins []*Checkin +func SelectAllCheckins(s *types.Service) []*types.Checkin { + var checkins []*types.Checkin col := DbSession.Collection("checkins").Find("service", s.Id).OrderBy("-id") col.All(&checkins) s.Checkins = checkins @@ -78,33 +78,33 @@ func (f *Checkin) Ago() string { return got } -func (c *Checkin) Run() { - if c.Interval == 0 { - return - } - fmt.Println("checking: ", c.Api) - between := time.Now().Sub(c.Last).Seconds() - if between > float64(c.Interval) { - guard := make(chan struct{}) - c.RecheckCheckinFailure(guard) - <-guard - } - time.Sleep(1 * time.Second) - c.Run() -} - -func (s *Service) StartCheckins() { - for _, c := range s.Checkins { - checkin := c - go checkin.Run() - } -} - -func CheckinProcess() { - for _, s := range CoreApp.Services { - for _, c := range s.Checkins { - checkin := c - go checkin.Run() - } - } -} +//func (c *Checkin) Run() { +// if c.Interval == 0 { +// return +// } +// fmt.Println("checking: ", c.Api) +// between := time.Now().Sub(c.Last).Seconds() +// if between > float64(c.Interval) { +// guard := make(chan struct{}) +// c.RecheckCheckinFailure(guard) +// <-guard +// } +// time.Sleep(1 * time.Second) +// c.Run() +//} +// +//func (s *Service) StartCheckins() { +// for _, c := range s.Checkins { +// checkin := c.(*Checkin) +// go checkin.Run() +// } +//} +// +//func CheckinProcess() { +// for _, s := range CoreApp.Services { +// for _, c := range s.Checkins { +// checkin := c +// go checkin.Run() +// } +// } +//} diff --git a/core/configs.go b/core/configs.go index 5229bac8..e80d81de 100644 --- a/core/configs.go +++ b/core/configs.go @@ -14,7 +14,7 @@ import ( func LoadConfig() (*types.Config, error) { if os.Getenv("DB_CONN") != "" { utils.Log(1, "DB_CONN environment variable was found, sleeping for 30 seconds") - time.Sleep(30 * time.Second) + //time.Sleep(30 * time.Second) return LoadUsingEnv() } Configs = new(types.Config) @@ -85,7 +85,7 @@ func LoadUsingEnv() (*types.Config, error) { DropDatabase() CreateDatabase() - CoreApp = &Core{ + CoreApp = &Core{Core: &types.Core{ Name: dbConfig.Project, Description: dbConfig.Description, Config: "config.yml", @@ -93,22 +93,22 @@ func LoadUsingEnv() (*types.Config, error) { ApiSecret: utils.NewSHA1Hash(16), Domain: dbConfig.Domain, MigrationId: time.Now().Unix(), - } + }} CoreApp.DbConnection = dbConfig.DbConn - err := CoreApp.Insert() + err := InsertCore(CoreApp) if err != nil { utils.Log(3, err) } - admin := &User{ + admin := &types.User{ Username: "admin", Password: "admin", Email: "info@admin.com", Admin: true, } - admin.Create() + CreateUser(admin) LoadSampleData() diff --git a/core/core.go b/core/core.go index 84bec322..947cb9e7 100644 --- a/core/core.go +++ b/core/core.go @@ -3,7 +3,6 @@ package core import ( "github.com/GeertJohan/go.rice" "github.com/hunterlong/statup/notifiers" - "github.com/hunterlong/statup/plugin" "github.com/hunterlong/statup/types" "github.com/pkg/errors" "os" @@ -14,24 +13,8 @@ type PluginJSON types.PluginJSON type PluginRepos types.PluginRepos type Core struct { - Name string `db:"name" json:"name"` - Description string `db:"description" json:"name"` - Config string `db:"config" json:"-"` - ApiKey string `db:"api_key" json:"-"` - ApiSecret string `db:"api_secret" json:"-"` - Style string `db:"style" json:"-"` - Footer string `db:"footer" json:"-"` - Domain string `db:"domain" json:"domain,omitempty"` - Version string `db:"version" json:"version,omitempty"` - MigrationId int64 `db:"migration_id" json:"-"` - UseCdn bool `db:"use_cdn" json:"-"` - Services []*Service `json:"services,omitempty"` - Plugins []plugin.Info - Repos []PluginJSON - AllPlugins []plugin.PluginActions - Communications []notifiers.AllNotifiers - DbConnection string - started time.Time + *types.Core + Services []*Service } var ( @@ -42,7 +25,6 @@ var ( ScssBox *rice.Box JsBox *rice.Box TmplBox *rice.Box - EmailBox *rice.Box SetupMode bool UsingAssets bool VERSION string @@ -54,16 +36,21 @@ func init() { func NewCore() *Core { CoreApp = new(Core) - CoreApp.started = time.Now() + CoreApp.Core = new(types.Core) + CoreApp.Started = time.Now() return CoreApp } -func (c *Core) Insert() error { +func InsertCore(c *Core) error { col := DbSession.Collection("core") - _, err := col.Insert(c) + _, err := col.Insert(c.Core) return err } +func (c *Core) ToCore() *types.Core { + return c.Core +} + func InitApp() { SelectCore() notifiers.Collections = DbSession.Collection("communication") @@ -73,9 +60,9 @@ func InitApp() { go DatabaseMaintence() } -func (c *Core) Update() (*Core, error) { +func UpdateCore(c *Core) (*Core, error) { res := DbSession.Collection("core").Find().Limit(1) - err := res.Update(c) + err := res.Update(c.Core) return c, err } @@ -105,7 +92,8 @@ func (c Core) MobileSASS() string { } func (c Core) AllOnline() bool { - for _, s := range CoreApp.Services { + for _, ser := range CoreApp.Services { + s := ser.ToService() if !s.Online { return false } @@ -114,7 +102,7 @@ func (c Core) AllOnline() bool { } func SelectLastMigration() (int64, error) { - var c *Core + var c *types.Core if DbSession == nil { return 0, errors.New("Database connection has not been created yet") } @@ -126,17 +114,16 @@ func SelectLastMigration() (int64, error) { } func SelectCore() (*Core, error) { - var c *Core + var c *types.Core exists := DbSession.Collection("core").Exists() if !exists { return nil, errors.New("core database has not been setup yet.") } - err := DbSession.Collection("core").Find().One(&c) if err != nil { return nil, err } - CoreApp = c + CoreApp.Core = c CoreApp.DbConnection = Configs.Connection CoreApp.Version = VERSION CoreApp.Services, _ = SelectAllServices() diff --git a/core/database.go b/core/database.go index 1aecfc01..4adf195b 100644 --- a/core/database.go +++ b/core/database.go @@ -119,7 +119,7 @@ func (c *DbConfig) Save() error { DropDatabase() CreateDatabase() - newCore := &Core{ + newCore := &types.Core{ Name: c.Project, Description: c.Description, Config: "config.yml", @@ -131,7 +131,7 @@ func (c *DbConfig) Save() error { col := DbSession.Collection("core") _, err = col.Insert(newCore) if err == nil { - CoreApp = newCore + CoreApp = &Core{Core: newCore} } CoreApp, err = SelectCore() @@ -204,7 +204,7 @@ func RunDatabaseUpgrades() error { panic(err) } CoreApp.MigrationId = currentMigration - CoreApp.Update() + UpdateCore(CoreApp) } return err } diff --git a/core/events.go b/core/events.go index df27fcc0..2cfe3216 100644 --- a/core/events.go +++ b/core/events.go @@ -3,7 +3,7 @@ package core import ( "github.com/fatih/structs" "github.com/hunterlong/statup/notifiers" - "github.com/hunterlong/statup/plugin" + "github.com/hunterlong/statup/types" "upper.io/db.v3/lib/sqlbuilder" ) @@ -13,55 +13,55 @@ func OnLoad(db sqlbuilder.Database) { } } -func OnSuccess(s *Service) { +func OnSuccess(s *types.Service) { for _, p := range CoreApp.AllPlugins { p.OnSuccess(structs.Map(s)) } - notifiers.OnSuccess() + notifiers.OnSuccess(s) } -func OnFailure(s *Service, f FailureData) { +func OnFailure(s *types.Service, f FailureData) { for _, p := range CoreApp.AllPlugins { p.OnFailure(structs.Map(s)) } - notifiers.OnFailure() + notifiers.OnFailure(s) } -func OnSettingsSaved(c *Core) { +func OnSettingsSaved(c *types.Core) { for _, p := range CoreApp.AllPlugins { p.OnSettingsSaved(structs.Map(c)) } } -func OnNewUser(u *User) { +func OnNewUser(u *types.User) { for _, p := range CoreApp.AllPlugins { p.OnNewUser(structs.Map(u)) } } -func OnNewService(s *Service) { +func OnNewService(s *types.Service) { for _, p := range CoreApp.AllPlugins { p.OnNewService(structs.Map(s)) } } -func OnDeletedService(s *Service) { +func OnDeletedService(s *types.Service) { for _, p := range CoreApp.AllPlugins { p.OnDeletedService(structs.Map(s)) } } -func OnUpdateService(s *Service) { +func OnUpdateService(s *types.Service) { for _, p := range CoreApp.AllPlugins { p.OnUpdatedService(structs.Map(s)) } } -func SelectPlugin(name string) plugin.PluginActions { +func SelectPlugin(name string) types.PluginActions { for _, p := range CoreApp.AllPlugins { if p.GetInfo().Name == name { return p } } - return plugin.PluginInfo{} + return types.PluginInfo{} } diff --git a/core/failures.go b/core/failures.go index bb3fdb48..b25cbbe2 100644 --- a/core/failures.go +++ b/core/failures.go @@ -3,13 +3,14 @@ package core import ( "fmt" "github.com/ararog/timeago" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "strings" "time" ) -func (s *Service) CreateFailure(data FailureData) (int64, error) { - fail := &Failure{ +func CreateServiceFailure(s *types.Service, data FailureData) (int64, error) { + fail := &types.Failure{ Issue: data.Issue, Service: s.Id, CreatedAt: time.Now(), @@ -26,8 +27,8 @@ func (s *Service) CreateFailure(data FailureData) (int64, error) { return uuid.(int64), err } -func (s *Service) SelectAllFailures() []*Failure { - var fails []*Failure +func SelectAllFailures(s *types.Service) []*types.Failure { + var fails []*types.Failure col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id") err := col.All(&fails) if err != nil { @@ -36,7 +37,7 @@ func (s *Service) SelectAllFailures() []*Failure { return fails } -func (u *Service) DeleteFailures() { +func DeleteFailures(u *types.Service) { var fails []*Failure col := DbSession.Collection("failures") col.Find("service", u.Id).All(&fails) @@ -45,26 +46,33 @@ func (u *Service) DeleteFailures() { } } -func (s *Service) LimitedFailures() []*Failure { - var fails []*Failure +func (ser *Service) LimitedFailures() []*Failure { + s := ser.ToService() + var fails []*types.Failure + var failArr []*Failure col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id").Limit(10) col.All(&fails) - return fails + for _, f := range fails { + failArr = append(failArr, MakeFailure(f)) + } + return failArr } -func reverseFailures(input []*Failure) []*Failure { +func reverseFailures(input []*types.Failure) []*types.Failure { if len(input) == 0 { return input } return append(reverseFailures(input[1:]), input[0]) } -func (f *Failure) Ago() string { +func (fail *Failure) Ago() string { + f := fail.ToFailure() got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt) return got } -func (f *Failure) Delete() error { +func (fail *Failure) Delete() error { + f := fail.ToFailure() col := DbSession.Collection("failures").Find("id", f.Id) return col.Delete() } @@ -79,19 +87,31 @@ func CountFailures() uint64 { return amount } -func (s *Service) TotalFailures() (uint64, error) { +func (ser *Service) TotalFailures() (uint64, error) { + s := ser.ToService() col := DbSession.Collection("failures").Find("service", s.Id) amount, err := col.Count() return amount, err } -func (s *Service) TotalFailures24Hours() (uint64, error) { +func (ser *Service) TotalFailures24Hours() (uint64, error) { + s := ser.ToService() col := DbSession.Collection("failures").Find("service", s.Id) amount, err := col.Count() return amount, err } -func (f *Failure) ParseError() string { +func (f *Failure) ToFailure() *types.Failure { + return f.F.(*types.Failure) +} + +func MakeFailure(f *types.Failure) *Failure { + fail := &Failure{f} + return fail +} + +func (fail *Failure) ParseError() string { + f := fail.ToFailure() err := strings.Contains(f.Issue, "operation timed out") if err { return fmt.Sprintf("HTTP Request Timed Out") diff --git a/core/hits.go b/core/hits.go index c2d94cd1..38a9f092 100644 --- a/core/hits.go +++ b/core/hits.go @@ -13,7 +13,7 @@ func hitCol() db.Collection { return DbSession.Collection("hits") } -func (s *Service) CreateHit(d HitData) (int64, error) { +func CreateServiceHit(s *types.Service, d HitData) (int64, error) { h := Hit{ Service: s.Id, Latency: d.Latency, @@ -27,14 +27,16 @@ func (s *Service) CreateHit(d HitData) (int64, error) { return uuid.(int64), err } -func (s *Service) Hits() ([]Hit, error) { +func (ser *Service) Hits() ([]Hit, error) { + s := ser.ToService() var hits []Hit col := hitCol().Find("service", s.Id).OrderBy("-id") err := col.All(&hits) return hits, err } -func (s *Service) LimitedHits() ([]*Hit, error) { +func (ser *Service) LimitedHits() ([]*Hit, error) { + s := ser.ToService() var hits []*Hit col := hitCol().Find("service", s.Id).OrderBy("-id").Limit(1024) err := col.All(&hits) @@ -48,14 +50,16 @@ func reverseHits(input []*Hit) []*Hit { return append(reverseHits(input[1:]), input[0]) } -func (s *Service) SelectHitsGroupBy(group string) ([]Hit, error) { +func (ser *Service) SelectHitsGroupBy(group string) ([]Hit, error) { + s := ser.ToService() var hits []Hit col := hitCol().Find("service", s.Id) err := col.All(&hits) return hits, err } -func (s *Service) TotalHits() (uint64, error) { +func (ser *Service) TotalHits() (uint64, error) { + s := ser.ToService() col := hitCol().Find("service", s.Id) amount, err := col.Count() return amount, err diff --git a/core/services.go b/core/services.go index d763ea99..292b6449 100644 --- a/core/services.go +++ b/core/services.go @@ -10,33 +10,12 @@ import ( "upper.io/db.v3" ) -type Failure types.Failure - type Service struct { - Id int64 `db:"id,omitempty" json:"id"` - Name string `db:"name" json:"name"` - Domain string `db:"domain" json:"domain"` - Expected string `db:"expected" json:"expected"` - ExpectedStatus int `db:"expected_status" json:"expected_status"` - Interval int `db:"check_interval" json:"check_interval"` - Type string `db:"check_type" json:"type"` - Method string `db:"method" json:"method"` - PostData string `db:"post_data" json:"post_data"` - Port int `db:"port" json:"port"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Online bool `json:"online"` - Latency float64 `json:"latency"` - Online24Hours float32 `json:"24_hours_online"` - AvgResponse string `json:"avg_response"` - TotalUptime string `json:"uptime"` - OrderId int64 `json:"order_id"` - Failures []*Failure `json:"failures"` - Checkins []*Checkin `json:"checkins"` - stopRoutine chan struct{} - LastResponse string - LastStatusCode int - LastOnline time.Time - dnsLookup float64 `json:"dns_lookup_time"` + s *types.Service +} + +type Failure struct { + F interface{} } func serviceCol() db.Collection { @@ -45,27 +24,31 @@ func serviceCol() db.Collection { func SelectService(id int64) *Service { for _, s := range CoreApp.Services { - if s.Id == id { - return s + ser := s.ToService() + if ser.Id == id { + return &Service{ser} } } return nil } func SelectAllServices() ([]*Service, error) { - var srvcs []*Service + var services []*types.Service + var sers []*Service col := serviceCol().Find() - err := col.All(&srvcs) + err := col.All(&services) if err != nil { utils.Log(3, err) return nil, err } - for _, s := range srvcs { - s.Checkins = s.SelectAllCheckins() - s.Failures = s.SelectAllFailures() + for _, s := range services { + ser := NewService(s) + sers = append(sers, ser) + s.Checkins = SelectAllCheckins(s) + s.Failures = SelectAllFailures(s) } - CoreApp.Services = srvcs - return srvcs, err + CoreApp.Services = sers + return sers, err } func (s *Service) AvgTime() float64 { @@ -80,9 +63,10 @@ func (s *Service) AvgTime() float64 { return val } -func (s *Service) Online24() float32 { - total, _ := s.TotalHits() - failed, _ := s.TotalFailures24Hours() +func (ser *Service) Online24() float32 { + s := ser.ToService() + total, _ := ser.TotalHits() + failed, _ := ser.TotalFailures24Hours() if failed == 0 { s.Online24Hours = 100.00 return s.Online24Hours @@ -106,12 +90,22 @@ type DateScan struct { Value int64 `json:"y"` } -func (s *Service) SmallText() string { - last := s.LimitedFailures() - hits, _ := s.LimitedHits() +func (s *Service) ToService() *types.Service { + return s.s +} + +func NewService(s *types.Service) *Service { + return &Service{s} +} + +func (ser *Service) SmallText() string { + s := ser.ToService() + last := ser.LimitedFailures() + hits, _ := ser.LimitedHits() if !s.Online { if len(last) > 0 { - return fmt.Sprintf("%v on %v", last[0].ParseError(), last[0].CreatedAt.Format("Monday 3:04PM, Jan _2 2006")) + lastFailure := MakeFailure(last[0].ToFailure()) + return fmt.Sprintf("%v on %v", lastFailure.ParseError(), last[0].ToFailure().CreatedAt.Format("Monday 3:04PM, Jan _2 2006")) } else { return fmt.Sprintf("%v is currently offline", s.Name) } @@ -138,7 +132,8 @@ func GroupDataBy(column string, id int64, tm time.Time, increment string) string return sql } -func (s *Service) GraphData() string { +func (ser *Service) GraphData() string { + s := ser.ToService() var d []*DateScan since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0) @@ -172,9 +167,10 @@ func (s *Service) GraphData() string { return string(data) } -func (s *Service) AvgUptime() string { - failed, _ := s.TotalFailures() - total, _ := s.TotalHits() +func (ser *Service) AvgUptime() string { + s := ser.ToService() + failed, _ := ser.TotalFailures() + total, _ := ser.TotalHits() if failed == 0 { s.TotalUptime = "100" return s.TotalUptime @@ -195,10 +191,11 @@ func (s *Service) AvgUptime() string { return s.TotalUptime } -func (u *Service) RemoveArray() []*Service { +func RemoveArray(u *types.Service) []*Service { var srvcs []*Service for _, s := range CoreApp.Services { - if s.Id != u.Id { + ser := s.ToService() + if ser.Id != u.Id { srvcs = append(srvcs, s) } } @@ -206,7 +203,7 @@ func (u *Service) RemoveArray() []*Service { return srvcs } -func (u *Service) Delete() error { +func DeleteService(u *types.Service) error { res := serviceCol().Find("id", u.Id) err := res.Delete() if err != nil { @@ -214,28 +211,27 @@ func (u *Service) Delete() error { return err } utils.Log(1, fmt.Sprintf("Stopping %v Monitoring...", u.Name)) - if u.stopRoutine != nil { - close(u.stopRoutine) + if u.StopRoutine != nil { + close(u.StopRoutine) } utils.Log(1, fmt.Sprintf("Stopped %v Monitoring Service", u.Name)) - u.RemoveArray() + RemoveArray(u) OnDeletedService(u) return err } -func (u *Service) Update(s *Service) *Service { - s.CreatedAt = time.Now() +func UpdateService(u *types.Service) *types.Service { + u.CreatedAt = time.Now() res := serviceCol().Find("id", u.Id) - err := res.Update(s) + err := res.Update(u) if err != nil { utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", u.Name, err)) } - *u = *s OnUpdateService(u) return u } -func (u *Service) Create() (int64, error) { +func CreateService(u *types.Service) (int64, error) { u.CreatedAt = time.Now() uuid, err := serviceCol().Insert(u) if uuid == nil { @@ -243,15 +239,16 @@ func (u *Service) Create() (int64, error) { return 0, err } u.Id = uuid.(int64) - u.stopRoutine = make(chan struct{}) - CoreApp.Services = append(CoreApp.Services, u) + u.StopRoutine = make(chan struct{}) + CoreApp.Services = append(CoreApp.Services, &Service{u}) return uuid.(int64), err } func CountOnline() int { amount := 0 - for _, v := range CoreApp.Services { - if v.Online { + for _, s := range CoreApp.Services { + ser := s.ToService() + if ser.Online { amount++ } } diff --git a/core/setup.go b/core/setup.go index 841ca1f9..80df737a 100644 --- a/core/setup.go +++ b/core/setup.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "os" ) @@ -19,7 +20,7 @@ type ErrorResponse struct { func LoadSampleData() error { utils.Log(1, "Inserting Sample Data...") - s1 := &Service{ + s1 := &types.Service{ Name: "Google", Domain: "https://google.com", ExpectedStatus: 200, @@ -28,7 +29,7 @@ func LoadSampleData() error { Type: "http", Method: "GET", } - s2 := &Service{ + s2 := &types.Service{ Name: "Statup Github", Domain: "https://github.com/hunterlong/statup", ExpectedStatus: 200, @@ -37,7 +38,7 @@ func LoadSampleData() error { Type: "http", Method: "GET", } - s3 := &Service{ + s3 := &types.Service{ Name: "JSON Users Test", Domain: "https://jsonplaceholder.typicode.com/users", ExpectedStatus: 200, @@ -46,7 +47,7 @@ func LoadSampleData() error { Type: "http", Method: "GET", } - s4 := &Service{ + s4 := &types.Service{ Name: "JSON API Tester", Domain: "https://jsonplaceholder.typicode.com/posts", ExpectedStatus: 201, @@ -56,19 +57,19 @@ func LoadSampleData() error { Method: "POST", PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`, } - id, err := s1.Create() + id, err := CreateService(s1) if err != nil { utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) } - id, err = s2.Create() + id, err = CreateService(s2) if err != nil { utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) } - id, err = s3.Create() + id, err = CreateService(s3) if err != nil { utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) } - id, err = s4.Create() + id, err = CreateService(s4) if err != nil { utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) } diff --git a/core/users.go b/core/users.go index a341aa9b..e203b248 100644 --- a/core/users.go +++ b/core/users.go @@ -10,36 +10,36 @@ import ( type User types.User -func SelectUser(id int64) (*User, error) { - var user User +func SelectUser(id int64) (*types.User, error) { + var user *types.User col := DbSession.Collection("users") res := col.Find("id", id) err := res.One(&user) - return &user, err + return user, err } -func SelectUsername(username string) (*User, error) { - var user User +func SelectUsername(username string) (*types.User, error) { + var user *types.User col := DbSession.Collection("users") res := col.Find("username", username) err := res.One(&user) - return &user, err + return user, err } -func (u *User) Delete() error { +func DeleteUser(u *types.User) error { col := DbSession.Collection("users") user := col.Find("id", u.Id) return user.Delete() } -func (u *User) Update() error { +func UpdateUser(u *types.User) error { u.CreatedAt = time.Now() col := DbSession.Collection("users") user := col.Find("id", u.Id) return user.Update(u) } -func (u *User) Create() (int64, error) { +func CreateUser(u *types.User) (int64, error) { u.CreatedAt = time.Now() u.Password = utils.HashPassword(u.Password) u.ApiKey = utils.NewSHA1Hash(5) @@ -63,7 +63,7 @@ func SelectAllUsers() ([]User, error) { return users, err } -func AuthUser(username, password string) (*User, bool) { +func AuthUser(username, password string) (*types.User, bool) { var auth bool user, err := SelectUsername(username) if err != nil { diff --git a/handlers/api.go b/handlers/api.go index a96c86ce..bedc8eb4 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/gorilla/mux" "github.com/hunterlong/statup/core" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" ) @@ -16,6 +17,21 @@ func ApiIndexHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(core.CoreApp) } +func ApiRenewHandler(w http.ResponseWriter, r *http.Request) { + if !isAPIAuthorized(r) { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + var err error + core.CoreApp.ApiKey = utils.NewSHA1Hash(40) + core.CoreApp.ApiSecret = utils.NewSHA1Hash(40) + core.CoreApp, err = core.UpdateCore(core.CoreApp) + if err != nil { + utils.Log(3, err) + } + http.Redirect(w, r, "/settings", http.StatusSeeOther) +} + func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) { if !isAPIAuthorized(r) { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) @@ -23,7 +39,7 @@ func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) checkin := core.FindCheckin(vars["api"]) - checkin.Receivehit() + //checkin.Receivehit() w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(checkin) } @@ -44,11 +60,12 @@ func ApiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - var s core.Service + serv := core.SelectService(utils.StringInt(vars["id"])) + var s *types.Service decoder := json.NewDecoder(r.Body) decoder.Decode(&s) - service.Update(&s) + service := serv.ToService() + core.UpdateService(service) json.NewEncoder(w).Encode(s) } diff --git a/handlers/index.go b/handlers/index.go index c9b8a393..f5cf56c3 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -6,8 +6,7 @@ import ( ) type index struct { - Core core.Core - Services []*core.Service + Core *core.Core } func IndexHandler(w http.ResponseWriter, r *http.Request) { @@ -15,6 +14,5 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/setup", http.StatusSeeOther) return } - out := index{*core.CoreApp, core.CoreApp.Services} - ExecuteResponse(w, r, "index.html", out) + ExecuteResponse(w, r, "index.html", core.CoreApp) } diff --git a/handlers/prometheus.go b/handlers/prometheus.go index 96576a50..efdace07 100644 --- a/handlers/prometheus.go +++ b/handlers/prometheus.go @@ -28,7 +28,8 @@ func PrometheusHandler(w http.ResponseWriter, r *http.Request) { system := fmt.Sprintf("statup_total_failures %v\n", core.CountFailures()) system += fmt.Sprintf("statup_total_services %v", len(core.CoreApp.Services)) metrics = append(metrics, system) - for _, v := range core.CoreApp.Services { + for _, ser := range core.CoreApp.Services { + v := ser.ToService() online := 1 if !v.Online { online = 0 diff --git a/handlers/routes.go b/handlers/routes.go index 873afe43..c71e38d6 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -33,7 +33,7 @@ func Router() *mux.Router { r.Handle("/user/{id}", http.HandlerFunc(UsersEditHandler)).Methods("GET") r.Handle("/user/{id}", http.HandlerFunc(UpdateUserHandler)).Methods("POST") r.Handle("/user/{id}/delete", http.HandlerFunc(UsersDeleteHandler)).Methods("GET") - r.Handle("/settings", http.HandlerFunc(PluginsHandler)).Methods("GET") + r.Handle("/settings", http.HandlerFunc(SettingsHandler)).Methods("GET") r.Handle("/settings", http.HandlerFunc(SaveSettingsHandler)).Methods("POST") r.Handle("/settings/css", http.HandlerFunc(SaveSASSHandler)).Methods("POST") r.Handle("/settings/build", http.HandlerFunc(SaveAssetsHandler)).Methods("GET") @@ -42,6 +42,7 @@ func Router() *mux.Router { r.Handle("/plugins/{name}/save", http.HandlerFunc(PluginSavedHandler)).Methods("POST") r.Handle("/help", http.HandlerFunc(HelpHandler)) r.Handle("/api", http.HandlerFunc(ApiIndexHandler)) + r.Handle("/api/renew", http.HandlerFunc(ApiRenewHandler)) r.Handle("/api/checkin/{api}", http.HandlerFunc(ApiCheckinHandler)) r.Handle("/api/services", http.HandlerFunc(ApiAllServicesHandler)) r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceHandler)).Methods("GET") diff --git a/handlers/services.go b/handlers/services.go index 23e8b0b8..0e7bea86 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gorilla/mux" "github.com/hunterlong/statup/core" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" "strconv" @@ -40,7 +41,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { checkType := r.PostForm.Get("check_type") postData := r.PostForm.Get("post_data") - service := &core.Service{ + service := &types.Service{ Name: name, Domain: domain, Method: method, @@ -51,12 +52,12 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { Port: port, PostData: postData, } - _, err := service.Create() + _, err := core.CreateService(service) if err != nil { utils.Log(3, fmt.Sprintf("Error starting %v check routine. %v", service.Name, err)) } - go service.CheckQueue() + go core.CheckQueue(service) core.OnNewService(service) http.Redirect(w, r, "/services", http.StatusSeeOther) @@ -68,21 +69,25 @@ func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - service.Delete() + serv := core.SelectService(utils.StringInt(vars["id"])) + service := serv.ToService() + core.DeleteService(service) http.Redirect(w, r, "/services", http.StatusSeeOther) } func ServicesViewHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - ExecuteResponse(w, r, "service.html", service) + serv := core.SelectService(utils.StringInt(vars["id"])) + + fmt.Println(serv.ToService()) + + ExecuteResponse(w, r, "service.html", serv) } func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - + serv := core.SelectService(utils.StringInt(vars["id"])) + service := serv.ToService() var badge []byte if service.Online { badge = []byte(`` + service.Name + `` + service.Name + `onlineonline`) @@ -92,9 +97,7 @@ func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Write(badge) - } func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) { @@ -103,7 +106,8 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) + serv := core.SelectService(utils.StringInt(vars["id"])) + service := serv.ToService() r.ParseForm() name := r.PostForm.Get("name") domain := r.PostForm.Get("domain") @@ -114,7 +118,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) { port, _ := strconv.Atoi(r.PostForm.Get("port")) checkType := r.PostForm.Get("check_type") postData := r.PostForm.Get("post_data") - serviceUpdate := &core.Service{ + serviceUpdate := &types.Service{ Id: service.Id, Name: name, Domain: domain, @@ -126,7 +130,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) { Port: port, PostData: postData, } - service = service.Update(serviceUpdate) + service = core.UpdateService(serviceUpdate) ExecuteResponse(w, r, "service.html", service) } @@ -136,9 +140,9 @@ func ServicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) { return } vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - - service.DeleteFailures() + serv := core.SelectService(utils.StringInt(vars["id"])) + service := serv.ToService() + core.DeleteFailures(service) core.CoreApp.Services, _ = core.SelectAllServices() http.Redirect(w, r, "/services", http.StatusSeeOther) } @@ -150,7 +154,8 @@ func CheckinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) { } vars := mux.Vars(r) interval := utils.StringInt(r.PostForm.Get("interval")) - service := core.SelectService(utils.StringInt(vars["id"])) + serv := core.SelectService(utils.StringInt(vars["id"])) + service := serv.ToService() checkin := &core.Checkin{ Service: service.Id, Interval: interval, diff --git a/handlers/settings.go b/handlers/settings.go index 59951b21..9b9a230a 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -9,23 +9,11 @@ import ( "net/http" ) -func PluginsHandler(w http.ResponseWriter, r *http.Request) { +func SettingsHandler(w http.ResponseWriter, r *http.Request) { if !IsAuthenticated(r) { http.Redirect(w, r, "/", http.StatusSeeOther) return } - //CoreApp.FetchPluginRepo() - - //var pluginFields []PluginSelect - // - //for _, p := range allPlugins { - // fields := structs.Map(p.GetInfo()) - // - // pluginFields = append(pluginFields, PluginSelect{p.GetInfo().Name, p.GetForm(), fields}) - //} - - //CoreApp.PluginFields = pluginFields - ExecuteResponse(w, r, "settings.html", core.CoreApp) } @@ -56,8 +44,8 @@ func SaveSettingsHandler(w http.ResponseWriter, r *http.Request) { core.CoreApp.Domain = domain } core.CoreApp.UseCdn = (r.PostForm.Get("enable_cdn") == "on") - core.CoreApp.Update() - core.OnSettingsSaved(core.CoreApp) + core.CoreApp, _ = core.UpdateCore(core.CoreApp) + core.OnSettingsSaved(core.CoreApp.ToCore()) http.Redirect(w, r, "/settings", http.StatusSeeOther) } @@ -105,8 +93,8 @@ func SaveNotificationHandler(w http.ResponseWriter, r *http.Request) { var2 := r.PostForm.Get("var2") apiKey := r.PostForm.Get("api_key") apiSecret := r.PostForm.Get("api_secret") - limits := int64(utils.StringInt(r.PostForm.Get("limits"))) - notifer := notifiers.Select(utils.StringInt(notifierId)) + limits := int(utils.StringInt(r.PostForm.Get("limits"))) + notifer := notifiers.SelectNotifier(utils.StringInt(notifierId)).Select() if host != "" { notifer.Host = host diff --git a/handlers/setup.go b/handlers/setup.go index 975b8482..d6d1d18c 100644 --- a/handlers/setup.go +++ b/handlers/setup.go @@ -104,13 +104,13 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) { return } - admin := &core.User{ + admin := &types.User{ Username: config.Username, Password: config.Password, Email: config.Email, Admin: true, } - admin.Create() + core.CreateUser(admin) if sample == "on" { core.LoadSampleData() diff --git a/handlers/users.go b/handlers/users.go index 854d1bdf..61a8af26 100644 --- a/handlers/users.go +++ b/handlers/users.go @@ -64,7 +64,7 @@ func UpdateUserHandler(w http.ResponseWriter, r *http.Request) { if password != "##########" { user.Password = utils.HashPassword(password) } - user.Update() + core.UpdateUser(user) users, _ := core.SelectAllUsers() ExecuteResponse(w, r, "users.html", users) } @@ -80,13 +80,13 @@ func CreateUserHandler(w http.ResponseWriter, r *http.Request) { email := r.PostForm.Get("email") admin := r.PostForm.Get("admin") - user := &core.User{ + user := &types.User{ Username: username, Password: password, Email: email, Admin: (admin == "on"), } - _, err := user.Create() + _, err := core.CreateUser(user) if err != nil { utils.Log(2, err) } @@ -108,6 +108,6 @@ func UsersDeleteHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/users", http.StatusSeeOther) return } - user.Delete() + core.DeleteUser(user) http.Redirect(w, r, "/users", http.StatusSeeOther) } diff --git a/main.go b/main.go index cdc76b76..9de838e7 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ import ( "github.com/fatih/structs" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/handlers" - "github.com/hunterlong/statup/plugin" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "github.com/joho/godotenv" "io/ioutil" @@ -50,7 +50,6 @@ func RenderBoxes() { core.ScssBox = rice.MustFindBox("source/scss") core.JsBox = rice.MustFindBox("source/js") core.TmplBox = rice.MustFindBox("source/tmpl") - core.EmailBox = rice.MustFindBox("source/emails") } func LoadDotEnvs() { @@ -124,8 +123,8 @@ func LoadPlugins(debug bool) { utils.Log(1, structs.Map(symPlugin)) } - var plugActions plugin.PluginActions - plugActions, ok := symPlugin.(plugin.PluginActions) + var plugActions types.PluginActions + plugActions, ok := symPlugin.(types.PluginActions) if !ok { utils.Log(3, fmt.Sprintf("Plugin '%v' could not load correctly, error: %v", f.Name(), err)) if debug { diff --git a/main_test.go b/main_test.go index 40256ddc..7850b93b 100644 --- a/main_test.go +++ b/main_test.go @@ -6,6 +6,7 @@ import ( "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/handlers" "github.com/hunterlong/statup/notifiers" + "github.com/hunterlong/statup/types" "github.com/rendon/testcli" "github.com/stretchr/testify/assert" "net/http" @@ -31,7 +32,7 @@ func RunInit(t *testing.T) { core.CoreApp = core.NewCore() } -var forceSequential chan bool = make(chan bool, 1) +var forceSequential = make(chan bool, 1) func TestRunAll(t *testing.T) { @@ -187,7 +188,6 @@ func TestAssetsCommand(t *testing.T) { assert.True(t, fileExists("assets/js/main.js")) assert.True(t, fileExists("assets/css/base.css")) assert.True(t, fileExists("assets/scss/base.scss")) - assert.True(t, fileExists("assets/emails/failure.html")) } func RunMySQLMakeConfig(t *testing.T, db string) { @@ -267,22 +267,22 @@ func RunUser_SelectAll(t *testing.T) { } func RunUser_Create(t *testing.T) { - user := &core.User{ + user := &types.User{ Username: "admin", Password: "admin", Email: "info@testuser.com", Admin: true, } - id, err := user.Create() + id, err := core.CreateUser(user) assert.Nil(t, err) assert.Equal(t, int64(1), id) - user2 := &core.User{ + user2 := &types.User{ Username: "superadmin", Password: "admin", Email: "info@adminer.com", Admin: true, } - id, err = user2.Create() + id, err = core.CreateUser(user2) assert.Nil(t, err) assert.Equal(t, int64(2), id) } @@ -291,7 +291,7 @@ func RunUser_Update(t *testing.T) { user, err := core.SelectUser(1) user.Email = "info@updatedemail.com" assert.Nil(t, err) - err = user.Update() + err = core.UpdateUser(user) assert.Nil(t, err) updatedUser, err := core.SelectUser(1) assert.Nil(t, err) @@ -299,12 +299,12 @@ func RunUser_Update(t *testing.T) { } func RunUser_NonUniqueCreate(t *testing.T) { - user := &core.User{ + user := &types.User{ Username: "admin", Password: "admin", Email: "info@testuser.com", } - _, err := user.Create() + _, err := core.CreateUser(user) assert.NotNil(t, err) } @@ -312,7 +312,7 @@ func RunUser_Delete(t *testing.T) { user, err := core.SelectUser(2) assert.Nil(t, err) assert.NotNil(t, user) - err = user.Delete() + err = core.DeleteUser(user) assert.Nil(t, err) } @@ -327,11 +327,11 @@ func RunOneService_Check(t *testing.T) { service := core.SelectService(1) assert.NotNil(t, service) t.Log(service) - assert.Equal(t, "Google", service.Name) + assert.Equal(t, "Google", service.ToService().Name) } func RunService_Create(t *testing.T) { - service := &core.Service{ + service := &types.Service{ Name: "test service", Domain: "https://google.com", ExpectedStatus: 200, @@ -340,7 +340,7 @@ func RunService_Create(t *testing.T) { Type: "http", Method: "GET", } - id, err := service.Create() + id, err := core.CreateService(service) assert.Nil(t, err) assert.Equal(t, int64(5), id) t.Log(service) @@ -371,7 +371,7 @@ func RunService_GraphData(t *testing.T) { } func RunBadService_Create(t *testing.T) { - service := &core.Service{ + service := &types.Service{ Name: "Bad Service", Domain: "https://9839f83h72gey2g29278hd2od2d.com", ExpectedStatus: 200, @@ -380,7 +380,7 @@ func RunBadService_Create(t *testing.T) { Type: "http", Method: "GET", } - id, err := service.Create() + id, err := core.CreateService(service) assert.Nil(t, err) assert.Equal(t, int64(6), id) } @@ -388,14 +388,14 @@ func RunBadService_Create(t *testing.T) { func RunBadService_Check(t *testing.T) { service := core.SelectService(4) assert.NotNil(t, service) - assert.Equal(t, "JSON API Tester", service.Name) + assert.Equal(t, "JSON API Tester", service.ToService().Name) } func RunDeleteService(t *testing.T) { service := core.SelectService(4) assert.NotNil(t, service) - assert.Equal(t, "JSON API Tester", service.Name) - err := service.Delete() + assert.Equal(t, "JSON API Tester", service.ToService().Name) + err := core.DeleteService(service.ToService()) assert.Nil(t, err) } @@ -405,7 +405,7 @@ func RunCreateService_Hits(t *testing.T) { assert.NotNil(t, services) for i := 0; i <= 10; i++ { for _, s := range services { - service := s.Check() + service := core.ServiceCheck(s.ToService()) assert.NotNil(t, service) } } @@ -423,7 +423,7 @@ func RunService_Failures(t *testing.T) { t.SkipNow() service := core.SelectService(6) assert.NotNil(t, service) - assert.NotEmpty(t, service.Failures) + assert.NotEmpty(t, service.ToService().Failures) } func RunService_LimitedHits(t *testing.T) { diff --git a/notifiers/email.go b/notifiers/email.go index ab9631c0..ad9715e9 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -20,12 +20,13 @@ const ( var ( emailer *Email emailArray []string - emailQueue []*types.Email + emailQueue []*EmailOutgoing + emailBox *rice.Box + mailer *gomail.Dialer ) type Email struct { *Notification - mailer *gomail.Dialer } // DEFINE YOUR NOTIFICATION HERE. @@ -36,41 +37,35 @@ func init() { Id: EMAIL_ID, Method: EMAIL_METHOD, Form: []NotificationForm{{ - id: 1, + Id: 1, Type: "text", Title: "SMTP Host", Placeholder: "Insert your SMTP Host here.", DbField: "Host", }, { - id: 1, + Id: 1, Type: "text", Title: "SMTP Username", Placeholder: "Insert your SMTP Username here.", DbField: "Username", }, { - id: 1, + Id: 1, Type: "password", Title: "SMTP Password", Placeholder: "Insert your SMTP Password here.", DbField: "Password", }, { - id: 1, + Id: 1, Type: "number", Title: "SMTP Port", Placeholder: "Insert your SMTP Port here.", DbField: "Port", }, { - id: 1, + Id: 1, Type: "text", Title: "Outgoing Email Address", Placeholder: "Insert your Outgoing Email Address", DbField: "Var1", - }, { - id: 1, - Type: "number", - Title: "Limits per Hour", - Placeholder: "How many emails can it send per hour", - DbField: "Limits", }}, }} @@ -84,7 +79,9 @@ func (u *Email) Select() *Notification { // WHEN NOTIFIER LOADS func (u *Email) Init() error { + emailBox = rice.MustFindBox("emails") err := u.Install() + utils.Log(1, fmt.Sprintf("Creating Mailer: %v:%v", u.Notification.Host, u.Notification.Port)) if err == nil { notifier, _ := SelectNotification(u.Id) @@ -94,29 +91,43 @@ func (u *Email) Init() error { if u.Enabled { utils.Log(1, fmt.Sprintf("Loading SMTP Emailer using host: %v:%v", u.Notification.Host, u.Notification.Port)) - u.mailer = gomail.NewDialer(u.Notification.Host, u.Notification.Port, u.Notification.Username, u.Notification.Password) - u.mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true} + mailer = gomail.NewDialer(u.Notification.Host, u.Notification.Port, u.Notification.Username, u.Notification.Password) + mailer.TLSConfig = &tls.Config{InsecureSkipVerify: true} go u.Run() } } - - //go u.Run() return nil } func (u *Email) Test() error { - //email := &types.Email{ - // To: "info@socialeck.com", - // Subject: "Test Email", - // Template: "message.html", - // Data: nil, - // From: emailer.Var1, - //} - //SendEmail(core.EmailBox, email) + if u.Enabled { + email := &EmailOutgoing{ + To: "info@socialeck.com", + Subject: "Test Email", + Template: "message.html", + Data: nil, + From: emailer.Var1, + } + SendEmail(emailBox, email) + } return nil } +type emailMessage struct { + Service *types.Service +} + +type EmailOutgoing struct { + To string + Subject string + Template string + From string + Data interface{} + Source string + Sent bool +} + // AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS func (u *Email) Run() error { var sentAddresses []string @@ -125,16 +136,18 @@ func (u *Email) Run() error { emailQueue = removeEmail(emailQueue, email) continue } - e := email - go func(email *types.Email) { + if u.CanSend() { err := u.dialSend(email) if err == nil { email.Sent = true sentAddresses = append(sentAddresses, email.To) utils.Log(1, fmt.Sprintf("Email '%v' sent to: %v using the %v template (size: %v)", email.Subject, email.To, email.Template, len([]byte(email.Source)))) emailQueue = removeEmail(emailQueue, email) + u.Log(fmt.Sprintf("Subject: %v to %v", email.Subject, email.To)) + } else { + utils.Log(3, fmt.Sprintf("Email Notifier could not send email: %v", err)) } - }(e) + } } time.Sleep(60 * time.Second) if u.Enabled { @@ -144,19 +157,30 @@ func (u *Email) Run() error { } // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS -func (u *Email) OnFailure() error { +func (u *Email) OnFailure(s *types.Service) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) - // Do failing stuff here! + + msg := emailMessage{ + Service: s, + } + + email := &EmailOutgoing{ + To: "info@socialeck.com", + Subject: fmt.Sprintf("Service %v is Failing", s.Name), + Template: "failure.html", + Data: msg, + From: emailer.Var1, + } + SendEmail(emailBox, email) + } return nil } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS -func (u *Email) OnSuccess() error { +func (u *Email) OnSuccess(s *types.Service) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) - // Do failing stuff here! + } return nil } @@ -172,7 +196,7 @@ func (u *Email) OnSave() error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Email) Install() error { - inDb, err := emailer.Notification.isInDatabase() + inDb, err := emailer.Notification.IsInDatabase() if !inDb { newNotifer, err := InsertDatabase(u.Notification) if err != nil { @@ -184,20 +208,21 @@ func (u *Email) Install() error { return err } -func (u *Email) dialSend(email *types.Email) error { +func (u *Email) dialSend(email *EmailOutgoing) error { + fmt.Println("sending dailsend to emailer") m := gomail.NewMessage() m.SetHeader("From", email.From) m.SetHeader("To", email.To) m.SetHeader("Subject", email.Subject) m.SetBody("text/html", email.Source) - if err := u.mailer.DialAndSend(m); err != nil { + if err := mailer.DialAndSend(m); err != nil { utils.Log(3, fmt.Sprintf("Email '%v' sent to: %v using the %v template (size: %v) %v", email.Subject, email.To, email.Template, len([]byte(email.Source)), err)) return err } return nil } -func SendEmail(box *rice.Box, email *types.Email) { +func SendEmail(box *rice.Box, email *EmailOutgoing) { source := EmailTemplate(box, email.Template, email.Data) email.Source = source emailQueue = append(emailQueue, email) @@ -221,8 +246,8 @@ func EmailTemplate(box *rice.Box, tmpl string, data interface{}) string { return result } -func removeEmail(emails []*types.Email, em *types.Email) []*types.Email { - var newArr []*types.Email +func removeEmail(emails []*EmailOutgoing, em *EmailOutgoing) []*EmailOutgoing { + var newArr []*EmailOutgoing for _, e := range emails { if e != em { newArr = append(newArr, e) diff --git a/source/emails/failure.html b/notifiers/emails/failure.html similarity index 84% rename from source/emails/failure.html rename to notifiers/emails/failure.html index ae964e3c..2d11e1c4 100644 --- a/source/emails/failure.html +++ b/notifiers/emails/failure.html @@ -50,15 +50,13 @@
- View Service + View Service - Statup Dashboard + Statup Dashboard
- - diff --git a/source/emails/message.html b/notifiers/emails/message.html similarity index 100% rename from source/emails/message.html rename to notifiers/emails/message.html diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index 07401e2f..f1b2c8e4 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -2,6 +2,7 @@ package notifiers import ( "fmt" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "strings" "time" @@ -9,28 +10,11 @@ import ( ) var ( - AllCommunications []AllNotifiers + AllCommunications []types.AllNotifiers Collections db.Collection + Logs []*NotificationLog ) -type AllNotifiers interface{} - -func add(c interface{}) { - AllCommunications = append(AllCommunications, c) -} - -func Load() []AllNotifiers { - utils.Log(1, "Loading notifiers") - var notifiers []AllNotifiers - for _, comm := range AllCommunications { - n := comm.(Notifier) - n.Init() - notifiers = append(notifiers, n) - n.Test() - } - return notifiers -} - type Notification struct { Id int64 `db:"id,omitempty" json:"id"` Method string `db:"method" json:"method"` @@ -43,7 +27,7 @@ type Notification struct { ApiKey string `db:"api_key" json:"-"` ApiSecret string `db:"api_secret" json:"-"` Enabled bool `db:"enabled" json:"enabled"` - Limits int64 `db:"limits" json:"-"` + Limits int `db:"limits" json:"-"` Removable bool `db:"removable" json:"-"` CreatedAt time.Time `db:"created_at" json:"created_at"` Form []NotificationForm @@ -54,21 +38,69 @@ type Notifier interface { Init() error Install() error Run() error - OnFailure() error - OnSuccess() error + OnFailure(*types.Service) error + OnSuccess(*types.Service) error Select() *Notification Test() error } type NotificationForm struct { - id int64 + Id int64 Type string Title string Placeholder string DbField string } -func (n *Notification) isInDatabase() (bool, error) { +type NotificationLog struct { + Notifier *Notification + Message string + Time utils.Timestamp +} + +func add(c interface{}) { + AllCommunications = append(AllCommunications, c) +} + +func Load() []types.AllNotifiers { + utils.Log(1, "Loading notifiers") + var notifiers []types.AllNotifiers + for _, comm := range AllCommunications { + n := comm.(Notifier) + n.Init() + notifiers = append(notifiers, n) + n.Test() + } + return notifiers +} + +func (n *Notification) Log(msg string) { + log := &NotificationLog{ + Notifier: n, + Message: msg, + Time: utils.Timestamp(time.Now()), + } + Logs = append(Logs, log) +} + +func (n *Notification) Logs() []*NotificationLog { + var logs []*NotificationLog + for _, v := range Logs { + if v.Notifier.Id == n.Id { + logs = append(logs, v) + } + } + return reverseLogs(logs) +} + +func reverseLogs(input []*NotificationLog) []*NotificationLog { + if len(input) == 0 { + return input + } + return append(reverseLogs(input[1:]), input[0]) +} + +func (n *Notification) IsInDatabase() (bool, error) { return Collections.Find("id", n.Id).Exists() } @@ -81,12 +113,12 @@ func SelectNotification(id int64) (*Notification, error) { func (n *Notification) Update() (*Notification, error) { n.CreatedAt = time.Now() err := Collections.Find("id", n.Id).Update(n) - return n, err } func InsertDatabase(n *Notification) (int64, error) { n.CreatedAt = time.Now() + n.Limits = 3 newId, err := Collections.Insert(n) if err != nil { return 0, err @@ -94,18 +126,6 @@ func InsertDatabase(n *Notification) (int64, error) { return newId.(int64), err } -func Select(id int64) *Notification { - var notifier *Notification - for _, n := range AllCommunications { - notif := n.(Notifier) - notifier = notif.Select() - if notifier.Id == id { - return notifier - } - } - return notifier -} - func SelectNotifier(id int64) Notifier { var notifier Notifier for _, n := range AllCommunications { @@ -118,9 +138,33 @@ func SelectNotifier(id int64) Notifier { return notifier } +func (f Notification) CanSend() bool { + if f.SentLastHour() >= f.Limits { + return false + } + return true +} + +func (f Notification) SentLastHour() int { + sent := 0 + hourAgo := time.Now().Add(-1 * time.Hour) + for _, v := range f.Logs() { + lastTime := time.Time(v.Time) + if lastTime.After(hourAgo) { + sent++ + } + } + return sent +} + func (f NotificationForm) Value() string { - notifier := Select(f.id) - return notifier.GetValue(f.DbField) + noti := SelectNotifier(f.Id) + return noti.Select().GetValue(f.DbField) +} + +func (f Notification) LimitValue() int64 { + notifier, _ := SelectNotification(f.Id) + return utils.StringInt(notifier.GetValue("limits")) } func (n *Notification) GetValue(dbField string) string { @@ -144,30 +188,42 @@ func (n *Notification) GetValue(dbField string) string { return n.ApiKey case "api_secret": return n.ApiSecret + case "limits": + return utils.IntString(int(n.Limits)) } return "" } -func OnFailure() { +func OnFailure(s *types.Service) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnFailure() + n.OnFailure(s) } } -func OnSuccess() { +func OnSuccess(s *types.Service) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnSuccess() + n.OnSuccess(s) } } -func uniqueMessages(arr []string, v string) []string { - var newArray []string - for _, i := range arr { - if i != v { - newArray = append(newArray, v) +func uniqueStrings(elements []string) []string { + result := []string{} + + for i := 0; i < len(elements); i++ { + // Scan slice for a previous element of the same value. + exists := false + for v := 0; v < i; v++ { + if elements[v] == elements[i] { + exists = true + break + } + } + // If no previous element exists, append this one. + if !exists { + result = append(result, elements[i]) } } - return newArray + return result } diff --git a/notifiers/rice-box.go b/notifiers/rice-box.go new file mode 100644 index 00000000..fc32458a --- /dev/null +++ b/notifiers/rice-box.go @@ -0,0 +1,48 @@ +package notifiers + +import ( + "github.com/GeertJohan/go.rice/embedded" + "time" +) + +func init() { + + // define files + file2 := &embedded.EmbeddedFile{ + Filename: "failure.html", + FileModTime: time.Unix(1531720141, 0), + Content: string("\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

{{ .Service.Name }} is Offline!

\n

\n Your Statup service '{{.Service.Name}}' has been triggered with a HTTP status code of '{{.Service.LastStatusCode}}' and is currently offline based on your requirements. This failure was created on {{.Service.CreatedAt}}.\n

\n\n {{if .Service.LastResponse }}\n

Last Response

\n

\n {{ .Service.LastResponse }}\n

\n {{end}}\n\n \n \n \n
\n View Service\n \n Statup Dashboard\n
\n
\n
\n
\n\n"), + } + file3 := &embedded.EmbeddedFile{ + Filename: "message.html", + FileModTime: time.Unix(1530546686, 0), + Content: string("\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

Looks Like Emails Work!

\n

\n Since you got this email, it confirms that your Statup Status Page email system is working correctly.\n

\n

\n

\n Enjoy using Statup!\n
Statup.io Team

\n\n \n\n
\n
\n
\n
\n\n"), + } + + // define dirs + dir1 := &embedded.EmbeddedDir{ + Filename: "", + DirModTime: time.Unix(1531720141, 0), + ChildFiles: []*embedded.EmbeddedFile{ + file2, // "failure.html" + file3, // "message.html" + + }, + } + + // link ChildDirs + dir1.ChildDirs = []*embedded.EmbeddedDir{} + + // register embeddedBox + embedded.RegisterEmbeddedBox(`emails`, &embedded.EmbeddedBox{ + Name: `emails`, + Time: time.Unix(1531720141, 0), + Dirs: map[string]*embedded.EmbeddedDir{ + "": dir1, + }, + Files: map[string]*embedded.EmbeddedFile{ + "failure.html": file2, + "message.html": file3, + }, + }) +} diff --git a/notifiers/slack.go b/notifiers/slack.go index b7994f07..f7df1b9d 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -3,25 +3,37 @@ package notifiers import ( "bytes" "fmt" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" + "sync" + "text/template" "time" ) const ( - SLACK_ID = 2 - SLACK_METHOD = "slack" + SLACK_ID = 2 + SLACK_METHOD = "slack" + FAILING_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected", "value": "{{.Service.Expected}}", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#FF0000", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` + SUCCESS_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#00FF00", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` + TEST_TEMPLATE = `{"text":"{{.}}"}` ) var ( slacker *Slack slackMessages []string + messageLock *sync.Mutex ) type Slack struct { *Notification } +type slackMessage struct { + Service *types.Service + Time int64 +} + // DEFINE YOUR NOTIFICATION HERE. func init() { slacker = &Slack{&Notification{ @@ -29,7 +41,7 @@ func init() { Method: SLACK_METHOD, Host: "https://webhooksurl.slack.com/***", Form: []NotificationForm{{ - id: 2, + Id: 2, Type: "text", Title: "Incoming Webhook Url", Placeholder: "Insert your Slack webhook URL here.", @@ -37,6 +49,7 @@ func init() { }}}, } add(slacker) + messageLock = new(sync.Mutex) } // Select Obj @@ -46,9 +59,7 @@ func (u *Slack) Select() *Notification { // WHEN NOTIFIER LOADS func (u *Slack) Init() error { - err := u.Install() - if err == nil { notifier, _ := SelectNotification(u.Id) forms := u.Form @@ -63,21 +74,29 @@ func (u *Slack) Init() error { } func (u *Slack) Test() error { - SendSlack("Slack notifications on your Statup server is working!") + msg := fmt.Sprintf("You're Statup Slack Notifier is working correctly!") + SendSlack(TEST_TEMPLATE, msg) return nil } // AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS func (u *Slack) Run() error { + messageLock.Lock() + slackMessages = uniqueStrings(slackMessages) for _, msg := range slackMessages { - utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook: %v", msg)) - client := http.Client{Timeout: 15 * time.Second} - _, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(msg))) - if err != nil { - utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err)) + + if u.CanSend() { + utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook")) + client := http.Client{Timeout: 15 * time.Second} + _, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(msg))) + if err != nil { + utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err)) + } + u.Log(msg) } - slackMessages = uniqueMessages(slackMessages, msg) } + slackMessages = []string{} + messageLock.Unlock() time.Sleep(60 * time.Second) if u.Enabled { u.Run() @@ -86,29 +105,36 @@ func (u *Slack) Run() error { } // CUSTOM FUNCTION FO SENDING SLACK MESSAGES -func SendSlack(msg string) error { - //if slackUrl == "" { - // return errors.New("Slack Webhook URL has not been set in settings") - //} - fullMessage := fmt.Sprintf("{\"text\":\"%v\"}", msg) - slackMessages = append(slackMessages, fullMessage) +func SendSlack(temp string, data interface{}) error { + messageLock.Lock() + buf := new(bytes.Buffer) + slackTemp, _ := template.New("slack").Parse(temp) + slackTemp.Execute(buf, data) + slackMessages = append(slackMessages, buf.String()) + messageLock.Unlock() return nil } // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS -func (u *Slack) OnFailure() error { +func (u *Slack) OnFailure(s *types.Service) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) - // Do failing stuff here! + message := slackMessage{ + Service: s, + Time: time.Now().Unix(), + } + SendSlack(FAILING_TEMPLATE, message) } return nil } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS -func (u *Slack) OnSuccess() error { +func (u *Slack) OnSuccess(s *types.Service) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a successful notification.", u.Method)) - // Do checking or any successful things here + //message := slackMessage{ + // Service: s, + // Time: time.Now().Unix(), + //} + //SendSlack(SUCCESS_TEMPLATE, message) } return nil } @@ -124,7 +150,7 @@ func (u *Slack) OnSave() error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Slack) Install() error { - inDb, err := slacker.Notification.isInDatabase() + inDb, err := slacker.Notification.IsInDatabase() if !inDb { newNotifer, err := InsertDatabase(u.Notification) if err != nil { diff --git a/plugin/main.go b/plugin/main.go index 8ddfc49c..645f9bd0 100644 --- a/plugin/main.go +++ b/plugin/main.go @@ -1,7 +1,7 @@ package plugin import ( - "net/http" + "github.com/hunterlong/statup/types" "upper.io/db.v3/lib/sqlbuilder" ) @@ -21,48 +21,14 @@ var ( DB sqlbuilder.Database ) -func SetDatabase(database sqlbuilder.Database) { - DB = database +type PluginInfo struct { + i *types.Info } -type PluginInfo struct { - Info Info - PluginActions +func SetDatabase(database sqlbuilder.Database) { + DB = database } func (p *PluginInfo) Form() string { return "okkokokkok" } - -type PluginActions interface { - GetInfo() Info - GetForm() string - OnLoad(sqlbuilder.Database) - SetInfo(map[string]interface{}) Info - Routes() []Routing - OnSave(map[string]interface{}) - OnFailure(map[string]interface{}) - OnSuccess(map[string]interface{}) - OnSettingsSaved(map[string]interface{}) - OnNewUser(map[string]interface{}) - OnNewService(map[string]interface{}) - OnUpdatedService(map[string]interface{}) - OnDeletedService(map[string]interface{}) - OnInstall(map[string]interface{}) - OnUninstall(map[string]interface{}) - OnBeforeRequest(map[string]interface{}) - OnAfterRequest(map[string]interface{}) - OnShutdown() -} - -type Routing struct { - URL string - Method string - Handler func(http.ResponseWriter, *http.Request) -} - -type Info struct { - Name string - Description string - Form string -} diff --git a/servers/docker-compose.yml b/servers/docker-compose.yml index 97a5103b..79d65835 100644 --- a/servers/docker-compose.yml +++ b/servers/docker-compose.yml @@ -28,22 +28,22 @@ services: - internet - database depends_on: - - postgres + - postgres_statup volumes: - ./statup/app:/app environment: VIRTUAL_HOST: localhost VIRTUAL_PORT: 8080 DB_CONN: postgres - DB_HOST: postgres + DB_HOST: postgres_statup DB_USER: statup DB_PASS: password123 DB_DATABASE: statup NAME: EC2 Example DESCRIPTION: This is a Statup Docker Compose instance - postgres: - container_name: postgres + postgres_statup: + container_name: postgres_statup image: postgres restart: always networks: diff --git a/source/js/charts.js b/source/js/charts.js index 07ac03ff..1ca11b32 100644 --- a/source/js/charts.js +++ b/source/js/charts.js @@ -1,6 +1,6 @@ -{{ range . }}{{ if .AvgTime }}var ctx_{{.Id}}=document.getElementById("service_{{.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} +{{ range . }}{{$s := .ToService}}{{ if .AvgTime }}var ctx_{{$s.Id}}=document.getElementById("service_{{$s.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{$s.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50} if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70} - ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}}) + ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{$s.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}}) {{ end }} {{ end }} \ No newline at end of file diff --git a/source/js/main.js b/source/js/main.js index e87e2e29..9d1d0154 100644 --- a/source/js/main.js +++ b/source/js/main.js @@ -21,6 +21,11 @@ $(".confirm-btn").on('click', function() { }); +$(".select-input").on("click", function () { + $(this).select(); +}); + + var ranVar = false; var ranTheme = false; $('a[data-toggle="pill"]').on('shown.bs.tab', function (e) { diff --git a/source/tmpl/dashboard.html b/source/tmpl/dashboard.html index aff72068..25e270c1 100644 --- a/source/tmpl/dashboard.html +++ b/source/tmpl/dashboard.html @@ -48,36 +48,34 @@
{{ range .Services }} + {{ $s := .ToService }}
-
{{.Name}}
- {{if .Online}} ONLINE {{else}} OFFLINE {{end}} +
{{$s.Name}}
+ {{if $s.Online}} ONLINE {{else}} OFFLINE {{end}}

{{.SmallText}}

{{ end }}
- -

Latest Failures

- {{ range .Services }} - + {{ $s := .ToService }} {{ if .LimitedFailures }} -
+

{{$s.Name}} Failures

+
{{ range .LimitedFailures }} + {{ $f := .ToFailure }}
{{.ParseError}}
Reported {{.Ago}}
-

{{.Issue}}

+

{{$f.Issue}}

{{ end }}
{{ end }} - - {{ end }} diff --git a/source/tmpl/index.html b/source/tmpl/index.html index fb85c965..00b29d01 100644 --- a/source/tmpl/index.html +++ b/source/tmpl/index.html @@ -12,25 +12,27 @@ {{end}} - {{.Core.Name}} Status + {{.Name}} Status
-

{{.Core.Name}}

+

{{.Name}}

- {{ if .Core.Description }} -
{{ .Core.Description }}
+ {{ if .Description }} +
{{ .Description }}
{{ end }}
{{end}} {{ range .Services }} -
+ {{ $s := .ToService }} +
-

{{ .Name }} - {{if .Online}} +

{{ $s.Name }} + {{if $s.Online}} ONLINE {{ else }} OFFLINE @@ -84,13 +87,13 @@

{{ if .AvgTime }}
- +
{{ end }} -
+
{{.SmallText}} - View Service + View Service
@@ -115,9 +118,9 @@ {{end}} - {{ if .Core.Style }} + {{ if .Style }} {{ end }} diff --git a/source/tmpl/service.html b/source/tmpl/service.html index 9e086755..b2105971 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -1,4 +1,4 @@ - +{{ $s := .ToService }} @@ -13,7 +13,7 @@ {{end}} - Statup | {{.Name}} Service + Statup | {{$s.Name}} Service @@ -25,14 +25,14 @@
- {{if .Online }} + {{if $s.Online }} ONLINE {{ else }} OFFLINE {{end}} -

{{ .Name }} - {{if .Online }} +

{{ $s.Name }} + {{if $s.Online }} ONLINE {{ else }} OFFLINE @@ -59,14 +59,15 @@ {{ if .LimitedFailures }} -
+
{{ range .LimitedFailures }} + {{ $f := .ToFailure }}
{{.ParseError}}
Reported {{.Ago}}
-

{{.Issue}}

+

{{$f.Issue}}

{{ end }}
@@ -81,65 +82,65 @@

Edit Service

-
+
- +
- + +
- +
- + +
- +
- +
- +
- +
- +
@@ -147,7 +148,7 @@
@@ -156,13 +157,11 @@

Last Response

- - - +
- +
@@ -171,12 +170,12 @@

Service Checkins

-{{ range .Checkins }} +{{ range $s.Checkins }}
Check #{{.Id}} Checked in {{.Ago}}
{{ end }} -
+
diff --git a/source/tmpl/services.html b/source/tmpl/services.html index 9694107d..83c4f5f5 100644 --- a/source/tmpl/services.html +++ b/source/tmpl/services.html @@ -33,13 +33,14 @@ {{range .}} + {{ $s := .ToService }} - {{.Name}} - {{if .Online}}ONLINE{{else}}OFFLINE{{end}} + {{$s.Name}} + {{if $s.Online}}ONLINE{{else}}OFFLINE{{end}} @@ -47,7 +48,6 @@ -

Create Service

@@ -91,6 +91,7 @@
+ You can insert Regex to validate the response
diff --git a/source/tmpl/settings.html b/source/tmpl/settings.html index 26b2a705..7ab12473 100644 --- a/source/tmpl/settings.html +++ b/source/tmpl/settings.html @@ -20,7 +20,6 @@
- {{template "nav"}}
@@ -79,14 +78,19 @@ -
- - +
+ +
+ +
-
- - +
+ +
+ + You can Regenerate API Keys if you need to. +
@@ -122,7 +126,7 @@ {{ range .Communications }} -
+
{{range .Form}}
@@ -130,6 +134,12 @@
{{end}} + +
+ + +
+
@@ -143,6 +153,18 @@
+ + {{ if .Logs }} + Sent {{.SentLastHour}} out of {{.LimitValue}} in the last hour
+ {{ range .Logs }} +
+
+ {{.Message}} +

Sent {{.Time.Ago}}

+
+
+ {{ end }} + {{ end }}
{{ end }} diff --git a/types/interfaces.go b/types/interfaces.go new file mode 100644 index 00000000..d30bc752 --- /dev/null +++ b/types/interfaces.go @@ -0,0 +1,19 @@ +package types + +type ServiceInterface interface { + AvgTime() float64 + Online24() float32 + ToService() *Service + SmallText() string + GraphData() string + AvgUptime() string + LimitedFailures() []*Failure + TotalFailures() (uint64, error) + TotalFailures24Hours() (uint64, error) +} + +type FailureInterface interface { + ToFailure() *Failure + Ago() string + ParseError() string +} diff --git a/types/types.go b/types/types.go index d54bba6e..a767b682 100644 --- a/types/types.go +++ b/types/types.go @@ -1,9 +1,99 @@ package types import ( + "net/http" "time" + "upper.io/db.v3/lib/sqlbuilder" ) +type PluginInfo struct { + Info Info + PluginActions +} + +type Routing struct { + URL string + Method string + Handler func(http.ResponseWriter, *http.Request) +} + +type Info struct { + Name string + Description string + Form string +} + +type PluginActions interface { + GetInfo() Info + GetForm() string + OnLoad(sqlbuilder.Database) + SetInfo(map[string]interface{}) Info + Routes() []Routing + OnSave(map[string]interface{}) + OnFailure(map[string]interface{}) + OnSuccess(map[string]interface{}) + OnSettingsSaved(map[string]interface{}) + OnNewUser(map[string]interface{}) + OnNewService(map[string]interface{}) + OnUpdatedService(map[string]interface{}) + OnDeletedService(map[string]interface{}) + OnInstall(map[string]interface{}) + OnUninstall(map[string]interface{}) + OnBeforeRequest(map[string]interface{}) + OnAfterRequest(map[string]interface{}) + OnShutdown() +} + +type AllNotifiers interface{} + +type Core struct { + Name string `db:"name" json:"name"` + Description string `db:"description" json:"name"` + Config string `db:"config" json:"-"` + ApiKey string `db:"api_key" json:"-"` + ApiSecret string `db:"api_secret" json:"-"` + Style string `db:"style" json:"-"` + Footer string `db:"footer" json:"-"` + Domain string `db:"domain" json:"domain,omitempty"` + Version string `db:"version" json:"version,omitempty"` + MigrationId int64 `db:"migration_id" json:"-"` + UseCdn bool `db:"use_cdn" json:"-"` + Services []*Service `json:"services,omitempty"` + Plugins []Info + Repos []PluginJSON + AllPlugins []PluginActions + Communications []AllNotifiers + DbConnection string + Started time.Time +} + +type Service struct { + Id int64 `db:"id,omitempty" json:"id"` + Name string `db:"name" json:"name"` + Domain string `db:"domain" json:"domain"` + Expected string `db:"expected" json:"expected"` + ExpectedStatus int `db:"expected_status" json:"expected_status"` + Interval int `db:"check_interval" json:"check_interval"` + Type string `db:"check_type" json:"type"` + Method string `db:"method" json:"method"` + PostData string `db:"post_data" json:"post_data"` + Port int `db:"port" json:"port"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Online bool `json:"online"` + Latency float64 `json:"latency"` + Online24Hours float32 `json:"24_hours_online"` + AvgResponse string `json:"avg_response"` + TotalUptime string `json:"uptime"` + OrderId int64 `json:"order_id"` + Failures []*Failure `json:"failures"` + Checkins []*Checkin `json:"checkins"` + StopRoutine chan struct{} + LastResponse string + LastStatusCode int + LastOnline time.Time + DnsLookup float64 `json:"dns_lookup_time"` +} + type User struct { Id int64 `db:"id,omitempty" json:"id"` Username string `db:"username" json:"username"` @@ -40,16 +130,6 @@ type Checkin struct { Last time.Time `json:"last"` } -type Email struct { - To string - Subject string - Template string - From string - Data interface{} - Source string - Sent bool -} - type Config struct { Connection string `yaml:"connection"` Host string `yaml:"host"` diff --git a/utils/utils.go b/utils/utils.go index 8a638b15..1f457785 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,10 +1,12 @@ package utils import ( + "github.com/ararog/timeago" "os" "regexp" "strconv" "strings" + "time" ) func StringInt(s string) int64 { @@ -12,6 +14,21 @@ func StringInt(s string) int64 { return int64(num) } +func IntString(s int) string { + return strconv.Itoa(s) +} + +type Timestamp time.Time + +type Timestamper interface { + Ago() string +} + +func (t Timestamp) Ago() string { + got, _ := timeago.TimeAgoWithTime(time.Now(), time.Time(t)) + return got +} + func UnderScoreString(str string) string { // convert every letter to lower case