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

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")
//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))

View File

@ -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 = &currentFrameTime
}
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),
})

View File

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

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
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"`
}

View File

@ -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:

View File

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

View File

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

View File

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

View File

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