From 5ee6fe62ab5afe49bad69ef6803d7606504ca872 Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Fri, 13 Jul 2018 02:43:15 -0700 Subject: [PATCH 1/5] notifier temp --- core/events.go | 4 ++-- notifiers/email.go | 4 ++-- notifiers/notifiers.go | 12 ++++++------ notifiers/slack.go | 32 ++++++++++++++++++++++---------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/core/events.go b/core/events.go index df27fcc0..43c909af 100644 --- a/core/events.go +++ b/core/events.go @@ -17,14 +17,14 @@ func OnSuccess(s *Service) { for _, p := range CoreApp.AllPlugins { p.OnSuccess(structs.Map(s)) } - notifiers.OnSuccess() + notifiers.OnSuccess(structs.Map(s)) } func OnFailure(s *Service, f FailureData) { for _, p := range CoreApp.AllPlugins { p.OnFailure(structs.Map(s)) } - notifiers.OnFailure() + notifiers.OnFailure(structs.Map(s)) } func OnSettingsSaved(c *Core) { diff --git a/notifiers/email.go b/notifiers/email.go index ab9631c0..dfd5b062 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -144,7 +144,7 @@ func (u *Email) Run() error { } // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS -func (u *Email) OnFailure() error { +func (u *Email) OnFailure(data map[string]interface{}) error { if u.Enabled { utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) // Do failing stuff here! @@ -153,7 +153,7 @@ func (u *Email) OnFailure() error { } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS -func (u *Email) OnSuccess() error { +func (u *Email) OnSuccess(data map[string]interface{}) error { if u.Enabled { utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) // Do failing stuff here! diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index 07401e2f..59294736 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -54,8 +54,8 @@ type Notifier interface { Init() error Install() error Run() error - OnFailure() error - OnSuccess() error + OnFailure(map[string]interface{}) error + OnSuccess(map[string]interface{}) error Select() *Notification Test() error } @@ -148,17 +148,17 @@ func (n *Notification) GetValue(dbField string) string { return "" } -func OnFailure() { +func OnFailure(data map[string]interface{}) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnFailure() + n.OnFailure(data) } } -func OnSuccess() { +func OnSuccess(data map[string]interface{}) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnSuccess() + n.OnSuccess(data) } } diff --git a/notifiers/slack.go b/notifiers/slack.go index b7994f07..dec7606a 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -6,11 +6,14 @@ import ( "github.com/hunterlong/statup/utils" "net/http" "time" + "text/template" ) const ( SLACK_ID = 2 SLACK_METHOD = "slack" + SERVICE_TEMPLATE = `{ "attachments": [ { "fallback": "ReferenceError - UI is not defined: https://honeybadger.io/path/to/event/", "text": " - Your Statup service 'Google' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Response", "value": "production", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": 123456789 } ] }` + TEST_TEMPLATE = `{"text":"%{{.Message}}"}` ) var ( @@ -63,7 +66,7 @@ func (u *Slack) Init() error { } func (u *Slack) Test() error { - SendSlack("Slack notifications on your Statup server is working!") + SendSlack(TEST_TEMPLATE, nil) return nil } @@ -86,17 +89,16 @@ 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 { + buf := new(bytes.Buffer) + slackTemp, _ := template.New("slack").Parse(temp) + slackTemp.Execute(buf, data) + slackMessages = append(slackMessages, buf.String()) return nil } // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS -func (u *Slack) OnFailure() error { +func (u *Slack) OnFailure(data map[string]interface{}) error { if u.Enabled { utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) // Do failing stuff here! @@ -105,9 +107,19 @@ func (u *Slack) OnFailure() error { } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS -func (u *Slack) OnSuccess() error { +func (u *Slack) OnSuccess(data map[string]interface{}) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a successful notification.", u.Method)) + utils.Log(1, fmt.Sprintf("Notification %v is receiving a successful notification. %v", u.Method, data)) + + //domain := data["Domain"] + //expected := data["Expected"] + //expectedStatus := data["ExpectedStatus"] + failures := data["Failures"] + response := data["LastResponse"] + + fullMessage := fmt.Sprintf(`{ "attachments": [ { "fallback": "Service is currently offline", "text": "Service is currently offline", "fields": [ { "title": "Issue", "value": "%v", "short": true }, { "title": "Response", "value": "%v", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": %v } ] }`, failures, response, time.Now().Unix()) + slackMessages = append(slackMessages, fullMessage) + // Do checking or any successful things here } return nil From 4ce7ca7530a79c37c33899b15e5113876d31c2a0 Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Fri, 13 Jul 2018 19:37:39 -0700 Subject: [PATCH 2/5] notifiers - split interfaces/packages --- cli.go | 42 +++++++------ core/checker.go | 43 ++++++------- core/checkin.go | 69 +++++++++++---------- core/configs.go | 6 +- core/core.go | 3 +- core/events.go | 17 ++--- core/failures.go | 42 +++++++++---- core/hits.go | 14 +++-- core/services.go | 123 ++++++++++++++++++------------------- core/setup.go | 17 ++--- core/users.go | 20 +++--- handlers/api.go | 10 +-- handlers/index.go | 6 +- handlers/prometheus.go | 3 +- handlers/services.go | 36 ++++++----- handlers/setup.go | 4 +- handlers/users.go | 8 +-- main_test.go | 39 ++++++------ notifiers/email.go | 4 +- notifiers/notifiers.go | 36 +++++++---- notifiers/slack.go | 61 ++++++++++-------- servers/docker-compose.yml | 8 +-- source/js/charts.js | 4 +- source/tmpl/dashboard.html | 7 ++- source/tmpl/index.html | 33 +++++----- source/tmpl/service.html | 48 +++++++-------- source/tmpl/services.html | 10 +-- types/interfaces.go | 19 ++++++ types/types.go | 27 ++++++++ 29 files changed, 432 insertions(+), 327 deletions(-) create mode 100644 types/interfaces.go diff --git a/cli.go b/cli.go index cef480c8..c3fe9de6 100644 --- a/cli.go +++ b/cli.go @@ -5,6 +5,7 @@ import ( "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" @@ -105,8 +106,9 @@ func RunOnce() { if err != nil { utils.Log(4, err) } - for _, s := range core.CoreApp.Services { - out := s.Check() + for _, ser := range core.CoreApp.Services { + s := ser.ToService() + out := core.ServiceCheck(s) fmt.Printf(" Service %v | URL: %v | Latency: %0.0fms | Online: %v\n", out.Name, out.Domain, (out.Latency * 1000), out.Online) } } @@ -148,33 +150,33 @@ 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) 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) } @@ -212,21 +214,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 +236,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/checker.go b/core/checker.go index 976516e7..e21b52b6 100644 --- a/core/checker.go +++ b/core/checker.go @@ -18,21 +18,22 @@ 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() + obj := s //go obj.StartCheckins() - obj.stopRoutine = make(chan struct{}) - go obj.CheckQueue() + obj.StopRoutine = make(chan struct{}) + go CheckQueue(obj) } } -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 +44,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 +59,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 +78,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 +101,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 +122,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..9dabb240 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -14,8 +14,9 @@ func (c *Checkin) String() string { return c.Api } -func FindCheckin(api string) *Checkin { - for _, s := range CoreApp.Services { +func FindCheckin(api string) *types.Checkin { + for _, ser := range CoreApp.Services { + s := ser.ToService() for _, c := range s.Checkins { if c.Api == api { return c @@ -25,8 +26,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 +79,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..7908fdb6 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) @@ -102,13 +102,13 @@ func LoadUsingEnv() (*types.Config, error) { 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..1154147a 100644 --- a/core/core.go +++ b/core/core.go @@ -105,7 +105,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 } diff --git a/core/events.go b/core/events.go index 43c909af..0fc2d202 100644 --- a/core/events.go +++ b/core/events.go @@ -4,6 +4,7 @@ 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,18 +14,18 @@ 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(structs.Map(s)) + 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(structs.Map(s)) + notifiers.OnFailure(s) } func OnSettingsSaved(c *Core) { @@ -33,25 +34,25 @@ func OnSettingsSaved(c *Core) { } } -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)) } diff --git a/core/failures.go b/core/failures.go index bb3fdb48..9f939f5d 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,29 @@ func (u *Service) DeleteFailures() { } } -func (s *Service) LimitedFailures() []*Failure { - var fails []*Failure +func (ser *Service) LimitedFailures() []*types.Failure { + s := ser.ToService() + var fails []*types.Failure col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id").Limit(10) col.All(&fails) return fails } -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 +83,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..e08c01ec 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 interface{} +} + +type Failure struct { + F interface{} } func serviceCol() db.Collection { @@ -44,28 +23,32 @@ func serviceCol() db.Collection { } func SelectService(id int64) *Service { - for _, s := range CoreApp.Services { + for _, ser := range CoreApp.Services { + s := ser.ToService() if s.Id == id { - return s + return 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.S.(*types.Service) + total, _ := ser.TotalHits() + failed, _ := ser.TotalFailures24Hours() if failed == 0 { s.Online24Hours = 100.00 return s.Online24Hours @@ -106,12 +90,23 @@ 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.(*types.Service) +} + +func NewService(s *types.Service) *Service { + ser := &Service{s} + return ser +} + +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]) + return fmt.Sprintf("%v on %v", lastFailure.ParseError(), last[0].CreatedAt.Format("Monday 3:04PM, Jan _2 2006")) } else { return fmt.Sprintf("%v is currently offline", s.Name) } @@ -138,7 +133,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 +168,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,18 +192,19 @@ 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 { + for _, ser := range CoreApp.Services { + s := ser.ToService() if s.Id != u.Id { - srvcs = append(srvcs, s) + srvcs = append(srvcs, ser) } } CoreApp.Services = srvcs 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 +212,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 +240,17 @@ 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{}) + nn := &Service{u} + CoreApp.Services = append(CoreApp.Services, nn) return uuid.(int64), err } func CountOnline() int { amount := 0 - for _, v := range CoreApp.Services { - if v.Online { + for _, ser := range CoreApp.Services { + s := ser.ToService() + if s.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..1fbbc9a3 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" ) @@ -23,7 +24,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 +45,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/services.go b/handlers/services.go index 23e8b0b8..e4107f4f 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,22 @@ 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"])) + 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`) @@ -103,7 +105,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 +117,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 +129,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 +139,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 +153,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/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_test.go b/main_test.go index 40256ddc..519b6949 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) { @@ -267,22 +268,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 +292,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 +300,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 +313,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 +328,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 +341,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 +372,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 +381,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 +389,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 +406,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 +424,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 dfd5b062..35e5fbd8 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -144,7 +144,7 @@ func (u *Email) Run() error { } // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS -func (u *Email) OnFailure(data map[string]interface{}) 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! @@ -153,7 +153,7 @@ func (u *Email) OnFailure(data map[string]interface{}) error { } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS -func (u *Email) OnSuccess(data map[string]interface{}) 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! diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index 59294736..dfdaa242 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" @@ -54,8 +55,8 @@ type Notifier interface { Init() error Install() error Run() error - OnFailure(map[string]interface{}) error - OnSuccess(map[string]interface{}) error + OnFailure(*types.Service) error + OnSuccess(*types.Service) error Select() *Notification Test() error } @@ -81,7 +82,6 @@ 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 } @@ -148,26 +148,36 @@ func (n *Notification) GetValue(dbField string) string { return "" } -func OnFailure(data map[string]interface{}) { +func OnFailure(s *types.Service) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnFailure(data) + n.OnFailure(s) } } -func OnSuccess(data map[string]interface{}) { +func OnSuccess(s *types.Service) { for _, comm := range AllCommunications { n := comm.(Notifier) - n.OnSuccess(data) + 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][:10] == elements[i][:10] { + 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/slack.go b/notifiers/slack.go index dec7606a..36328ca9 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -3,28 +3,37 @@ package notifiers import ( "bytes" "fmt" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" - "time" + "sync" "text/template" + "time" ) const ( - SLACK_ID = 2 - SLACK_METHOD = "slack" - SERVICE_TEMPLATE = `{ "attachments": [ { "fallback": "ReferenceError - UI is not defined: https://honeybadger.io/path/to/event/", "text": " - Your Statup service 'Google' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Response", "value": "production", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": 123456789 } ] }` - TEST_TEMPLATE = `{"text":"%{{.Message}}"}` + 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", "ts": {{.Time}} } ] }` + 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", "ts": {{.Time}} } ] }` + 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{ @@ -40,6 +49,7 @@ func init() { }}}, } add(slacker) + messageLock = new(sync.Mutex) } // Select Obj @@ -49,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 @@ -66,21 +74,26 @@ func (u *Slack) Init() error { } func (u *Slack) Test() error { - SendSlack(TEST_TEMPLATE, nil) + 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)) + 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)) } - slackMessages = uniqueMessages(slackMessages, msg) + + fmt.Println(msg) } + messageLock.Unlock() time.Sleep(60 * time.Second) if u.Enabled { u.Run() @@ -89,37 +102,35 @@ func (u *Slack) Run() error { } // CUSTOM FUNCTION FO SENDING SLACK MESSAGES -func SendSlack(temp string, data ...interface{}) error { +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(data map[string]interface{}) 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(data map[string]interface{}) error { +func (u *Slack) OnSuccess(s *types.Service) error { if u.Enabled { - utils.Log(1, fmt.Sprintf("Notification %v is receiving a successful notification. %v", u.Method, data)) - - //domain := data["Domain"] - //expected := data["Expected"] - //expectedStatus := data["ExpectedStatus"] - failures := data["Failures"] - response := data["LastResponse"] - - fullMessage := fmt.Sprintf(`{ "attachments": [ { "fallback": "Service is currently offline", "text": "Service is currently offline", "fields": [ { "title": "Issue", "value": "%v", "short": true }, { "title": "Response", "value": "%v", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": %v } ] }`, failures, response, time.Now().Unix()) - slackMessages = append(slackMessages, fullMessage) - + SendSlack(SUCCESS_TEMPLATE, s) // Do checking or any successful things here } return nil 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/tmpl/dashboard.html b/source/tmpl/dashboard.html index aff72068..aa301a95 100644 --- a/source/tmpl/dashboard.html +++ b/source/tmpl/dashboard.html @@ -48,10 +48,11 @@
{{ range .Services }} + {{ $s := .ToService }}
-
{{.Name}}
- {{if .Online}} ONLINE {{else}} OFFLINE {{end}} +
{{$s.Name}}
+ {{if $s.Online}} ONLINE {{else}} OFFLINE {{end}}

{{.SmallText}}

@@ -62,7 +63,7 @@

Latest Failures

{{ range .Services }} - + {{ $s := .ToService }} {{ if .LimitedFailures }}
{{ range .LimitedFailures }} 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..47d63c71 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 @@ -81,65 +81,65 @@

Edit Service

-
+
- +
- + +
- +
- + +
- +
- +
- +
- +
- +
@@ -147,7 +147,7 @@
@@ -157,12 +157,12 @@

Last Response

- +
- +
@@ -171,12 +171,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..c1d93ae7 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

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..5c38ea9f 100644 --- a/types/types.go +++ b/types/types.go @@ -4,6 +4,33 @@ import ( "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"` From 949ba3958aa427d2145d10cdcc5acb83ab3d1747 Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Mon, 16 Jul 2018 00:02:33 -0700 Subject: [PATCH 3/5] notifiers - split interfaces/packages --- .gitignore | 2 +- .travis.yml | 2 +- core/assets.go | 4 +- core/core.go | 1 - main.go | 1 - notifiers/email.go | 58 ++++++++++++++++------- {source => notifiers}/emails/failure.html | 6 +-- {source => notifiers}/emails/message.html | 0 notifiers/notifiers.go | 2 +- notifiers/rice-box.go | 48 +++++++++++++++++++ notifiers/slack.go | 17 ++++--- 11 files changed, 103 insertions(+), 38 deletions(-) rename {source => notifiers}/emails/failure.html (84%) rename {source => notifiers}/emails/message.html (100%) create mode 100644 notifiers/rice-box.go 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/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/core.go b/core/core.go index 1154147a..7c75c969 100644 --- a/core/core.go +++ b/core/core.go @@ -42,7 +42,6 @@ var ( ScssBox *rice.Box JsBox *rice.Box TmplBox *rice.Box - EmailBox *rice.Box SetupMode bool UsingAssets bool VERSION string diff --git a/main.go b/main.go index cdc76b76..99ba6d33 100644 --- a/main.go +++ b/main.go @@ -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() { diff --git a/notifiers/email.go b/notifiers/email.go index 35e5fbd8..e9f14bb9 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -21,11 +21,12 @@ var ( emailer *Email emailArray []string emailQueue []*types.Email + emailBox *rice.Box + mailer *gomail.Dialer ) type Email struct { *Notification - mailer *gomail.Dialer } // DEFINE YOUR NOTIFICATION HERE. @@ -84,7 +85,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 +97,33 @@ 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 := &types.Email{ + 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 +} + // AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS func (u *Email) Run() error { var sentAddresses []string @@ -146,8 +153,20 @@ func (u *Email) Run() error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS 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 := &types.Email{ + 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 } @@ -155,8 +174,7 @@ func (u *Email) OnFailure(s *types.Service) error { // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS 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,6 +190,9 @@ func (u *Email) OnSave() error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Email) Install() error { + + fmt.Println("installing emailer") + inDb, err := emailer.Notification.isInDatabase() if !inDb { newNotifer, err := InsertDatabase(u.Notification) @@ -185,12 +206,13 @@ func (u *Email) Install() error { } func (u *Email) dialSend(email *types.Email) 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 } 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 dfdaa242..a54f1786 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -169,7 +169,7 @@ func uniqueStrings(elements []string) []string { // Scan slice for a previous element of the same value. exists := false for v := 0; v < i; v++ { - if elements[v][:10] == elements[i][:10] { + if elements[v] == elements[i] { exists = true break } 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 36328ca9..292e4b64 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -14,8 +14,8 @@ import ( const ( 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", "ts": {{.Time}} } ] }` - 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", "ts": {{.Time}} } ] }` + 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":"{{.}}"}` ) @@ -90,9 +90,8 @@ func (u *Slack) Run() error { if err != nil { utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err)) } - - fmt.Println(msg) } + slackMessages = []string{} messageLock.Unlock() time.Sleep(60 * time.Second) if u.Enabled { @@ -115,13 +114,10 @@ func SendSlack(temp string, data interface{}) error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Slack) OnFailure(s *types.Service) error { if u.Enabled { - // Do failing stuff here! - message := slackMessage{ Service: s, Time: time.Now().Unix(), } - SendSlack(FAILING_TEMPLATE, message) } return nil @@ -130,8 +126,11 @@ func (u *Slack) OnFailure(s *types.Service) error { // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS func (u *Slack) OnSuccess(s *types.Service) error { if u.Enabled { - SendSlack(SUCCESS_TEMPLATE, s) - // Do checking or any successful things here + //message := slackMessage{ + // Service: s, + // Time: time.Now().Unix(), + //} + //SendSlack(SUCCESS_TEMPLATE, message) } return nil } From d0ad55488ddbaf0e25a23cc4002fb90391f5375a Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Tue, 17 Jul 2018 02:18:20 -0700 Subject: [PATCH 4/5] notifier - moved core --- cli.go | 14 ++--- core/checker.go | 5 +- core/checkin.go | 3 +- core/configs.go | 6 +- core/core.go | 43 +++++--------- core/database.go | 6 +- core/events.go | 7 +-- core/failures.go | 8 ++- core/services.go | 38 ++++++------ handlers/api.go | 15 +++++ handlers/routes.go | 3 +- handlers/services.go | 5 +- handlers/settings.go | 22 ++----- main.go | 6 +- notifiers/email.go | 53 +++++++++-------- notifiers/notifiers.go | 118 ++++++++++++++++++++++++++----------- notifiers/slack.go | 18 +++--- plugin/main.go | 44 ++------------ source/js/main.js | 5 ++ source/tmpl/dashboard.html | 11 ++-- source/tmpl/service.html | 9 ++- source/tmpl/services.html | 3 +- source/tmpl/settings.html | 38 +++++++++--- types/types.go | 73 +++++++++++++++++++---- utils/utils.go | 17 ++++++ 25 files changed, 336 insertions(+), 234 deletions(-) diff --git a/cli.go b/cli.go index c3fe9de6..38918577 100644 --- a/cli.go +++ b/cli.go @@ -4,7 +4,6 @@ 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" @@ -106,9 +105,8 @@ func RunOnce() { if err != nil { utils.Log(4, err) } - for _, ser := range core.CoreApp.Services { - s := ser.ToService() - out := core.ServiceCheck(s) + for _, s := range core.CoreApp.Services { + 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) } } @@ -129,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() @@ -160,7 +158,7 @@ func TestPlugin(plug plugin.PluginActions) { 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).ToService()) @@ -180,11 +178,11 @@ func TestPlugin(plug plugin.PluginActions) { 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) diff --git a/core/checker.go b/core/checker.go index e21b52b6..68ad41c6 100644 --- a/core/checker.go +++ b/core/checker.go @@ -20,10 +20,9 @@ func CheckServices() { utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services))) for _, ser := range CoreApp.Services { s := ser.ToService() - obj := s //go obj.StartCheckins() - obj.StopRoutine = make(chan struct{}) - go CheckQueue(obj) + s.StopRoutine = make(chan struct{}) + go CheckQueue(s) } } diff --git a/core/checkin.go b/core/checkin.go index 9dabb240..8b0721ec 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -16,8 +16,7 @@ func (c *Checkin) String() string { func FindCheckin(api string) *types.Checkin { for _, ser := range CoreApp.Services { - s := ser.ToService() - for _, c := range s.Checkins { + for _, c := range ser.ToService().Checkins { if c.Api == api { return c } diff --git a/core/configs.go b/core/configs.go index 7908fdb6..e80d81de 100644 --- a/core/configs.go +++ b/core/configs.go @@ -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,11 +93,11 @@ 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) } diff --git a/core/core.go b/core/core.go index 7c75c969..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 ( @@ -53,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") @@ -72,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 } @@ -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 0fc2d202..2cfe3216 100644 --- a/core/events.go +++ b/core/events.go @@ -3,7 +3,6 @@ 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" ) @@ -28,7 +27,7 @@ func OnFailure(s *types.Service, f FailureData) { notifiers.OnFailure(s) } -func OnSettingsSaved(c *Core) { +func OnSettingsSaved(c *types.Core) { for _, p := range CoreApp.AllPlugins { p.OnSettingsSaved(structs.Map(c)) } @@ -58,11 +57,11 @@ func OnUpdateService(s *types.Service) { } } -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 9f939f5d..b25cbbe2 100644 --- a/core/failures.go +++ b/core/failures.go @@ -46,12 +46,16 @@ func DeleteFailures(u *types.Service) { } } -func (ser *Service) LimitedFailures() []*types.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 []*types.Failure) []*types.Failure { diff --git a/core/services.go b/core/services.go index e08c01ec..292b6449 100644 --- a/core/services.go +++ b/core/services.go @@ -11,7 +11,7 @@ import ( ) type Service struct { - S interface{} + s *types.Service } type Failure struct { @@ -23,10 +23,10 @@ func serviceCol() db.Collection { } func SelectService(id int64) *Service { - for _, ser := range CoreApp.Services { - s := ser.ToService() - if s.Id == id { - return ser + for _, s := range CoreApp.Services { + ser := s.ToService() + if ser.Id == id { + return &Service{ser} } } return nil @@ -64,7 +64,7 @@ func (s *Service) AvgTime() float64 { } func (ser *Service) Online24() float32 { - s := ser.S.(*types.Service) + s := ser.ToService() total, _ := ser.TotalHits() failed, _ := ser.TotalFailures24Hours() if failed == 0 { @@ -91,12 +91,11 @@ type DateScan struct { } func (s *Service) ToService() *types.Service { - return s.S.(*types.Service) + return s.s } func NewService(s *types.Service) *Service { - ser := &Service{s} - return ser + return &Service{s} } func (ser *Service) SmallText() string { @@ -105,8 +104,8 @@ func (ser *Service) SmallText() string { hits, _ := ser.LimitedHits() if !s.Online { if len(last) > 0 { - lastFailure := MakeFailure(last[0]) - return fmt.Sprintf("%v on %v", lastFailure.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) } @@ -194,10 +193,10 @@ func (ser *Service) AvgUptime() string { func RemoveArray(u *types.Service) []*Service { var srvcs []*Service - for _, ser := range CoreApp.Services { - s := ser.ToService() - if s.Id != u.Id { - srvcs = append(srvcs, ser) + for _, s := range CoreApp.Services { + ser := s.ToService() + if ser.Id != u.Id { + srvcs = append(srvcs, s) } } CoreApp.Services = srvcs @@ -241,16 +240,15 @@ func CreateService(u *types.Service) (int64, error) { } u.Id = uuid.(int64) u.StopRoutine = make(chan struct{}) - nn := &Service{u} - CoreApp.Services = append(CoreApp.Services, nn) + CoreApp.Services = append(CoreApp.Services, &Service{u}) return uuid.(int64), err } func CountOnline() int { amount := 0 - for _, ser := range CoreApp.Services { - s := ser.ToService() - if s.Online { + for _, s := range CoreApp.Services { + ser := s.ToService() + if ser.Online { amount++ } } diff --git a/handlers/api.go b/handlers/api.go index 1fbbc9a3..bedc8eb4 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -17,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) 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 e4107f4f..0e7bea86 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -78,6 +78,9 @@ func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) { func ServicesViewHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) serv := core.SelectService(utils.StringInt(vars["id"])) + + fmt.Println(serv.ToService()) + ExecuteResponse(w, r, "service.html", serv) } @@ -94,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) { 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/main.go b/main.go index 99ba6d33..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" @@ -123,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/notifiers/email.go b/notifiers/email.go index e9f14bb9..ad9715e9 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -20,7 +20,7 @@ const ( var ( emailer *Email emailArray []string - emailQueue []*types.Email + emailQueue []*EmailOutgoing emailBox *rice.Box mailer *gomail.Dialer ) @@ -37,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", }}, }} @@ -108,7 +102,7 @@ func (u *Email) Init() error { func (u *Email) Test() error { if u.Enabled { - email := &types.Email{ + email := &EmailOutgoing{ To: "info@socialeck.com", Subject: "Test Email", Template: "message.html", @@ -124,6 +118,16 @@ 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 @@ -132,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 { @@ -158,7 +164,7 @@ func (u *Email) OnFailure(s *types.Service) error { Service: s, } - email := &types.Email{ + email := &EmailOutgoing{ To: "info@socialeck.com", Subject: fmt.Sprintf("Service %v is Failing", s.Name), Template: "failure.html", @@ -190,10 +196,7 @@ func (u *Email) OnSave() error { // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Email) Install() error { - - fmt.Println("installing emailer") - - inDb, err := emailer.Notification.isInDatabase() + inDb, err := emailer.Notification.IsInDatabase() if !inDb { newNotifer, err := InsertDatabase(u.Notification) if err != nil { @@ -205,7 +208,7 @@ 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) @@ -219,7 +222,7 @@ func (u *Email) dialSend(email *types.Email) error { 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) @@ -243,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/notifiers/notifiers.go b/notifiers/notifiers.go index a54f1786..f1b2c8e4 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -10,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"` @@ -44,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 @@ -62,14 +45,62 @@ type Notifier interface { } 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() } @@ -87,6 +118,7 @@ func (n *Notification) Update() (*Notification, error) { 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,6 +188,8 @@ 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 "" } diff --git a/notifiers/slack.go b/notifiers/slack.go index 292e4b64..f7df1b9d 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -41,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.", @@ -84,11 +84,15 @@ func (u *Slack) Run() error { messageLock.Lock() slackMessages = uniqueStrings(slackMessages) for _, msg := range slackMessages { - 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)) + + 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 = []string{} @@ -146,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/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 aa301a95..25e270c1 100644 --- a/source/tmpl/dashboard.html +++ b/source/tmpl/dashboard.html @@ -59,26 +59,23 @@ {{ 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/service.html b/source/tmpl/service.html index 47d63c71..b2105971 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -59,14 +59,15 @@ {{ if .LimitedFailures }} -
+
{{ range .LimitedFailures }} + {{ $f := .ToFailure }}
{{.ParseError}}
Reported {{.Ago}}
-

{{.Issue}}

+

{{$f.Issue}}

{{ end }}
@@ -147,7 +148,7 @@
@@ -156,9 +157,7 @@

Last Response

- -
diff --git a/source/tmpl/services.html b/source/tmpl/services.html index c1d93ae7..83c4f5f5 100644 --- a/source/tmpl/services.html +++ b/source/tmpl/services.html @@ -40,7 +40,7 @@ @@ -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/types.go b/types/types.go index 5c38ea9f..a767b682 100644 --- a/types/types.go +++ b/types/types.go @@ -1,9 +1,72 @@ 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"` @@ -67,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 From 17668c0586c5088201093234855491cea4d92249 Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Tue, 17 Jul 2018 09:09:48 -0700 Subject: [PATCH 5/5] notifiers - split interfaces/packages testing --- main_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/main_test.go b/main_test.go index 519b6949..7850b93b 100644 --- a/main_test.go +++ b/main_test.go @@ -188,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) {