Downtimes CRUD (#11)

pull/1062/head
Amrendra Singh 2021-09-09 17:54:56 +05:30 committed by GitHub
parent 6636fae7bc
commit ee5a4b8f37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 264 additions and 42 deletions

View File

@ -5,6 +5,7 @@ import (
"github.com/statping/statping/types/checkins" "github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/configs" "github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core" "github.com/statping/statping/types/core"
"github.com/statping/statping/types/downtimes"
"github.com/statping/statping/types/errors" "github.com/statping/statping/types/errors"
"github.com/statping/statping/types/groups" "github.com/statping/statping/types/groups"
"github.com/statping/statping/types/incidents" "github.com/statping/statping/types/incidents"
@ -163,6 +164,10 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
case *incidents.IncidentUpdate: case *incidents.IncidentUpdate:
objName = "incident_update" objName = "incident_update"
objId = v.Id objId = v.Id
case *downtimes.Downtime:
objName = "downtime"
objId = v.Id
default: default:
objName = fmt.Sprintf("%T", v) objName = fmt.Sprintf("%T", v)
} }

164
handlers/downtimes.go Normal file
View File

@ -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)
}

View File

@ -192,6 +192,13 @@ func Router() *mux.Router {
api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE") api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE")
//r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler)) //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 // API Generic Routes
r.Handle("/health", http.HandlerFunc(healthCheckHandler)) r.Handle("/health", http.HandlerFunc(healthCheckHandler))

View File

