From 8a08a9d14dae3dc1e380dfdd379cc8190387f44f Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Sun, 7 Oct 2018 15:38:56 -0700 Subject: [PATCH] checkins - comments --- Makefile | 4 +- core/checkin.go | 118 ++++++++++++++++++++++++++-------- core/database.go | 12 ++-- core/failures.go | 7 +- core/hits.go | 2 +- core/sample.go | 6 +- core/services.go | 22 ++++++- core/services_checkin_test.go | 2 +- handlers/handlers.go | 3 + handlers/routes.go | 5 +- handlers/services.go | 39 +++++++---- source/tmpl/form_checkin.html | 18 ++++-- source/tmpl/form_service.html | 4 +- source/tmpl/service.html | 43 +++++++++---- source/tmpl/settings.html | 1 - types/checkin.go | 29 ++++++++- types/failure.go | 1 + utils/encryption.go | 2 + 18 files changed, 235 insertions(+), 83 deletions(-) diff --git a/Makefile b/Makefile index 6f8f7707..ea2bdae2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.73 +VERSION=0.74 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go @@ -37,7 +37,7 @@ build-all: build-mac build-linux build-windows build-alpine compress docker-build-all: docker-build-base docker-build-dev docker-build-latest # push all docker tags built -docker-publish-all: docker-push-base docker-push-dev +docker-publish-all: docker-push-base docker-push-dev docker-push-latest # build Statup for local arch build: compile diff --git a/core/checkin.go b/core/checkin.go index eb801f73..67980a24 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -23,7 +23,7 @@ import ( "time" ) -type checkin struct { +type Checkin struct { *types.Checkin } @@ -31,14 +31,40 @@ type checkinHit struct { *types.CheckinHit } -// String will return a checkin API string -func (c *checkin) String() string { +// Routine for checking if the last Checkin was within its interval +func (c *Checkin) Routine() { + if c.Last() == nil { + return + } + reCheck := c.Period() +CheckinLoop: + for { + select { + case <-c.Running: + utils.Log(1, fmt.Sprintf("Stopping checkin routing: %v", c.Name)) + break CheckinLoop + case <-time.After(reCheck): + utils.Log(1, fmt.Sprintf("checking %v is expected at %v, checking every %v", c.Name, c.Expected(), c.Period())) + if c.Expected().Seconds() <= 0 { + issue := fmt.Sprintf("checkin is failing, no request since %v", c.Last().CreatedAt) + utils.Log(3, issue) + c.Service() + c.CreateFailure() + } + reCheck = c.Period() + } + continue + } +} + +// String will return a Checkin API string +func (c *Checkin) String() string { return c.ApiKey } -// ReturnCheckin converts *types.Checking to *core.checkin -func ReturnCheckin(c *types.Checkin) *checkin { - return &checkin{Checkin: c} +// ReturnCheckin converts *types.Checking to *core.Checkin +func ReturnCheckin(c *types.Checkin) *Checkin { + return &Checkin{Checkin: c} } // ReturnCheckinHit converts *types.checkinHit to *core.checkinHit @@ -46,27 +72,52 @@ func ReturnCheckinHit(c *types.CheckinHit) *checkinHit { return &checkinHit{CheckinHit: c} } -// SelectCheckin will find a checkin based on the API supplied -func SelectCheckin(api string) *checkin { - var checkin checkin +func (c *Checkin) Service() *Service { + service := SelectService(c.ServiceId) + return service +} + +func (c *Checkin) CreateFailure() (int64, error) { + service := c.Service() + fail := &types.Failure{ + Issue: fmt.Sprintf("Checkin %v was not reported %v ago, it expects a request every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())), + Method: "checkin", + MethodId: c.Id, + Service: service.Id, + PingTime: c.Expected().Seconds() * 0.001, + } + row := failuresDB().Create(&fail) + return fail.Id, row.Error +} + +// SelectCheckin will find a Checkin based on the API supplied +func SelectCheckin(api string) *Checkin { + var checkin Checkin checkinDB().Where("api_key = ?", api).First(&checkin) return &checkin } -// Period will return the duration of the checkin interval -func (c *checkin) Period() time.Duration { +// SelectCheckin will find a Checkin based on the API supplied +func SelectCheckinId(id int64) *Checkin { + var checkin Checkin + checkinDB().Where("id = ?", id).First(&checkin) + return &checkin +} + +// Period will return the duration of the Checkin interval +func (c *Checkin) Period() time.Duration { duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval)) return duration } -// Grace will return the duration of the checkin Grace Period (after service hasn't responded, wait a bit for a response) -func (c *checkin) Grace() time.Duration { +// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response) +func (c *Checkin) Grace() time.Duration { duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod)) return duration } -// Expected returns the duration of when the serviec should receive a checkin -func (c *checkin) Expected() time.Duration { +// Expected returns the duration of when the serviec should receive a Checkin +func (c *Checkin) Expected() time.Duration { last := c.Last().CreatedAt now := time.Now() lastDir := now.Sub(last) @@ -74,24 +125,37 @@ func (c *checkin) Expected() time.Duration { return sub } -// Last returns the last checkinHit for a checkin -func (c *checkin) Last() checkinHit { +// Last returns the last checkinHit for a Checkin +func (c *Checkin) Last() *checkinHit { var hit checkinHit checkinHitsDB().Where("checkin = ?", c.Id).Last(&hit) - return hit + return &hit } -// Hits returns all of the CheckinHits for a given checkin -func (c *checkin) Hits() []checkinHit { - var checkins []checkinHit +func (c *Checkin) Link() string { + return fmt.Sprintf("%v/checkin/%v", CoreApp.Domain, c.ApiKey) +} + +// Hits returns all of the CheckinHits for a given Checkin +func (c *Checkin) Hits() []*checkinHit { + var checkins []*checkinHit checkinHitsDB().Where("checkin = ?", c.Id).Order("id DESC").Find(&checkins) return checkins } -// Create will create a new checkin -func (c *checkin) Create() (int64, error) { +// Create will create a new Checkin +func (c *Checkin) Delete() error { + c.Close() + row := checkinDB().Delete(&c) + return row.Error +} + +// Create will create a new Checkin +func (c *Checkin) Create() (int64, error) { c.ApiKey = utils.RandomString(7) row := checkinDB().Create(&c) + c.Start() + go c.Routine() if row.Error != nil { utils.Log(2, row.Error) return 0, row.Error @@ -99,8 +163,8 @@ func (c *checkin) Create() (int64, error) { return c.Id, row.Error } -// Update will update a checkin -func (c *checkin) Update() (int64, error) { +// Update will update a Checkin +func (c *Checkin) Update() (int64, error) { row := checkinDB().Update(c) if row.Error != nil { utils.Log(2, row.Error) @@ -128,8 +192,8 @@ func (c *checkinHit) Ago() string { return got } -// RecheckCheckinFailure will check if a Service checkin has been reported yet -func (c *checkin) RecheckCheckinFailure(guard chan struct{}) { +// RecheckCheckinFailure will check if a Service Checkin has been reported yet +func (c *Checkin) RecheckCheckinFailure(guard chan struct{}) { between := time.Now().Sub(time.Now()).Seconds() if between > float64(c.Interval) { fmt.Println("rechecking every 15 seconds!") diff --git a/core/database.go b/core/database.go index 9d8125be..31e582bd 100644 --- a/core/database.go +++ b/core/database.go @@ -63,12 +63,12 @@ func usersDB() *gorm.DB { return DbSession.Model(&types.User{}) } -// checkinDB returns the checkin records for a service +// checkinDB returns the Checkin records for a service func checkinDB() *gorm.DB { return DbSession.Model(&types.Checkin{}) } -// checkinHitsDB returns the 'hits' from the checkin record +// checkinHitsDB returns the 'hits' from the Checkin record func checkinHitsDB() *gorm.DB { return DbSession.Model(&types.CheckinHit{}) } @@ -115,8 +115,8 @@ func (u *user) AfterFind() (err error) { return } -// AfterFind for checkin will set the timezone -func (c *checkin) AfterFind() (err error) { +// AfterFind for Checkin will set the timezone +func (c *Checkin) AfterFind() (err error) { c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) return } @@ -159,8 +159,8 @@ func (s *Service) BeforeCreate() (err error) { return } -// BeforeCreate for checkin will set CreatedAt to UTC -func (c *checkin) BeforeCreate() (err error) { +// BeforeCreate for Checkin will set CreatedAt to UTC +func (c *Checkin) BeforeCreate() (err error) { if c.CreatedAt.IsZero() { c.CreatedAt = time.Now().UTC() } diff --git a/core/failures.go b/core/failures.go index 116b56b5..bb61241e 100644 --- a/core/failures.go +++ b/core/failures.go @@ -43,7 +43,7 @@ func (s *Service) CreateFailure(f *types.Failure) (int64, error) { // AllFailures will return all failures attached to a service func (s *Service) AllFailures() []*failure { var fails []*failure - col := failuresDB().Where("service = ?", s.Id).Order("id desc") + col := failuresDB().Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc") err := col.Find(&fails) if err.Error != nil { utils.Log(3, fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err)) @@ -123,13 +123,16 @@ func (s *Service) TotalFailures() (uint64, error) { // TotalFailuresSince returns the total amount of failures for a service since a specific time/date func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) { var count uint64 - rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")) + rows := failuresDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Not("method = 'checkin'") err := rows.Count(&count) return count, err.Error } // ParseError returns a human readable error for a failure func (f *failure) ParseError() string { + if f.Method == "checkin" { + return fmt.Sprintf("Checkin is Offline") + } err := strings.Contains(f.Issue, "connection reset by peer") if err { return fmt.Sprintf("Connection Reset") diff --git a/core/hits.go b/core/hits.go index 9719f01c..697c16ee 100644 --- a/core/hits.go +++ b/core/hits.go @@ -27,7 +27,7 @@ type Hit struct { // CreateHit will create a new 'hit' record in the database for a successful/online service func (s *Service) CreateHit(h *types.Hit) (int64, error) { - db := hitsDB().Create(h) + db := hitsDB().Create(&h) if db.Error != nil { utils.Log(2, db.Error) return 0, db.Error diff --git a/core/sample.go b/core/sample.go index d6355d00..1dbabe91 100644 --- a/core/sample.go +++ b/core/sample.go @@ -89,11 +89,11 @@ func InsertSampleData() error { return nil } -// insertSampleCheckins will create 2 checkins with 60 successful hits per checkin +// insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin func insertSampleCheckins() error { s1 := SelectService(1) checkin1 := ReturnCheckin(&types.Checkin{ - Service: s1.Id, + ServiceId: s1.Id, Interval: 300, GracePeriod: 300, }) @@ -101,7 +101,7 @@ func insertSampleCheckins() error { s2 := SelectService(1) checkin2 := ReturnCheckin(&types.Checkin{ - Service: s2.Id, + ServiceId: s2.Id, Interval: 900, GracePeriod: 300, }) diff --git a/core/services.go b/core/services.go index a8508bf4..5fd9a3cc 100644 --- a/core/services.go +++ b/core/services.go @@ -51,13 +51,29 @@ func SelectService(id int64) *Service { return nil } +// CheckinProcess runs the checkin routine for each checkin attached to service +func (s *Service) CheckinProcess() { + checkins := s.Checkins() + for _, c := range checkins { + c.Start() + go c.Routine() + } +} + // Checkins will return a slice of Checkins for a Service -func (s *Service) Checkins() []*checkin { - var checkin []*checkin +func (s *Service) Checkins() []*Checkin { + var checkin []*Checkin checkinDB().Where("service = ?", s.Id).Find(&checkin) return checkin } +// LimitedCheckins will return a slice of Checkins for a Service +func (s *Service) LimitedCheckins() []*Checkin { + var checkin []*Checkin + checkinDB().Where("service = ?", s.Id).Limit(10).Find(&checkin) + return checkin +} + // SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup. func (c *Core) SelectAllServices() ([]*Service, error) { var services []*Service @@ -69,7 +85,7 @@ func (c *Core) SelectAllServices() ([]*Service, error) { CoreApp.Services = nil for _, service := range services { service.Start() - service.Checkins() + service.CheckinProcess() service.AllFailures() CoreApp.Services = append(CoreApp.Services, service) } diff --git a/core/services_checkin_test.go b/core/services_checkin_test.go index b953b8a9..c892ffe5 100644 --- a/core/services_checkin_test.go +++ b/core/services_checkin_test.go @@ -24,7 +24,7 @@ import ( ) var ( - testCheckin *checkin + testCheckin *Checkin ) func TestCreateCheckin(t *testing.T) { diff --git a/handlers/handlers.go b/handlers/handlers.go index 46f68f41..683d4ede 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -157,6 +157,9 @@ var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap "NewUser": func() *types.User { return new(types.User) }, + "NewCheckin": func() *types.Checkin { + return new(types.Checkin) + }, } } diff --git a/handlers/routes.go b/handlers/routes.go index 85b938e0..914903e9 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -86,8 +86,9 @@ func Router() *mux.Router { r.Handle("/service/{id}/edit", http.HandlerFunc(servicesViewHandler)) r.Handle("/service/{id}/delete", http.HandlerFunc(servicesDeleteHandler)) r.Handle("/service/{id}/delete_failures", http.HandlerFunc(servicesDeleteFailuresHandler)).Methods("GET") - r.Handle("/service/{id}/checkin", http.HandlerFunc(checkinCreateUpdateHandler)).Methods("POST") - r.Handle("/checkin/{id}", http.HandlerFunc(checkinUpdateHandler)) + r.Handle("/service/{id}/checkin", http.HandlerFunc(checkinCreateHandler)).Methods("POST") + r.Handle("/checkin/{id}/delete", http.HandlerFunc(checkinDeleteHandler)).Methods("GET") + r.Handle("/checkin/{id}", http.HandlerFunc(checkinHitHandler)) // API SERVICE Routes r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET") diff --git a/handlers/services.go b/handlers/services.go index fd03f5a7..8913c513 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -217,25 +217,42 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) { executeResponse(w, r, "services.html", core.CoreApp.Services, "/services") } -func checkinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) { +func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) { if !IsAuthenticated(r) { http.Redirect(w, r, "/", http.StatusSeeOther) return } vars := mux.Vars(r) - service := core.SelectService(utils.StringInt(vars["id"])) - api := r.PostForm.Get("api") - checkin := core.SelectCheckin(api) - - interval := utils.StringInt(r.PostForm.Get("interval")) - grace := utils.StringInt(r.PostForm.Get("grace")) - checkin.Interval = interval - checkin.GracePeriod = grace - checkin.Update() + checkin := core.SelectCheckinId(utils.StringInt(vars["id"])) + service := core.SelectService(checkin.ServiceId) + fmt.Println(checkin, service) + checkin.Delete() executeResponse(w, r, "service.html", service, fmt.Sprintf("/service/%v", service.Id)) } -func checkinUpdateHandler(w http.ResponseWriter, r *http.Request) { +func checkinCreateHandler(w http.ResponseWriter, r *http.Request) { + if !IsAuthenticated(r) { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + vars := mux.Vars(r) + r.ParseForm() + service := core.SelectService(utils.StringInt(vars["id"])) + fmt.Println(service.Name) + name := r.PostForm.Get("name") + interval := utils.StringInt(r.PostForm.Get("interval")) + grace := utils.StringInt(r.PostForm.Get("grace")) + checkin := core.ReturnCheckin(&types.Checkin{ + Name: name, + ServiceId: service.Id, + Interval: interval, + GracePeriod: grace, + }) + checkin.Create() + executeResponse(w, r, "service.html", service, fmt.Sprintf("/service/%v", service.Id)) +} + +func checkinHitHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) checkin := core.SelectCheckin(vars["id"]) ip, _, _ := net.SplitHostPort(r.RemoteAddr) diff --git a/source/tmpl/form_checkin.html b/source/tmpl/form_checkin.html index 5f6f2310..01ae7e86 100644 --- a/source/tmpl/form_checkin.html +++ b/source/tmpl/form_checkin.html @@ -1,15 +1,19 @@ {{define "form_checkin"}} -
+
-
- - +
+ +
-
+
+ + +
+
- +
-
+
diff --git a/source/tmpl/form_service.html b/source/tmpl/form_service.html index 0575068c..6933881c 100644 --- a/source/tmpl/form_service.html +++ b/source/tmpl/form_service.html @@ -41,13 +41,13 @@ You can insert Regex to validate the response
-
+
-
+
diff --git a/source/tmpl/service.html b/source/tmpl/service.html index ef81135b..905e1e3a 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -95,6 +95,7 @@

