From f64fc9e682b7ef06c148f625e44989c38c9f2401 Mon Sep 17 00:00:00 2001 From: hunterlong Date: Sun, 23 Feb 2020 21:53:15 -0800 Subject: [PATCH] ye --- core/checkin.go | 37 ++-- core/failures.go | 13 -- core/hits.go | 4 +- core/services.go | 134 ++---------- core/services_test.go | 6 - database/database.go | 83 ++----- database/group.go | 202 +++++++++++++----- database/service.go | 16 +- frontend/src/API.js | 4 +- .../src/components/Service/ServiceChart.vue | 8 +- .../src/components/Service/ServiceInfo.vue | 13 +- frontend/src/forms/Service.vue | 7 +- frontend/src/mixin.js | 14 ++ go.mod | 1 + handlers/checkin.go | 4 +- handlers/errors.go | 47 ++++ handlers/function.go | 11 + handlers/query.go | 1 + handlers/services.go | 64 +++--- handlers/settings.go | 31 --- types/checkin.go | 26 +-- types/failure.go | 2 - types/time.go | 45 +++- types/time_test.go | 46 ++++ types/types.go | 9 +- 25 files changed, 433 insertions(+), 395 deletions(-) create mode 100644 handlers/errors.go create mode 100644 handlers/query.go delete mode 100644 handlers/settings.go create mode 100644 types/time_test.go diff --git a/core/checkin.go b/core/checkin.go index 92905ffd..0ba060a8 100644 --- a/core/checkin.go +++ b/core/checkin.go @@ -20,7 +20,6 @@ import ( "github.com/ararog/timeago" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" - "sort" "time" ) @@ -94,8 +93,8 @@ func (c *Checkin) CreateFailure() (int64, error) { PingTime: c.Expected().Seconds(), }} row := Database(&Failure{}).Create(&fail) - sort.Sort(types.FailSort(c.Failures)) - c.Failures = append(c.Failures, fail) + //sort.Sort(types.FailSort(c.Failures)) + //c.Failures = append(c.Failures, fail) if len(c.Failures) > limitedFailures { c.Failures = c.Failures[1:] } @@ -167,23 +166,27 @@ func (c *Checkin) AllHits() []*types.CheckinHit { return checkins } -// Hits returns all of the CheckinHits for a given Checkin -func (c *Checkin) LimitedFailures(amount int) []types.FailureInterface { - var failures []*Failure - var failInterfaces []types.FailureInterface - col := Database(&types.Failure{}).Where("checkin = ?", c.Id).Where("method = 'checkin'").Limit(amount).Order("id desc") - col.Find(&failures) - for _, f := range failures { - failInterfaces = append(failInterfaces, f) - } - return failInterfaces -} - // Hits returns all of the CheckinHits for a given Checkin func (c *Checkin) AllFailures() []*types.Failure { var failures []*types.Failure - col := Database(&types.Failure{}).Where("checkin = ?", c.Id).Where("method = 'checkin'").Order("id desc") - col.Find(&failures) + Database(&types.Failure{}). + Where("checkin = ?", c.Id). + Where("method = 'checkin'"). + Order("id desc"). + Find(&failures) + + return failures +} + +func (c *Checkin) GetFailures(count int) []*types.Failure { + var failures []*types.Failure + Database(&types.Failure{}). + Where("checkin = ?", c.Id). + Where("method = 'checkin'"). + Limit(count). + Order("id desc"). + Find(&failures) + return failures } diff --git a/core/failures.go b/core/failures.go index a61e8d2b..1ebc4dd9 100644 --- a/core/failures.go +++ b/core/failures.go @@ -18,9 +18,7 @@ package core import ( "fmt" "github.com/ararog/timeago" - "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" - "net/http" "sort" "strings" "time" @@ -62,10 +60,6 @@ func (s *Service) AllFailures() []types.Failure { return fails } -func (s *Service) FailuresDb(r *http.Request) database.Database { - return Database(&types.Failure{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc") -} - // DeleteFailures will delete all failures for a service func (s *Service) DeleteFailures() { err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id) @@ -75,13 +69,6 @@ func (s *Service) DeleteFailures() { s.Failures = nil } -// LimitedFailures will return the last amount of failures from a service -func (s *Service) LimitedFailures(amount int) []*Failure { - var failArr []*Failure - Database(&types.Failure{}).Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr) - return failArr -} - // LimitedFailures will return the last amount of failures from a service func (s *Service) LimitedCheckinFailures(amount int) []*Failure { var failArr []*Failure diff --git a/core/hits.go b/core/hits.go index c9d9f260..573c1496 100644 --- a/core/hits.go +++ b/core/hits.go @@ -47,13 +47,13 @@ func (s *Service) CountHits() (int64, error) { // Hits returns all successful hits for a service func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) { var hits []*types.Hit - col := Database(&types.Hit{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc") + col := Database(&types.Hit{}).Where("service = ?", s.Id).Requests(r) err := col.Find(&hits) return hits, err.Error() } func (s *Service) HitsDb(r *http.Request) database.Database { - return Database(&types.Hit{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc") + return Database(&types.Hit{}).Where("service = ?", s.Id).Requests(r).Order("id desc") } // Hits returns all successful hits for a service diff --git a/core/services.go b/core/services.go index a5427099..8e443aee 100644 --- a/core/services.go +++ b/core/services.go @@ -16,9 +16,7 @@ package core import ( - "encoding/json" "fmt" - "github.com/ararog/timeago" "github.com/hunterlong/statping/core/notifier" "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" @@ -58,6 +56,13 @@ func SelectService(id int64) *Service { return nil } +func (s *Service) GetFailures(count int) []*Failure { + var fails []*Failure + db := Database(&types.Failure{}).Where("service = ?", s.Id) + db.Limit(count).Find(&fails) + return fails +} + func (s *Service) UpdateStats() *Service { s.Online24Hours = s.OnlineDaysPercent(1) s.Online7Days = s.OnlineDaysPercent(7) @@ -108,7 +113,8 @@ func (s *Service) AllCheckins() []*Checkin { return checkin } -// SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup. +// SelectAllServices returns a slice of *core.Service to be store on []*core.Services +// should only be called once on startup. func (c *Core) SelectAllServices(start bool) ([]*Service, error) { var services []*Service db := Database(&Service{}).Find(&services).Order("order_id desc") @@ -122,13 +128,13 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) { service.Start() service.CheckinProcess() } - fails := service.LimitedFailures(limitedFailures) + fails := service.GetFailures(limitedFailures) for _, f := range fails { service.Failures = append(service.Failures, f) } checkins := service.AllCheckins() for _, c := range checkins { - c.Failures = c.LimitedFailures(limitedFailures) + c.Failures = c.GetFailures(limitedFailures) c.Hits = c.LimitedHits(limitedHits) service.Checkins = append(service.Checkins, c) } @@ -185,8 +191,8 @@ func (s *Service) OnlineSince(ago time.Time) float32 { } // lastFailure returns the last Failure a service had -func (s *Service) lastFailure() *Failure { - limited := s.LimitedFailures(1) +func (s *Service) lastFailure() types.FailureInterface { + limited := s.GetFailures(1) if len(limited) == 0 { return nil } @@ -194,29 +200,6 @@ func (s *Service) lastFailure() *Failure { return last } -// SmallText returns a short description about a services status -// service.SmallText() -// // Online since Monday 3:04:05PM, Jan _2 2006 -func (s *Service) SmallText() string { - last := s.LimitedFailures(1) - //hits, _ := s.LimitedHits(1) - zone := CoreApp.Timezone - if s.Online { - if len(last) == 0 { - return fmt.Sprintf("Online since %v", utils.Timezoner(s.CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006")) - } else { - return fmt.Sprintf("Online, last Failure was %v", utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006")) - } - } - if len(last) > 0 { - lastFailure := s.lastFailure() - got, _ := timeago.TimeAgoWithTime(time.Now().UTC().Add(s.Downtime()), time.Now().UTC()) - return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError()) - } else { - return fmt.Sprintf("%v is currently offline", s.Name) - } -} - // DowntimeText will return the amount of downtime for a service based on the duration // service.DowntimeText() // // Service has been offline for 15 minutes @@ -261,99 +244,24 @@ func (s *Service) Downtime() time.Duration { return time.Duration(0) } if len(hits) == 0 { - return time.Now().UTC().Sub(fail.CreatedAt.UTC()) + return time.Now().UTC().Sub(fail.Select().CreatedAt.UTC()) } - since := fail.CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) + since := fail.Select().CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC()) return since } -// DateScanObj struct is for creating the charts.js graph JSON array -type DateScanObj struct { - Array []*database.DateScan `json:"data"` -} - -// GraphDataRaw will return all the hits between 2 times for a Service -func GraphHitsDataRaw(service types.ServiceInterface, query *types.GroupQuery, column string) []*database.TimeValue { - srv := service.(*Service) - - dbQuery, err := Database(&types.Hit{}). - Where("service = ?", srv.Id). - GroupQuery(query).ToTimeValue() +// GraphData will return all hits or failures +func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*database.TimeValue { + dbQuery, err := q.Database().GroupQuery(q, by).ToTimeValue(dbType) if err != nil { log.Error(err) return nil } - return dbQuery.FillMissing() -} - -// GraphDataRaw will return all the hits between 2 times for a Service -func GraphFailuresDataRaw(service types.ServiceInterface, query *types.GroupQuery) []*database.TimeValue { - srv := service.(*Service) - - dbQuery, err := Database(&types.Failure{}). - Where("service = ?", srv.Id). - GroupQuery(query).ToTimeValue() - - if err != nil { - log.Error(err) - return nil + if q.FillEmpty { + return dbQuery.FillMissing(q.Start, q.End) } - return dbQuery.FillMissing() -} - -// ToString will convert the DateScanObj into a JSON string for the charts to render -func (d *DateScanObj) ToString() string { - data, err := json.Marshal(d.Array) - if err != nil { - log.Warnln(err) - return "{}" - } - return string(data) -} - -// AvgUptime24 returns a service's average online status for last 24 hours -func (s *Service) AvgUptime24() string { - ago := time.Now().UTC().Add(-24 * time.Hour) - return s.AvgUptime(ago) -} - -// AvgUptime returns average online status for last 24 hours -func (s *Service) AvgUptime(ago time.Time) string { - failed, _ := s.TotalFailuresSince(ago) - if failed == 0 { - return "100" - } - total, _ := s.TotalHitsSince(ago) - if total == 0 { - return "0.00" - } - percent := float64(failed) / float64(total) * 100 - percent = 100 - percent - if percent < 0 { - percent = 0 - } - amount := fmt.Sprintf("%0.2f", percent) - if amount == "100.00" { - amount = "100" - } - return amount -} - -// TotalUptime returns the total uptime percent of a service -func (s *Service) TotalUptime() string { - hits, _ := s.TotalHits() - failures, _ := s.TotalFailures() - percent := float64(failures) / float64(hits) * 100 - percent = 100 - percent - if percent < 0 { - percent = 0 - } - amount := fmt.Sprintf("%0.2f", percent) - if amount == "100.00" { - amount = "100" - } - return amount + return dbQuery.ToValues() } // index returns a services index int for updating the []*core.Services slice diff --git a/core/services_test.go b/core/services_test.go index 728c2f7d..1e90abc3 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -127,12 +127,6 @@ func TestServiceOnline24Hours(t *testing.T) { assert.True(t, service3.OnlineSince(since) > float32(49)) } -func TestServiceSmallText(t *testing.T) { - service := SelectService(5) - text := service.SmallText() - assert.Contains(t, text, "Online since") -} - func TestServiceAvgUptime(t *testing.T) { since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute) service := SelectService(1) diff --git a/database/database.go b/database/database.go index 9494a2a4..789d62b6 100644 --- a/database/database.go +++ b/database/database.go @@ -2,14 +2,12 @@ package database import ( "database/sql" - "fmt" "github.com/hunterlong/statping/types" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" _ "github.com/jinzhu/gorm/dialects/sqlite" "net/http" - "strconv" "strings" "time" ) @@ -21,6 +19,10 @@ const ( TIME_DAY = "2006-01-02" ) +var ( + database Database +) + // Database is an interface which DB implements type Database interface { Close() error @@ -99,11 +101,9 @@ type Database interface { // extra Error() error RowsAffected() int64 - QuerySearch(*http.Request) Database Since(time.Time) Database Between(time.Time, time.Time) Database - Hits() ([]*types.Hit, error) ToChart() ([]*DateScan, error) SelectByTime(string) string @@ -112,7 +112,14 @@ type Database interface { FormatTime(t time.Time) string ParseTime(t string) (time.Time, error) - GroupQuery(query *types.GroupQuery) GroupByer + Requests(*http.Request) Database + + GroupQuery(query *GroupQuery, by By) GroupByer +} + +func (it *Db) Requests(r *http.Request) Database { + g := ParseQueries(r, it) + return g.db } func (it *Db) MultipleSelects(args ...string) Database { @@ -120,10 +127,6 @@ func (it *Db) MultipleSelects(args ...string) Database { return it.Select(joined) } -func CountAmount() string { - return fmt.Sprintf("COUNT(id) as amount") -} - type Db struct { Database *gorm.DB Type string @@ -132,7 +135,11 @@ type Db struct { // Openw is a drop-in replacement for Open() func Openw(dialect string, args ...interface{}) (db Database, err error) { gormdb, err := gorm.Open(dialect, args...) - return Wrap(gormdb), err + if err != nil { + return nil, err + } + database = Wrap(gormdb) + return database, err } // Wrap wraps gorm.DB in an interface @@ -460,8 +467,8 @@ type DateScan struct { } type TimeValue struct { - Timeframe time.Time `json:"timeframe"` - Amount int64 `json:"amount"` + Timeframe string `json:"timeframe"` + Amount float64 `json:"amount"` } func (it *Db) ToChart() ([]*DateScan, error) { @@ -487,55 +494,3 @@ func (it *Db) ToChart() ([]*DateScan, error) { } return data, err } - -func (it *Db) QuerySearch(r *http.Request) Database { - if r == nil { - return it - } - db := it.Database - start := defaultField(r, "start") - end := defaultField(r, "end") - limit := defaultField(r, "limit") - offset := defaultField(r, "offset") - params := &Params{ - Start: start, - End: end, - Limit: limit, - Offset: offset, - } - if params.Start != nil && params.End != nil { - db = db.Where("created_at BETWEEN ? AND ?", time.Unix(*params.Start, 0).Format(TIME), time.Unix(*params.End, 0).UTC().Format(TIME)) - } else if params.Start != nil && params.End == nil { - db = db.Where("created_at > ?", time.Unix(*params.Start, 0).UTC().Format(TIME)) - } - if params.Limit != nil { - db = db.Limit(*params.Limit) - } else { - db = db.Limit(10000) - } - if params.Offset != nil { - db = db.Offset(*params.Offset) - } else { - db = db.Offset(0) - } - return Wrap(db) -} - -type Params struct { - Start *int64 - End *int64 - Limit *int64 - Offset *int64 -} - -func defaultField(r *http.Request, key string) *int64 { - r.ParseForm() - val := r.Form.Get(key) - if val == "" { - return nil - } - - gg, _ := strconv.Atoi(val) - num := int64(gg) - return &num -} diff --git a/database/group.go b/database/group.go index 543b26cd..a6df21c3 100644 --- a/database/group.go +++ b/database/group.go @@ -3,42 +3,60 @@ package database import ( "fmt" "github.com/hunterlong/statping/types" + "github.com/hunterlong/statping/utils" + "net/http" + "net/url" + "strconv" "time" ) type GroupBy struct { db Database - query *types.GroupQuery + query *GroupQuery } type GroupByer interface { - ToTimeValue() (*TimeVar, error) + ToTimeValue(interface{}) (*TimeVar, error) } -type GroupMethod interface { +type By string + +func (b By) String() string { + return string(b) +} + +type GroupQuery struct { + db Database + Start time.Time + End time.Time + Group string + Order string + Limit int + Offset int + FillEmpty bool +} + +func (b GroupQuery) Database() Database { + return b.db } var ( - ByCount = func() GroupMethod { - return fmt.Sprintf("COUNT(id) as amount") + ByCount = By("COUNT(id) as amount") + BySum = func(column string) By { + return By(fmt.Sprintf("SUM(%s) as amount", column)) } - BySum = func(column string) GroupMethod { - return fmt.Sprintf("SUM(%s) as amount", column) - } - ByAverage = func(column string) GroupMethod { - return fmt.Sprintf("SUM(%s) as amount", column) + ByAverage = func(column string) By { + return By(fmt.Sprintf("AVG(%s) as amount", column)) } ) -func execute(db Database, query *types.GroupQuery) Database { - return db.MultipleSelects( - db.SelectByTime(query.Group), - CountAmount(), - ).Between(query.Start, query.End).Group("timeframe").Debug() -} +func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer { + dbQuery := db.MultipleSelects( + db.SelectByTime(q.Group), + by.String(), + ).Group("timeframe") -func (db *Db) GroupQuery(query *types.GroupQuery) GroupByer { - return &GroupBy{execute(db, query), query} + return &GroupBy{dbQuery, q} } type TimeVar struct { @@ -46,64 +64,72 @@ type TimeVar struct { data []*TimeValue } -func (g *GroupBy) ToTimeValue() (*TimeVar, error) { +func (t *TimeVar) ToValues() []*TimeValue { + return t.data +} + +func (g *GroupBy) toFloatRows() []*TimeValue { + rows, err := g.db.Rows() + if err != nil { + return nil + } + var data []*TimeValue + for rows.Next() { + var timeframe time.Time + amount := float64(0) + rows.Scan(&timeframe, &amount) + newTs := types.FixedTime(timeframe, g.duration()) + data = append(data, &TimeValue{ + Timeframe: newTs, + Amount: amount, + }) + } + return data +} + +func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) { rows, err := g.db.Rows() if err != nil { return nil, err } var data []*TimeValue for rows.Next() { - var timeframe string - var amount int64 - if err := rows.Scan(&timeframe, &amount); err != nil { - return nil, err - } - createdTime, _ := g.db.ParseTime(timeframe) + var timeframe time.Time + amount := float64(0) + rows.Scan(&timeframe, &amount) + newTs := types.FixedTime(timeframe, g.duration()) data = append(data, &TimeValue{ - Timeframe: createdTime, + Timeframe: newTs, Amount: amount, }) - } return &TimeVar{g, data}, nil } -func (t *TimeVar) Values() []*TimeValue { +func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue { + timeMap := make(map[string]float64) var validSet []*TimeValue + dur := t.g.duration() for _, v := range t.data { - validSet = append(validSet, &TimeValue{ - Timeframe: v.Timeframe, - Amount: v.Amount, - }) + timeMap[v.Timeframe] = v.Amount } - return validSet -} + currentStr := types.FixedTime(current, t.g.duration()) -func (t *TimeVar) FillMissing() []*TimeValue { - timeMap := make(map[time.Time]*TimeValue) - var validSet []*TimeValue - if len(t.data) == 0 { - return nil - } - current := t.data[0].Timeframe - for _, v := range t.data { - timeMap[v.Timeframe] = v - } - maxTime := t.g.query.End for { - amount := int64(0) - if timeMap[current] != nil { - amount = timeMap[current].Amount + var amount float64 + if timeMap[currentStr] != 0 { + amount = timeMap[currentStr] } validSet = append(validSet, &TimeValue{ - Timeframe: current, + Timeframe: currentStr, Amount: amount, }) - if current.After(maxTime) { + if current.After(end) { break } - current = current.Add(t.g.duration()) + current = current.Add(dur) + currentStr = types.FixedTime(current, t.g.duration()) } return validSet @@ -112,18 +138,78 @@ func (t *TimeVar) FillMissing() []*TimeValue { func (g *GroupBy) duration() time.Duration { switch g.query.Group { case "second": - return time.Second + return types.Second case "minute": - return time.Minute + return types.Minute case "hour": - return time.Hour + return types.Hour case "day": - return time.Hour * 24 + return types.Day case "month": - return time.Hour * 730 + return types.Month case "year": - return time.Hour * 8760 + return types.Year default: - return time.Hour + return types.Hour } } + +func ParseQueries(r *http.Request, db Database) *GroupQuery { + fields := parseGet(r) + grouping := fields.Get("group") + if grouping == "" { + grouping = "hour" + } + startField := utils.ToInt(fields.Get("start")) + endField := utils.ToInt(fields.Get("end")) + limit := utils.ToInt(fields.Get("limit")) + offset := utils.ToInt(fields.Get("offset")) + fill, _ := strconv.ParseBool(fields.Get("fill")) + orderBy := fields.Get("order") + if limit == 0 { + limit = 10000 + } + + query := &GroupQuery{ + Start: time.Unix(startField, 0).UTC(), + End: time.Unix(endField, 0).UTC(), + Group: grouping, + Order: orderBy, + Limit: int(limit), + Offset: int(offset), + FillEmpty: fill, + } + + if query.Limit != 0 { + db = db.Limit(query.Limit) + } + if query.Offset > 0 { + db = db.Offset(query.Offset) + } + if !query.Start.IsZero() && !query.End.IsZero() { + db = db.Where("created_at BETWEEN ? AND ?", db.FormatTime(query.Start), db.FormatTime(query.End)) + } else { + if !query.Start.IsZero() { + db = db.Where("created_at > ?", db.FormatTime(query.Start)) + } + if !query.End.IsZero() { + db = db.Where("created_at < ?", db.FormatTime(query.End)) + } + } + if query.Order != "" { + db = db.Order(query.Order) + } + query.db = db.Debug() + + return query +} + +func parseForm(r *http.Request) url.Values { + r.ParseForm() + return r.PostForm +} + +func parseGet(r *http.Request) url.Values { + r.ParseForm() + return r.Form +} diff --git a/database/service.go b/database/service.go index 93207f33..a6917791 100644 --- a/database/service.go +++ b/database/service.go @@ -2,7 +2,7 @@ package database import "github.com/hunterlong/statping/types" -type Service struct { +type ServiceObj struct { db Database service *types.Service } @@ -12,16 +12,16 @@ type Servicer interface { Hits() Database } -func (it *Db) GetService(id int64) (Servicer, error) { +func Service(id int64) (Servicer, error) { var service types.Service - query := it.Model(&types.Service{}).Where("id = ?", id).Find(&service) - return &Service{it, &service}, query.Error() + query := database.Model(&types.Service{}).Where("id = ?", id).Find(&service) + return &ServiceObj{query, &service}, query.Error() } -func (s *Service) Failures() Database { - return s.db.Model(&types.Failure{}).Where("service = ?", s.service.Id) +func (s *ServiceObj) Failures() Database { + return database.Model(&types.Failure{}).Where("service = ?", s.service.Id) } -func (s *Service) Hits() Database { - return s.db.Model(&types.Hit{}).Where("service = ?", s.service.Id) +func (s *ServiceObj) Hits() Database { + return database.Model(&types.Hit{}).Where("service = ?", s.service.Id) } diff --git a/frontend/src/API.js b/frontend/src/API.js index 6ace37f5..c99e0a68 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -37,11 +37,11 @@ class Api { } async service_hits(id, start, end, group) { - return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data)) + return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=true').then(response => (response.data)) } async service_failures_data(id, start, end, group) { - return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data)) + return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=true').then(response => (response.data)) } async service_heatmap(id, start, end, group) { diff --git a/frontend/src/components/Service/ServiceChart.vue b/frontend/src/components/Service/ServiceChart.vue index 83128ef1..44af6d1e 100644 --- a/frontend/src/components/Service/ServiceChart.vue +++ b/frontend/src/components/Service/ServiceChart.vue @@ -132,15 +132,15 @@ }, methods: { async chartHits(group) { - const start = this.nowSubtract((3600 * 24) * 7) + const start = this.nowSubtract((3600 * 24) * 30) this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group) - if (this.data.length === 0 && group !== "minute") { - await this.chartHits("minute") + if (this.data.length === 0 && group !== "hour") { + await this.chartHits("hour") } this.series = [{ name: this.service.name, - ...this.data + ...this.convertToChartData(this.data) }] this.ready = true } diff --git a/frontend/src/components/Service/ServiceInfo.vue b/frontend/src/components/Service/ServiceInfo.vue index a0321ae0..21221291 100644 --- a/frontend/src/components/Service/ServiceInfo.vue +++ b/frontend/src/components/Service/ServiceInfo.vue @@ -16,7 +16,7 @@ - Failed {{ago(parseTime(failure.created_at))}}
+ Failed {{failure.created_at}}
{{failure.issue}}
@@ -49,9 +49,9 @@ } }, async mounted() { - this.set1 = await this.getHits(24, "minute") + this.set1 = await this.getHits(24, "hour") this.set1_name = this.calc(this.set1) - this.set2 = await this.getHits(24 * 7, "hour") + this.set2 = await this.getHits(24 * 7, "day") this.set2_name = this.calc(this.set2) this.loaded = true }, @@ -62,15 +62,16 @@ this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5) return [{name: "None", data: []}] } - const data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group) + const fetched = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group) if (!data) { return [{name: "None", data: []}] } - return [{name: "Latency", data: data.data}] + const data = this.convertToChartData(fetched, 1000, true) + return [{name: "Latency", data}] }, calc(s) { let data = s[0].data - if (data.length > 1) { + if (data) { let total = 0 data.forEach((f) => { total += f.y diff --git a/frontend/src/forms/Service.vue b/frontend/src/forms/Service.vue index 2b77b70e..fae1acf3 100644 --- a/frontend/src/forms/Service.vue +++ b/frontend/src/forms/Service.vue @@ -149,7 +149,7 @@
-
@@ -208,6 +208,7 @@ methods: { async saveService() { let s = this.service + this.loading = true delete s.failures delete s.created_at delete s.updated_at @@ -219,6 +220,10 @@ } else { await this.createService(s) } + const services = await Api.services() + this.$store.commit('setServices', services) + this.loading = false + this.$router.push('/dashboard/services') }, async createService(s) { await Api.service_create(s) diff --git a/frontend/src/mixin.js b/frontend/src/mixin.js index 27301f09..3f2d75aa 100644 --- a/frontend/src/mixin.js +++ b/frontend/src/mixin.js @@ -93,5 +93,19 @@ export default Vue.mixin({ return "bars" } }, + convertToChartData(data = [], multiplier=1, asInt=false) { + let newSet = []; + data.forEach((f) => { + let amount = f.amount * multiplier; + if (asInt) { + amount = amount.toFixed(0) + } + newSet.push({ + x: f.timeframe, + y: amount + }) + }) + return {data: newSet} + } } }); diff --git a/go.mod b/go.mod index b8c0df93..f418da33 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/joho/godotenv v1.3.0 github.com/lib/pq v1.2.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/pkg/errors v0.8.1 github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f github.com/russross/blackfriday/v2 v2.0.1 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect diff --git a/handlers/checkin.go b/handlers/checkin.go index 554db403..9d872032 100644 --- a/handlers/checkin.go +++ b/handlers/checkin.go @@ -30,7 +30,7 @@ func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) { checkins := core.AllCheckins() for _, c := range checkins { c.Hits = c.AllHits() - c.Failures = c.LimitedFailures(64) + c.Failures = c.GetFailures(64) } returnJson(checkins, w, r) } @@ -43,7 +43,7 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) { return } checkin.Hits = checkin.LimitedHits(32) - checkin.Failures = checkin.LimitedFailures(32) + checkin.Failures = checkin.GetFailures(32) returnJson(checkin, w, r) } diff --git a/handlers/errors.go b/handlers/errors.go new file mode 100644 index 00000000..be4637e4 --- /dev/null +++ b/handlers/errors.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "encoding/json" + "github.com/pkg/errors" + "net/http" +) + +type Error struct { + err error + code int +} + +func (e Error) Error() string { + return e.err.Error() +} + +var ( + NewError = func(e error) Error { + return Error{ + err: e, + code: http.StatusInternalServerError, + } + } + NotFound = func(err error) Error { + return Error{ + err: errors.Wrap(err, "not found"), + code: http.StatusNotFound, + } + } + Unauthorized = func(e error) Error { + return Error{ + err: e, + code: http.StatusUnauthorized, + } + } +) + +func RespondError(w http.ResponseWriter, err Error) { + output := apiResponse{ + Status: "error", + Error: err.Error(), + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.code) + json.NewEncoder(w).Encode(output) +} diff --git a/handlers/function.go b/handlers/function.go index 2b12dcbd..08832cd7 100644 --- a/handlers/function.go +++ b/handlers/function.go @@ -4,12 +4,23 @@ import ( "github.com/hunterlong/statping/core" "html/template" "net/http" + "net/url" ) var ( basePath = "/" ) +func parseForm(r *http.Request) url.Values { + r.ParseForm() + return r.PostForm +} + +func parseGet(r *http.Request) url.Values { + r.ParseForm() + return r.Form +} + var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap { return template.FuncMap{ "VERSION": func() string { diff --git a/handlers/query.go b/handlers/query.go new file mode 100644 index 00000000..5ac8282f --- /dev/null +++ b/handlers/query.go @@ -0,0 +1 @@ +package handlers diff --git a/handlers/services.go b/handlers/services.go index 5d995d91..dcbdec43 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/gorilla/mux" "github.com/hunterlong/statping/core" + "github.com/hunterlong/statping/database" "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "net/http" @@ -151,56 +152,41 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) { func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service := core.SelectService(utils.ToInt(vars["id"])) - if service == nil { + service, err := database.Service(utils.ToInt(vars["id"])) + if err != nil { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := parseGroupQuery(r) - obj := core.GraphHitsDataRaw(service, groupQuery, "latency") + groupQuery := database.ParseQueries(r, service.Hits()) + + obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency")) returnJson(obj, w, r) } func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service := core.SelectService(utils.ToInt(vars["id"])) - if service == nil { + service, err := database.Service(utils.ToInt(vars["id"])) + if err != nil { sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := parseGroupQuery(r) + groupQuery := database.ParseQueries(r, service.Failures()) - obj := core.GraphFailuresDataRaw(service, groupQuery) + obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount) returnJson(obj, w, r) } -func parseGroupQuery(r *http.Request) *types.GroupQuery { - fields := parseGet(r) - grouping := fields.Get("group") - if grouping == "" { - grouping = "hour" - } - startField := utils.ToInt(fields.Get("start")) - endField := utils.ToInt(fields.Get("end")) - - return &types.GroupQuery{ - Start: time.Unix(startField, 0).UTC(), - End: time.Unix(endField, 0).UTC(), - Group: grouping, - } -} - func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) - service := core.SelectService(utils.ToInt(vars["id"])) - if service == nil { - sendErrorJson(errors.New("service not found"), w, r) + service, err := database.Service(utils.ToInt(vars["id"])) + if err != nil { + sendErrorJson(errors.New("service data not found"), w, r) return } - groupQuery := parseGroupQuery(r) + groupQuery := database.ParseQueries(r, service.Hits()) - obj := core.GraphHitsDataRaw(service, groupQuery, "ping_time") + obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time")) returnJson(obj, w, r) } @@ -301,24 +287,26 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) { func apiServiceFailuresHandler(r *http.Request) interface{} { vars := mux.Vars(r) - servicer := core.SelectService(utils.ToInt(vars["id"])) - if servicer == nil { + + service, err := database.Service(utils.ToInt(vars["id"])) + if err != nil { return errors.New("service not found") } - fails := servicer.LimitedFailures(100) + + var fails []types.Failure + service.Failures().Requests(r).Find(&fails) return fails } func apiServiceHitsHandler(r *http.Request) interface{} { vars := mux.Vars(r) - servicer := core.SelectService(utils.ToInt(vars["id"])) - if servicer == nil { + service, err := database.Service(utils.ToInt(vars["id"])) + if err != nil { return errors.New("service not found") } - hits, err := servicer.HitsDb(r).Hits() - if err != nil { - return err - } + + var hits []types.Hit + service.Hits().Find(&hits) return hits } diff --git a/handlers/settings.go b/handlers/settings.go deleted file mode 100644 index 1a446dc4..00000000 --- a/handlers/settings.go +++ /dev/null @@ -1,31 +0,0 @@ -// Statping -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statping -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package handlers - -import ( - "net/http" - "net/url" -) - -func parseForm(r *http.Request) url.Values { - r.ParseForm() - return r.PostForm -} - -func parseGet(r *http.Request) url.Values { - r.ParseForm() - return r.Form -} diff --git a/types/checkin.go b/types/checkin.go index 71bbbe42..0c924362 100644 --- a/types/checkin.go +++ b/types/checkin.go @@ -21,19 +21,19 @@ 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" json:"id"` - ServiceId int64 `gorm:"index;column:service" json:"service_id"` - Name string `gorm:"column:name" json:"name"` - Interval int64 `gorm:"column:check_interval" json:"interval"` - GracePeriod int64 `gorm:"column:grace_period" json:"grace"` - ApiKey string `gorm:"column:api_key" json:"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:"-"` - Failing bool `gorm:"-" json:"failing"` - LastHit time.Time `gorm:"-" json:"last_hit"` - Hits []*CheckinHit `gorm:"-" json:"hits"` - Failures []FailureInterface `gorm:"-" json:"failures"` + Id int64 `gorm:"primary_key;column:id" json:"id"` + ServiceId int64 `gorm:"index;column:service" json:"service_id"` + Name string `gorm:"column:name" json:"name"` + Interval int64 `gorm:"column:check_interval" json:"interval"` + GracePeriod int64 `gorm:"column:grace_period" json:"grace"` + ApiKey string `gorm:"column:api_key" json:"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:"-"` + Failing bool `gorm:"-" json:"failing"` + LastHit time.Time `gorm:"-" json:"last_hit"` + Hits []*CheckinHit `gorm:"-" json:"hits"` + Failures []*Failure `gorm:"-" json:"failures"` } type CheckinInterface interface { diff --git a/types/failure.go b/types/failure.go index 75aa8eb2..a0f8c0da 100644 --- a/types/failure.go +++ b/types/failure.go @@ -35,8 +35,6 @@ type Failure struct { type FailureInterface interface { Select() *Failure - Ago() string // Ago returns a human readable timestamp - ParseError() string // ParseError returns a human readable error for a service failure } // BeforeCreate for Failure will set CreatedAt to UTC diff --git a/types/time.go b/types/time.go index 30510b92..86c7f4e4 100644 --- a/types/time.go +++ b/types/time.go @@ -16,6 +16,7 @@ package types import ( + "fmt" "time" ) @@ -27,11 +28,41 @@ const ( ) var ( - NOW = func() time.Time { return time.Now().UTC() }() - //HOUR_1_AGO = time.Now().Add(-1 * time.Hour) - //HOUR_24_AGO = time.Now().Add(-24 * time.Hour) - //HOUR_72_AGO = time.Now().Add(-72 * time.Hour) - //DAY_7_AGO = NOW.AddDate(0, 0, -7) - //MONTH_1_AGO = NOW.AddDate(0, -1, 0) - //YEAR_1_AGO = NOW.AddDate(-1, 0, 0) + Second = time.Second + Minute = time.Minute + Hour = time.Hour + Day = Hour * 24 + Week = Day * 7 + Month = Week * 4 + Year = Day * 365 ) + +func FixedTime(t time.Time, d time.Duration) string { + switch d { + case Month: + month := fmt.Sprintf("%v", int(t.Month())) + if int(t.Month()) < 10 { + month = fmt.Sprintf("0%v", int(t.Month())) + } + return fmt.Sprintf("%v-%v-01T00:00:00Z", t.Year(), month) + case Year: + return fmt.Sprintf("%v-01-01T00:00:00Z", t.Year()) + default: + return t.Format(durationStr(d)) + } +} + +func durationStr(d time.Duration) string { + switch d { + case Second: + return "2006-01-02T15:04:05Z" + case Minute: + return "2006-01-02T15:04:00Z" + case Hour: + return "2006-01-02T15:00:00Z" + case Day: + return "2006-01-02T00:00:00Z" + default: + return "2006-01-02T00:00:00Z" + } +} diff --git a/types/time_test.go b/types/time_test.go new file mode 100644 index 00000000..2bb9e916 --- /dev/null +++ b/types/time_test.go @@ -0,0 +1,46 @@ +package types + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestFixedTime(t *testing.T) { + + timeVal, err := time.Parse("2006-01-02T15:04:05Z", "2020-05-22T06:02:13Z") + require.Nil(t, err) + + examples := []struct { + Time time.Time + Duration time.Duration + Expected string + }{{ + timeVal, + time.Second, + "2020-05-22T06:02:13Z", + }, { + timeVal, + time.Minute, + "2020-05-22T06:02:00Z", + }, { + timeVal, + time.Hour, + "2020-05-22T06:00:00Z", + }, { + timeVal, + time.Hour * 24, + "2020-05-22T00:00:00Z", + }, { + timeVal, + time.Hour * 24 * 30, + "2020-05-01T00:00:00Z", + }} + + for _, e := range examples { + assert.Equal(t, e.Expected, FixedTime(e.Time, e.Duration), fmt.Sprintf("reformating for: %v %v", e.Time, e.Duration)) + } + +} diff --git a/types/types.go b/types/types.go index 86f3caf4..5792b7c0 100644 --- a/types/types.go +++ b/types/types.go @@ -19,13 +19,6 @@ import ( "time" ) -type GroupQuery struct { - Id int64 - Start time.Time - End time.Time - Group string -} - // Hit struct is a 'successful' ping or web response entry for a service. type Hit struct { Id int64 `gorm:"primary_key;column:id" json:"id"` @@ -49,7 +42,7 @@ type DbConfig struct { DbHost string `yaml:"host" json:"-"` DbUser string `yaml:"user" json:"-"` DbPass string `yaml:"password" json:"-"` - DbData string `yaml:"Db" json:"-"` + DbData string `yaml:"database" json:"-"` DbPort int64 `yaml:"port" json:"-"` ApiKey string `yaml:"api_key" json:"-"` ApiSecret string `yaml:"api_secret" json:"-"`