@ -158,6 +158,7 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
s2.Online = zeroBool s2.Online = zeroBool
s2.FailureCounter = zeroInt s2.FailureCounter = zeroInt
s2.CurrentDowntime = zeroInt64 s2.CurrentDowntime = zeroInt64
s2.ManualDowntime = zeroBool
if err := s2.Update(); err != nil { if err := s2.Update(); err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
@ -444,27 +445,32 @@ func apiServiceBlockSeriesHandlerCoreV2(r *http.Request, service *services.Servi
Status: services.STATUS_UP, Status: services.STATUS_UP,
Downtimes: &[]services.Downtime{}} Downtimes: &[]services.Downtime{}}
for _, data := range *downtimesList { now := time.Now()
if (currentFrameTime.Before(data.Start) && nextFrameTime.After(data.Start)) || for _, data := range *downtimesList {
(currentFrameTime.Before(data.End) && nextFrameTime.After(data.End)) || if data.End == nil {
(currentFrameTime.After(data.Start) && nextFrameTime.Before(data.End)) { 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 start := data.Start
end := data.End end := data.End
if currentFrameTime.After(data.Start) { if currentFrameTime.After(*data.Start) {
start = currentFrameTime start = &currentFrameTime
} }
if nextFrameTime.Before(data.End) { if nextFrameTime.Before(*data.End) {
end = nextFrameTime end = &nextFrameTime
} }
*block.Downtimes = append(*block.Downtimes, services.Downtime{ *block.Downtimes = append(*block.Downtimes, services.Downtime{
Start: start, Start: *start,
End: end, End: *end,
Duration: end.Sub(start).Milliseconds(), Duration: end.Sub(*start).Milliseconds(),
SubStatus: services.HandleEmptyStatus(data.SubStatus), SubStatus: services.HandleEmptyStatus(data.SubStatus),
}) })

View File

@ -6,6 +6,9 @@ import (
"time" "time"
) )
var (
zeroInt64 int64
)
var db database.Database var db database.Database
var dbHits database.Database var dbHits database.Database
@ -25,6 +28,29 @@ func Find(id int64) (*Downtime, error) {
return &downtime, q.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) { func FindByService(service int64, start time.Time, end time.Time) (*[]Downtime, error) {
var downtime []Downtime var downtime []Downtime
q := db.Where("service = ? and start BETWEEN ? AND ? ", service, start, end) 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 { func (c *Downtime) Delete() error {
q := dbHits.Where("id = ?", c.Id).Delete(&Downtime{}) q := db.Model(&Downtime{}).Delete(c)
if err := q.Error(); err != nil {
return err
}
q = db.Model(&Downtime{}).Delete(c)
return q.Error() return q.Error()
} }

View File

@ -6,10 +6,11 @@ import (
// Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online // Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online
type Downtime struct { type Downtime struct {
Id int64 `gorm:"primary_key;column:id" json:"id"` Id int64 `gorm:"primary_key;column:id" json:"id"`
ServiceId int64 `gorm:"index;column:service" json:"service_id"` ServiceId int64 `gorm:"index;column:service" json:"service_id"`
SubStatus string `gorm:"column:sub_status" json:"sub_status"` SubStatus string `gorm:"column:sub_status" json:"sub_status"`
Failures int `gorm:"column:failures" json:"failures"` Failures int `gorm:"column:failures" json:"failures"`
Start time.Time `gorm:"index;column:start" json:"start"` Start *time.Time `gorm:"index;column:start" json:"start"`
End time.Time `gorm:"column:end" json:"end"` End *time.Time `gorm:"column:end" json:"end"`
Type string `gorm:"default:'auto';column:type" json:"type"`
} }

View File

@ -15,18 +15,6 @@ const (
STATUS_DEGRADED = "degraded" 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 { func ApplyStatus(current string, apply string, defaultStatus string) string {
switch current { switch current {
case STATUS_DOWN: case STATUS_DOWN:

View File

@ -142,13 +142,15 @@ func (s *Service) UpdateOrder() (err error) {
d := db.Model(s).Where(" id = ? ", s.Id).Updates(updateFields) d := db.Model(s).Where(" id = ? ", s.Id).Updates(updateFields)
if err = d.Error(); d.Error() != nil { 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()) log.Errorf("[DB ERROR]Failed toservice order : %s %s %s %s", s.Id, s.Name, updateFields, d.Error())
return err
} }
if d.RowsAffected() == 0 { if d.RowsAffected() == 0 {
err = fmt.Errorf("[Zero]Failed to update service order : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) 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()) 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) log.Infof("Service Order updates Saved : %s %s %s", s.Id, s.Name, updateFields)
return return nil
} }
func (s *Service) Delete() error { func (s *Service) Delete() error {
@ -208,6 +210,20 @@ func (s *Service) acquireServiceRun() error {
return nil 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() { func (s *Service) markServiceRunProcessed() {
updateFields := map[string]interface{}{ updateFields := map[string]interface{}{
"online": s.Online, "online": s.Online,
@ -218,12 +234,14 @@ func (s *Service) markServiceRunProcessed() {
"current_downtime": s.CurrentDowntime, "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 { if d.Error() != nil {
log.Errorf("[DB ERROR]Failed to update service run : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) 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 { if d.RowsAffected() == 0 {
log.Errorf("[Zero]Failed to update service run : %s %s %s %s", s.Id, s.Name, updateFields, d.Error()) 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) log.Infof("Service Run Updates Saved : %s %s %s", s.Id, s.Name, updateFields)
} }

View File

@ -218,8 +218,12 @@ func addDurations(s []series, on bool) int64 {
func addDowntimeDurations(d *[]downtimes.Downtime) int64 { func addDowntimeDurations(d *[]downtimes.Downtime) int64 {
var dur int64 var dur int64
now := time.Now()
for _, v := range *d { 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 return dur
} }

View File

@ -36,7 +36,7 @@ func CheckServices() {
func refreshAllServices() { func refreshAllServices() {
for { for {
time.Sleep(time.Duration(time.Second * 60)) time.Sleep(time.Duration(time.Second * 20))
newList := all() newList := all()
@ -71,7 +71,7 @@ CheckLoop:
s, er := Find(s.Id) s, er := Find(s.Id)
if er == nil { if er == nil {
if err == nil { if err == nil && !s.ManualDowntime {
log.Infof("Service Run Started : %s %s", s.Id, s.Name) 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 s.Online = false
t := time.Now().Add(time.Duration(-s.FailureCounter*s.Interval) * (time.Second))
downtime := &downtimes.Downtime{ downtime := &downtimes.Downtime{
Start: time.Now().Add(time.Duration(-s.FailureCounter*s.Interval) * (time.Second)), Start: &t,
ServiceId: s.Id, 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) newStatus := HandleEmptyStatus(s.LastFailureType)
start := time.Now().Add(time.Duration(-s.Interval) * (time.Second))
if downtime.SubStatus != "" && downtime.SubStatus != newStatus { if downtime.SubStatus != "" && downtime.SubStatus != newStatus {
downtime.Id = 0 downtime.Id = 0
downtime.Start = time.Now().Add(time.Duration(-s.Interval) * (time.Second)) downtime.Start = &start
} }
downtime.SubStatus = newStatus downtime.SubStatus = newStatus

View File

@ -75,6 +75,7 @@ type Service struct {
FailureCounter int `gorm:"column:failure_counter" json:"-" yaml:"-"` FailureCounter int `gorm:"column:failure_counter" json:"-" yaml:"-"`
CurrentDowntime int64 `gorm:"column:current_downtime" json:"-" yaml:"-"` CurrentDowntime int64 `gorm:"column:current_downtime" json:"-" yaml:"-"`
LastFailureType string `gorm:"-" 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) // ServiceOrder will reorder the services based on 'order_id' (Order)