Service Checkin

+ {{if $s.LimitedCheckins}} @@ -102,22 +103,35 @@ + - {{range $s.Checkins}} + {{range $s.LimitedCheckins}} {{ $ch := . }} - - + + - - + + + - {{template "form_checkin" $ch}} {{end}}
Report Period
Grace Period
Last Seen Expected
{{CoreApp.Domain}}/checkin/{{$ch.ApiKey}}
{{$ch.Name}}
{{$ch.Link}}
every {{Duration $ch.Period}}
after {{Duration $ch.Grace}}
{{Ago $ch.Last.CreatedAt}}{{ if lt $ch.Expected 0}}{{Duration $ch.Expected}} ago{{else}}in {{Duration $ch.Expected}}{{end}}{{ if $ch.Last.CreatedAt.IsZero}} + Never + {{else}} + {{Ago $ch.Last.CreatedAt}} + {{end}} + + {{ if $ch.Last.CreatedAt.IsZero}} + - + {{else}} + {{ if lt $ch.Expected 0}}{{Duration $ch.Expected}} ago{{else}}in {{Duration $ch.Expected}}{{end}} + {{end}} + Delete
- + {{end}} + {{template "form_checkin" $s}}
{{end}} @@ -219,19 +233,20 @@ } }); - startPick.setDate(new Date({{.Start}}*1000)); - endPick.setDate(new Date({{.End}}*1000)); - - startPick.hide() - endPick.hide() + startPick.setDate(new Date({{.Start}}* 1000); + ) + endPick.setDate(new Date({{.End}}* 1000); + ) + startPick.hide(); + endPick.hide(); $("#start_date").click(function(e) { startPick.show() - }) + }); $("#end_date").click(function(e) { endPick.show() - }) + }); AjaxChart(chartdata,{{$s.Id}},{{.Start}},{{.End}},"hour"); diff --git a/source/tmpl/settings.html b/source/tmpl/settings.html index 69223580..0088d60d 100644 --- a/source/tmpl/settings.html +++ b/source/tmpl/settings.html @@ -27,7 +27,6 @@

