From ee5a4b8f37e291c70f0f90c9a6ec3a73b264e079 Mon Sep 17 00:00:00 2001 From: Amrendra Singh Date: Thu, 9 Sep 2021 17:54:56 +0530 Subject: [PATCH] Downtimes CRUD (#11) --- handlers/api.go | 5 ++ handlers/downtimes.go | 164 ++++++++++++++++++++++++++++++++++++ handlers/routes.go | 7 ++ handlers/services.go | 28 +++--- types/downtimes/database.go | 32 +++++-- types/downtimes/struct.go | 13 +-- types/services/constants.go | 12 --- types/services/database.go | 22 ++++- types/services/methods.go | 6 +- types/services/routine.go | 16 ++-- types/services/struct.go | 1 + 11 files changed, 264 insertions(+), 42 deletions(-) create mode 100644 handlers/downtimes.go diff --git a/handlers/api.go b/handlers/api.go index cb960902..a564bd2e 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -5,6 +5,7 @@ import ( "github.com/statping/statping/types/checkins" "github.com/statping/statping/types/configs" "github.com/statping/statping/types/core" + "github.com/statping/statping/types/downtimes" "github.com/statping/statping/types/errors" "github.com/statping/statping/types/groups" "github.com/statping/statping/types/incidents" @@ -163,6 +164,10 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht case *incidents.IncidentUpdate: objName = "incident_update" objId = v.Id + case *downtimes.Downtime: + objName = "downtime" + objId = v.Id + default: objName = fmt.Sprintf("%T", v) } diff --git a/handlers/downtimes.go b/handlers/downtimes.go new file mode 100644 index 00000000..3a451684 --- /dev/null +++ b/handlers/downtimes.go @@ -0,0 +1,164 @@ +package handlers + +import ( + "fmt" + "github.com/gorilla/mux" + "github.com/statping/statping/types/downtimes" + "github.com/statping/statping/types/services" + "github.com/statping/statping/utils" + "net/http" + "time" +) + +func findDowntime(r *http.Request) (*downtimes.Downtime, error) { + vars := mux.Vars(r) + id := utils.ToInt(vars["id"]) + downtime, err := downtimes.Find(id) + if err != nil { + return nil, err + } + + return downtime, nil +} + +func apiAllDowntimesForServiceHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + serviceId := utils.ToInt(vars["service_id"]) + + ninetyDaysAgo := time.Now().Add(time.Duration(-90*24) * time.Hour) + + downtime, err := downtimes.FindByService(serviceId, ninetyDaysAgo, time.Now()) + if err != nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(downtime, "fetch", w, r) +} + +func apiCreateDowntimeHandler(w http.ResponseWriter, r *http.Request) { + var downtime *downtimes.Downtime + if err := DecodeJSON(r, &downtime); err != nil { + sendErrorJson(err, w, r) + return + } + + s, err := services.FindFirstFromDB(downtime.ServiceId) + if err != nil { + sendErrorJson(err, w, r) + return + } + + downtime.Type = "manual" + downtime.Id = zeroInt64 + + if err := downtime.Create(); err != nil { + sendErrorJson(err, w, r) + return + } + + if downtime.End == nil { + updateFields := map[string]interface{}{ + "online": false, + "current_downtime": downtime.Id, + "manual_downtime": true, + } + + if err := s.UpdateSpecificFields(updateFields); err != nil { + sendErrorJson(err, w, r) + return + } + } + + sendJsonAction(downtime, "create", w, r) +} + +func apiDowntimeHandler(w http.ResponseWriter, r *http.Request) { + downtime, err := findDowntime(r) + if downtime == nil { + sendErrorJson(err, w, r) + return + } + sendJsonAction(downtime, "fetch", w, r) +} + +func apiPatchDowntimeHandler(w http.ResponseWriter, r *http.Request) { + downtime, err := findDowntime(r) + if err != nil { + sendErrorJson(err, w, r) + return + } + var req downtimes.Downtime + if err := DecodeJSON(r, &req); err != nil { + sendErrorJson(err, w, r) + return + } + + s, err := services.FindFirstFromDB(downtime.ServiceId) + if err != nil { + sendErrorJson(err, w, r) + return + } + + if downtime.End != nil && req.End == nil { + fmt.Errorf("Cannot reopen a downtime!") + } + + if downtime.End == nil && req.End != nil { + updateFields := map[string]interface{}{ + "online": true, + "failure_counter": 0, + "current_downtime": 0, + "manual_downtime": false, + } + + if err := s.UpdateSpecificFields(updateFields); err != nil { + sendErrorJson(err, w, r) + return + } + } + + downtime.Start = req.Start + downtime.End = req.End + downtime.Type = "manual" + downtime.Failures = req.Failures + downtime.SubStatus = req.SubStatus + + if err := downtime.Update(); err != nil { + sendErrorJson(err, w, r) + return + } + + sendJsonAction(downtime, "update", w, r) +} + +func apiDeleteDowntimeHandler(w http.ResponseWriter, r *http.Request) { + downtime, err := findDowntime(r) + if err != nil { + sendErrorJson(err, w, r) + return + } + if downtime.End == nil { + s2, err := services.FindFirstFromDB(downtime.ServiceId) + if err != nil { + fmt.Errorf("Error updating service") + } + s2.LastProcessingTime = zeroTime + s2.Online = true + s2.FailureCounter = 0 + s2.CurrentDowntime = 0 + s2.ManualDowntime = false + + if err := s2.Update(); err != nil { + sendErrorJson(err, w, r) + return + } + } + + err = downtime.Delete() + if err != nil { + sendErrorJson(err, w, r) + return + } + + sendJsonAction(downtime, "delete", w, r) +} diff --git a/handlers/routes.go b/handlers/routes.go index fdb35774..cf070068 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -192,6 +192,13 @@ func Router() *mux.Router { api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE") //r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler)) + // API DOWNTIME Routes + api.Handle("/api/service/{service_id}/downtimes", authenticated(apiAllDowntimesForServiceHandler, false)).Methods("GET") + api.Handle("/api/downtimes", authenticated(apiCreateDowntimeHandler, false)).Methods("POST") + api.Handle("/api/downtimes/{id}", authenticated(apiDowntimeHandler, false)).Methods("GET") + api.Handle("/api/downtimes/{id}", authenticated(apiPatchDowntimeHandler, false)).Methods("PATCH") + api.Handle("/api/downtimes/{id}", authenticated(apiDeleteDowntimeHandler, false)).Methods("DELETE") + // API Generic Routes r.Handle("/health", http.HandlerFunc(healthCheckHandler)) diff --git a/handlers/services.go b/handlers/services.go index 0b69ba4c..2e67238b 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -158,6 +158,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { s2.Online = zeroBool s2.FailureCounter = zeroInt s2.CurrentDowntime = zeroInt64 + s2.ManualDowntime = zeroBool if err := s2.Update(); err != nil { sendErrorJson(err, w, r) @@ -444,27 +445,32 @@ func apiServiceBlockSeriesHandlerCoreV2(r *http.Request, service *services.Servi Status: services.STATUS_UP, Downtimes: &[]services.Downtime{}} - for _, data := range *downtimesList { + now := time.Now() - if (currentFrameTime.Before(data.Start) && nextFrameTime.After(data.Start)) || - (currentFrameTime.Before(data.End) && nextFrameTime.After(data.End)) || - (currentFrameTime.After(data.Start) && nextFrameTime.Before(data.End)) { + for _, data := range *downtimesList { + if data.End == nil { + data.End = &now + } + + if (currentFrameTime.Before(*data.Start) && nextFrameTime.After(*data.Start)) || + (currentFrameTime.Before(*data.End) && nextFrameTime.After(*data.End)) || + (currentFrameTime.After(*data.Start) && nextFrameTime.Before(*data.End)) { start := data.Start end := data.End - if currentFrameTime.After(data.Start) { - start = currentFrameTime + if currentFrameTime.After(*data.Start) { + start = ¤tFrameTime } - if nextFrameTime.Before(data.End) { - end = nextFrameTime + if nextFrameTime.Before(*data.End) { + end = &nextFrameTime } *block.Downtimes = append(*block.Downtimes, services.Downtime{ - Start: start, - End: end, - Duration: end.Sub(start).Milliseconds(), + Start: *start, + End: *end, + Duration: end.Sub(*start).Milliseconds(), SubStatus: services.HandleEmptyStatus(data.SubStatus), }) diff --git a/types/downtimes/database.go b/types/downtimes/database.go index 59a92d3e..0d0f1941 100644 --- a/types/downtimes/database.go +++ b/types/downtimes/database.go @@ -6,6 +6,9 @@ import ( "time" ) +var ( + zeroInt64 int64 +) var db database.Database var dbHits database.Database @@ -25,6 +28,29 @@ func Find(id int64) (*Downtime, error) { return &downtime, q.Error() } +func (c *Downtime) Validate() error { + if c.Type == "manual" { + if c.End != nil && c.End.After(time.Now()) || c.Start.After(time.Now()) { + return fmt.Errorf("Downtime cannot be in future") + } + if c.ServiceId == zeroInt64 { + return fmt.Errorf("Service ID cannot be null") + } + if c.SubStatus != "down" && c.SubStatus != "degraded" { + return fmt.Errorf("SubStatus can only be 'down' or 'degraded'") + } + } + return nil +} + +func (c *Downtime) BeforeCreate() error { + return c.Validate() +} + +func (c *Downtime) BeforeUpdate() error { + return c.Validate() +} + func FindByService(service int64, start time.Time, end time.Time) (*[]Downtime, error) { var downtime []Downtime q := db.Where("service = ? and start BETWEEN ? AND ? ", service, start, end) @@ -43,10 +69,6 @@ func (c *Downtime) Update() error { } func (c *Downtime) Delete() error { - q := dbHits.Where("id = ?", c.Id).Delete(&Downtime{}) - if err := q.Error(); err != nil { - return err - } - q = db.Model(&Downtime{}).Delete(c) + q := db.Model(&Downtime{}).Delete(c) return q.Error() } diff --git a/types/downtimes/struct.go b/types/downtimes/struct.go index f0e92663..eed71c9e 100644 --- a/types/downtimes/struct.go +++ b/types/downtimes/struct.go @@ -6,10 +6,11 @@ import ( // Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online type Downtime struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - ServiceId int64 `gorm:"index;column:service" json:"service_id"` - SubStatus string `gorm:"column:sub_status" json:"sub_status"` - Failures int `gorm:"column:failures" json:"failures"` - Start time.Time `gorm:"index;column:start" json:"start"` - End time.Time `gorm:"column:end" json:"end"` + Id int64 `gorm:"primary_key;column:id" json:"id"` + ServiceId int64 `gorm:"index;column:service" json:"service_id"` + SubStatus string `gorm:"column:sub_status" json:"sub_status"` + Failures int `gorm:"column:failures" json:"failures"` + Start *time.Time `gorm:"index;column:start" json:"start"` + End *time.Time `gorm:"column:end" json:"end"` + Type string `gorm:"default:'auto';column:type" json:"type"` } diff --git a/types/services/constants.go b/types/services/constants.go index 5b51aa69..c3bb456e 100644 --- a/types/services/constants.go +++ b/types/services/constants.go @@ -15,18 +15,6 @@ const ( STATUS_DEGRADED = "degraded" ) -const ( - FAILURE_TYPE_COMPLETE = "complete" - FAILURE_TYPE_DEGRADED = "degraded" - FAILURE_TYPE_DEFAULT = "" -) - -var FailureTypeStatusMap = map[string]string{ - FAILURE_TYPE_DEFAULT: STATUS_DOWN, - FAILURE_TYPE_COMPLETE: STATUS_DOWN, - FAILURE_TYPE_DEGRADED: STATUS_DEGRADED, -} - func ApplyStatus(current string, apply string, defaultStatus string) string { switch current { case STATUS_DOWN: diff --git a/types/services/database.go b/types/services/database.go index 585db3da..87aa93be 100644 --- a/types/services/database.go +++ b/types/services/database.go @@ -142,13 +142,15 @@ func (s *Service) UpdateOrder() (err error) { d := db.Model(s).Where(" id = ? ", s.Id).Updates(updateFields) if err = d.Error(); d.Error() != nil { log.Errorf("[DB ERROR]Failed toservice order : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return err } if d.RowsAffected() == 0 { err = fmt.Errorf("[Zero]Failed to update service order : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) log.Errorf("[Zero]Failed to update service order : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return err } log.Infof("Service Order updates Saved : %s %s %s", s.Id, s.Name, updateFields) - return + return nil } func (s *Service) Delete() error { @@ -208,6 +210,20 @@ func (s *Service) acquireServiceRun() error { return nil } +func (s *Service) UpdateSpecificFields(updateFields map[string]interface{}) error { + d := db.Model(s).Where(" id = ?", s.Id).Updates(updateFields) + if d.Error() != nil { + log.Errorf("[DB ERROR]Failed to update service : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return d.Error() + } + if d.RowsAffected() == 0 { + log.Errorf("[Zero]Failed to update service : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return fmt.Errorf("Failed to update Service fields : %s", s.Id) + } + log.Infof("Service Updates Saved : %s %s %s", s.Id, s.Name, updateFields) + return nil +} + func (s *Service) markServiceRunProcessed() { updateFields := map[string]interface{}{ "online": s.Online, @@ -218,12 +234,14 @@ func (s *Service) markServiceRunProcessed() { "current_downtime": s.CurrentDowntime, } - d := db.Model(s).Where(" id = ? ", s.Id).Updates(updateFields) + d := db.Model(s).Where(" id = ? and manual_downtime = ?", s.Id, false).Updates(updateFields) if d.Error() != nil { log.Errorf("[DB ERROR]Failed to update service run : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return } if d.RowsAffected() == 0 { log.Errorf("[Zero]Failed to update service run : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) + return } log.Infof("Service Run Updates Saved : %s %s %s", s.Id, s.Name, updateFields) } diff --git a/types/services/methods.go b/types/services/methods.go index bc040925..b4ac3be7 100644 --- a/types/services/methods.go +++ b/types/services/methods.go @@ -218,8 +218,12 @@ func addDurations(s []series, on bool) int64 { func addDowntimeDurations(d *[]downtimes.Downtime) int64 { var dur int64 + now := time.Now() for _, v := range *d { - dur += v.End.Sub(v.Start).Milliseconds() + if v.End == nil { + v.End = &now + } + dur += v.End.Sub(*v.Start).Milliseconds() } return dur } diff --git a/types/services/routine.go b/types/services/routine.go index 0821d909..06e5def3 100644 --- a/types/services/routine.go +++ b/types/services/routine.go @@ -36,7 +36,7 @@ func CheckServices() { func refreshAllServices() { for { - time.Sleep(time.Duration(time.Second * 60)) + time.Sleep(time.Duration(time.Second * 20)) newList := all() @@ -71,7 +71,7 @@ CheckLoop: s, er := Find(s.Id) if er == nil { - if err == nil { + if err == nil && !s.ManualDowntime { log.Infof("Service Run Started : %s %s", s.Id, s.Name) @@ -579,8 +579,10 @@ func (s *Service) HandleDowntime(err error, record bool) { s.Online = false + t := time.Now().Add(time.Duration(-s.FailureCounter*s.Interval) * (time.Second)) + downtime := &downtimes.Downtime{ - Start: time.Now().Add(time.Duration(-s.FailureCounter*s.Interval) * (time.Second)), + Start: &t, ServiceId: s.Id, } @@ -594,12 +596,16 @@ func (s *Service) HandleDowntime(err error, record bool) { } } - downtime.End = time.Now() + now := time.Now() + + downtime.End = &now newStatus := HandleEmptyStatus(s.LastFailureType) + start := time.Now().Add(time.Duration(-s.Interval) * (time.Second)) + if downtime.SubStatus != "" && downtime.SubStatus != newStatus { downtime.Id = 0 - downtime.Start = time.Now().Add(time.Duration(-s.Interval) * (time.Second)) + downtime.Start = &start } downtime.SubStatus = newStatus diff --git a/types/services/struct.go b/types/services/struct.go index 879a5ed4..a6d99ce1 100644 --- a/types/services/struct.go +++ b/types/services/struct.go @@ -75,6 +75,7 @@ type Service struct { FailureCounter int `gorm:"column:failure_counter" json:"-" yaml:"-"` CurrentDowntime int64 `gorm:"column:current_downtime" json:"-" yaml:"-"` LastFailureType string `gorm:"-" json:"-" yaml:"-"` + ManualDowntime bool `gorm:"default:false;column:manual_downtime" json:"manual_downtime"` } // ServiceOrder will reorder the services based on 'order_id' (Order)