Settings

-
diff --git a/types/checkin.go b/types/checkin.go index beb9697d..f1f4089b 100644 --- a/types/checkin.go +++ b/types/checkin.go @@ -22,12 +22,14 @@ import ( // Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online type Checkin struct { Id int64 `gorm:"primary_key;column:id"` - Service int64 `gorm:"index;column:service"` + ServiceId int64 `gorm:"index;column:service"` + Name string `gorm:"column:name"` Interval int64 `gorm:"column:check_interval"` GracePeriod int64 `gorm:"column:grace_period"` ApiKey string `gorm:"column:api_key"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` + Running chan bool `gorm:"-" json:"-"` } // CheckinHit is a successful response from a Checkin @@ -37,3 +39,28 @@ type CheckinHit struct { From string `gorm:"column:from_location"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` } + +// Start will create a channel for the checkin checking go routine +func (s *Checkin) Start() { + s.Running = make(chan bool) +} + +// Close will stop the checkin routine +func (s *Checkin) Close() { + if s.IsRunning() { + close(s.Running) + } +} + +// IsRunning returns true if the checkin go routine is running +func (s *Checkin) IsRunning() bool { + if s.Running == nil { + return false + } + select { + case <-s.Running: + return false + default: + return true + } +} diff --git a/types/failure.go b/types/failure.go index e7301d0d..52c03a0e 100644 --- a/types/failure.go +++ b/types/failure.go @@ -25,6 +25,7 @@ type Failure struct { Id int64 `gorm:"primary_key;column:id" json:"id"` Issue string `gorm:"column:issue" json:"issue"` Method string `gorm:"column:method" json:"method,omitempty"` + MethodId int64 `gorm:"column:method_id" json:"method_id,omitempty"` Service int64 `gorm:"index;column:service" json:"-"` PingTime float64 `gorm:"column:ping_time"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` diff --git a/utils/encryption.go b/utils/encryption.go index 18ccd370..7c2a0cdd 100644 --- a/utils/encryption.go +++ b/utils/encryption.go @@ -20,6 +20,7 @@ import ( "fmt" "golang.org/x/crypto/bcrypt" "math/rand" + "time" ) // HashPassword returns the bcrypt hash of a password string @@ -46,6 +47,7 @@ var characterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY // RandomString generates a random string of n length func RandomString(n int) string { b := make([]rune, n) + rand.Seed(time.Now().UnixNano()) for i := range b { b[i] = characterRunes[rand.Intn(len(characterRunes))] }