mirror of https://github.com/statping/statping
pull/429/head
parent
871424f9c3
commit
f64fc9e682
|
@ -20,7 +20,6 @@ import (
|
||||||
"github.com/ararog/timeago"
|
"github.com/ararog/timeago"
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
"github.com/hunterlong/statping/utils"
|
"github.com/hunterlong/statping/utils"
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,8 +93,8 @@ func (c *Checkin) CreateFailure() (int64, error) {
|
||||||
PingTime: c.Expected().Seconds(),
|
PingTime: c.Expected().Seconds(),
|
||||||
}}
|
}}
|
||||||
row := Database(&Failure{}).Create(&fail)
|
row := Database(&Failure{}).Create(&fail)
|
||||||
sort.Sort(types.FailSort(c.Failures))
|
//sort.Sort(types.FailSort(c.Failures))
|
||||||
c.Failures = append(c.Failures, fail)
|
//c.Failures = append(c.Failures, fail)
|
||||||
if len(c.Failures) > limitedFailures {
|
if len(c.Failures) > limitedFailures {
|
||||||
c.Failures = c.Failures[1:]
|
c.Failures = c.Failures[1:]
|
||||||
}
|
}
|
||||||
|
@ -167,23 +166,27 @@ func (c *Checkin) AllHits() []*types.CheckinHit {
|
||||||
return checkins
|
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
|
// Hits returns all of the CheckinHits for a given Checkin
|
||||||
func (c *Checkin) AllFailures() []*types.Failure {
|
func (c *Checkin) AllFailures() []*types.Failure {
|
||||||
var failures []*types.Failure
|
var failures []*types.Failure
|
||||||
col := Database(&types.Failure{}).Where("checkin = ?", c.Id).Where("method = 'checkin'").Order("id desc")
|
Database(&types.Failure{}).
|
||||||
col.Find(&failures)
|
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
|
return failures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ararog/timeago"
|
"github.com/ararog/timeago"
|
||||||
"github.com/hunterlong/statping/database"
|
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
"net/http"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -62,10 +60,6 @@ func (s *Service) AllFailures() []types.Failure {
|
||||||
return fails
|
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
|
// DeleteFailures will delete all failures for a service
|
||||||
func (s *Service) DeleteFailures() {
|
func (s *Service) DeleteFailures() {
|
||||||
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
|
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
|
||||||
|
@ -75,13 +69,6 @@ func (s *Service) DeleteFailures() {
|
||||||
s.Failures = nil
|
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
|
// LimitedFailures will return the last amount of failures from a service
|
||||||
func (s *Service) LimitedCheckinFailures(amount int) []*Failure {
|
func (s *Service) LimitedCheckinFailures(amount int) []*Failure {
|
||||||
var failArr []*Failure
|
var failArr []*Failure
|
||||||
|
|
|
@ -47,13 +47,13 @@ func (s *Service) CountHits() (int64, error) {
|
||||||
// Hits returns all successful hits for a service
|
// Hits returns all successful hits for a service
|
||||||
func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) {
|
func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) {
|
||||||
var hits []*types.Hit
|
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)
|
err := col.Find(&hits)
|
||||||
return hits, err.Error()
|
return hits, err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HitsDb(r *http.Request) database.Database {
|
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
|
// Hits returns all successful hits for a service
|
||||||
|
|
134
core/services.go
134
core/services.go
|
@ -16,9 +16,7 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ararog/timeago"
|
|
||||||
"github.com/hunterlong/statping/core/notifier"
|
"github.com/hunterlong/statping/core/notifier"
|
||||||
"github.com/hunterlong/statping/database"
|
"github.com/hunterlong/statping/database"
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
|
@ -58,6 +56,13 @@ func SelectService(id int64) *Service {
|
||||||
return nil
|
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 {
|
func (s *Service) UpdateStats() *Service {
|
||||||
s.Online24Hours = s.OnlineDaysPercent(1)
|
s.Online24Hours = s.OnlineDaysPercent(1)
|
||||||
s.Online7Days = s.OnlineDaysPercent(7)
|
s.Online7Days = s.OnlineDaysPercent(7)
|
||||||
|
@ -108,7 +113,8 @@ func (s *Service) AllCheckins() []*Checkin {
|
||||||
return 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) {
|
func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
|
||||||
var services []*Service
|
var services []*Service
|
||||||
db := Database(&Service{}).Find(&services).Order("order_id desc")
|
db := Database(&Service{}).Find(&services).Order("order_id desc")
|
||||||
|
@ -122,13 +128,13 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
|
||||||
service.Start()
|
service.Start()
|
||||||
service.CheckinProcess()
|
service.CheckinProcess()
|
||||||
}
|
}
|
||||||
fails := service.LimitedFailures(limitedFailures)
|
fails := service.GetFailures(limitedFailures)
|
||||||
for _, f := range fails {
|
for _, f := range fails {
|
||||||
service.Failures = append(service.Failures, f)
|
service.Failures = append(service.Failures, f)
|
||||||
}
|
}
|
||||||
checkins := service.AllCheckins()
|
checkins := service.AllCheckins()
|
||||||
for _, c := range checkins {
|
for _, c := range checkins {
|
||||||
c.Failures = c.LimitedFailures(limitedFailures)
|
c.Failures = c.GetFailures(limitedFailures)
|
||||||
c.Hits = c.LimitedHits(limitedHits)
|
c.Hits = c.LimitedHits(limitedHits)
|
||||||
service.Checkins = append(service.Checkins, c)
|
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
|
// lastFailure returns the last Failure a service had
|
||||||
func (s *Service) lastFailure() *Failure {
|
func (s *Service) lastFailure() types.FailureInterface {
|
||||||
limited := s.LimitedFailures(1)
|
limited := s.GetFailures(1)
|
||||||
if len(limited) == 0 {
|
if len(limited) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -194,29 +200,6 @@ func (s *Service) lastFailure() *Failure {
|
||||||
return last
|
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
|
// DowntimeText will return the amount of downtime for a service based on the duration
|
||||||
// service.DowntimeText()
|
// service.DowntimeText()
|
||||||
// // Service has been offline for 15 minutes
|
// // Service has been offline for 15 minutes
|
||||||
|
@ -261,99 +244,24 @@ func (s *Service) Downtime() time.Duration {
|
||||||
return time.Duration(0)
|
return time.Duration(0)
|
||||||
}
|
}
|
||||||
if len(hits) == 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
|
return since
|
||||||
}
|
}
|
||||||
|
|
||||||
// DateScanObj struct is for creating the charts.js graph JSON array
|
// GraphData will return all hits or failures
|
||||||
type DateScanObj struct {
|
func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*database.TimeValue {
|
||||||
Array []*database.DateScan `json:"data"`
|
dbQuery, err := q.Database().GroupQuery(q, by).ToTimeValue(dbType)
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return dbQuery.FillMissing()
|
if q.FillEmpty {
|
||||||
}
|
return dbQuery.FillMissing(q.Start, q.End)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
return dbQuery.FillMissing()
|
return dbQuery.ToValues()
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// index returns a services index int for updating the []*core.Services slice
|
// index returns a services index int for updating the []*core.Services slice
|
||||||
|
|
|
@ -127,12 +127,6 @@ func TestServiceOnline24Hours(t *testing.T) {
|
||||||
assert.True(t, service3.OnlineSince(since) > float32(49))
|
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) {
|
func TestServiceAvgUptime(t *testing.T) {
|
||||||
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
|
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
|
||||||
service := SelectService(1)
|
service := SelectService(1)
|
||||||
|
|
|
@ -2,14 +2,12 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +19,10 @@ const (
|
||||||
TIME_DAY = "2006-01-02"
|
TIME_DAY = "2006-01-02"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
database Database
|
||||||
|
)
|
||||||
|
|
||||||
// Database is an interface which DB implements
|
// Database is an interface which DB implements
|
||||||
type Database interface {
|
type Database interface {
|
||||||
Close() error
|
Close() error
|
||||||
|
@ -99,11 +101,9 @@ type Database interface {
|
||||||
// extra
|
// extra
|
||||||
Error() error
|
Error() error
|
||||||
RowsAffected() int64
|
RowsAffected() int64
|
||||||
QuerySearch(*http.Request) Database
|
|
||||||
|
|
||||||
Since(time.Time) Database
|
Since(time.Time) Database
|
||||||
Between(time.Time, time.Time) Database
|
Between(time.Time, time.Time) Database
|
||||||
Hits() ([]*types.Hit, error)
|
|
||||||
ToChart() ([]*DateScan, error)
|
ToChart() ([]*DateScan, error)
|
||||||
|
|
||||||
SelectByTime(string) string
|
SelectByTime(string) string
|
||||||
|
@ -112,7 +112,14 @@ type Database interface {
|
||||||
FormatTime(t time.Time) string
|
FormatTime(t time.Time) string
|
||||||
ParseTime(t string) (time.Time, error)
|
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 {
|
func (it *Db) MultipleSelects(args ...string) Database {
|
||||||
|
@ -120,10 +127,6 @@ func (it *Db) MultipleSelects(args ...string) Database {
|
||||||
return it.Select(joined)
|
return it.Select(joined)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountAmount() string {
|
|
||||||
return fmt.Sprintf("COUNT(id) as amount")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Db struct {
|
type Db struct {
|
||||||
Database *gorm.DB
|
Database *gorm.DB
|
||||||
Type string
|
Type string
|
||||||
|
@ -132,7 +135,11 @@ type Db struct {
|
||||||
// Openw is a drop-in replacement for Open()
|
// Openw is a drop-in replacement for Open()
|
||||||
func Openw(dialect string, args ...interface{}) (db Database, err error) {
|
func Openw(dialect string, args ...interface{}) (db Database, err error) {
|
||||||
gormdb, err := gorm.Open(dialect, args...)
|
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
|
// Wrap wraps gorm.DB in an interface
|
||||||
|
@ -460,8 +467,8 @@ type DateScan struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeValue struct {
|
type TimeValue struct {
|
||||||
Timeframe time.Time `json:"timeframe"`
|
Timeframe string `json:"timeframe"`
|
||||||
Amount int64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *Db) ToChart() ([]*DateScan, error) {
|
func (it *Db) ToChart() ([]*DateScan, error) {
|
||||||
|
@ -487,55 +494,3 @@ func (it *Db) ToChart() ([]*DateScan, error) {
|
||||||
}
|
}
|
||||||
return data, err
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,42 +3,60 @@ package database
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
|
"github.com/hunterlong/statping/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupBy struct {
|
type GroupBy struct {
|
||||||
db Database
|
db Database
|
||||||
query *types.GroupQuery
|
query *GroupQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupByer interface {
|
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 (
|
var (
|
||||||
ByCount = func() GroupMethod {
|
ByCount = By("COUNT(id) as amount")
|
||||||
return fmt.Sprintf("COUNT(id) as amount")
|
BySum = func(column string) By {
|
||||||
|
return By(fmt.Sprintf("SUM(%s) as amount", column))
|
||||||
}
|
}
|
||||||
BySum = func(column string) GroupMethod {
|
ByAverage = func(column string) By {
|
||||||
return fmt.Sprintf("SUM(%s) as amount", column)
|
return By(fmt.Sprintf("AVG(%s) as amount", column))
|
||||||
}
|
|
||||||
ByAverage = func(column string) GroupMethod {
|
|
||||||
return fmt.Sprintf("SUM(%s) as amount", column)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func execute(db Database, query *types.GroupQuery) Database {
|
func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer {
|
||||||
return db.MultipleSelects(
|
dbQuery := db.MultipleSelects(
|
||||||
db.SelectByTime(query.Group),
|
db.SelectByTime(q.Group),
|
||||||
CountAmount(),
|
by.String(),
|
||||||
).Between(query.Start, query.End).Group("timeframe").Debug()
|
).Group("timeframe")
|
||||||
}
|
|
||||||
|
|
||||||
func (db *Db) GroupQuery(query *types.GroupQuery) GroupByer {
|
return &GroupBy{dbQuery, q}
|
||||||
return &GroupBy{execute(db, query), query}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimeVar struct {
|
type TimeVar struct {
|
||||||
|
@ -46,64 +64,72 @@ type TimeVar struct {
|
||||||
data []*TimeValue
|
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()
|
rows, err := g.db.Rows()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var data []*TimeValue
|
var data []*TimeValue
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var timeframe string
|
var timeframe time.Time
|
||||||
var amount int64
|
amount := float64(0)
|
||||||
if err := rows.Scan(&timeframe, &amount); err != nil {
|
rows.Scan(&timeframe, &amount)
|
||||||
return nil, err
|
newTs := types.FixedTime(timeframe, g.duration())
|
||||||
}
|
|
||||||
createdTime, _ := g.db.ParseTime(timeframe)
|
|
||||||
data = append(data, &TimeValue{
|
data = append(data, &TimeValue{
|
||||||
Timeframe: createdTime,
|
Timeframe: newTs,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
return &TimeVar{g, data}, nil
|
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
|
var validSet []*TimeValue
|
||||||
|
dur := t.g.duration()
|
||||||
for _, v := range t.data {
|
for _, v := range t.data {
|
||||||
validSet = append(validSet, &TimeValue{
|
timeMap[v.Timeframe] = v.Amount
|
||||||
Timeframe: v.Timeframe,
|
|
||||||
Amount: 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 {
|
for {
|
||||||
amount := int64(0)
|
var amount float64
|
||||||
if timeMap[current] != nil {
|
if timeMap[currentStr] != 0 {
|
||||||
amount = timeMap[current].Amount
|
amount = timeMap[currentStr]
|
||||||
}
|
}
|
||||||
validSet = append(validSet, &TimeValue{
|
validSet = append(validSet, &TimeValue{
|
||||||
Timeframe: current,
|
Timeframe: currentStr,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
})
|
})
|
||||||
if current.After(maxTime) {
|
if current.After(end) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
current = current.Add(t.g.duration())
|
current = current.Add(dur)
|
||||||
|
currentStr = types.FixedTime(current, t.g.duration())
|
||||||
}
|
}
|
||||||
|
|
||||||
return validSet
|
return validSet
|
||||||
|
@ -112,18 +138,78 @@ func (t *TimeVar) FillMissing() []*TimeValue {
|
||||||
func (g *GroupBy) duration() time.Duration {
|
func (g *GroupBy) duration() time.Duration {
|
||||||
switch g.query.Group {
|
switch g.query.Group {
|
||||||
case "second":
|
case "second":
|
||||||
return time.Second
|
return types.Second
|
||||||
case "minute":
|
case "minute":
|
||||||
return time.Minute
|
return types.Minute
|
||||||
case "hour":
|
case "hour":
|
||||||
return time.Hour
|
return types.Hour
|
||||||
case "day":
|
case "day":
|
||||||
return time.Hour * 24
|
return types.Day
|
||||||
case "month":
|
case "month":
|
||||||
return time.Hour * 730
|
return types.Month
|
||||||
case "year":
|
case "year":
|
||||||
return time.Hour * 8760
|
return types.Year
|
||||||
default:
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package database
|
||||||
|
|
||||||
import "github.com/hunterlong/statping/types"
|
import "github.com/hunterlong/statping/types"
|
||||||
|
|
||||||
type Service struct {
|
type ServiceObj struct {
|
||||||
db Database
|
db Database
|
||||||
service *types.Service
|
service *types.Service
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,16 @@ type Servicer interface {
|
||||||
Hits() Database
|
Hits() Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *Db) GetService(id int64) (Servicer, error) {
|
func Service(id int64) (Servicer, error) {
|
||||||
var service types.Service
|
var service types.Service
|
||||||
query := it.Model(&types.Service{}).Where("id = ?", id).Find(&service)
|
query := database.Model(&types.Service{}).Where("id = ?", id).Find(&service)
|
||||||
return &Service{it, &service}, query.Error()
|
return &ServiceObj{query, &service}, query.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Failures() Database {
|
func (s *ServiceObj) Failures() Database {
|
||||||
return s.db.Model(&types.Failure{}).Where("service = ?", s.service.Id)
|
return database.Model(&types.Failure{}).Where("service = ?", s.service.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Hits() Database {
|
func (s *ServiceObj) Hits() Database {
|
||||||
return s.db.Model(&types.Hit{}).Where("service = ?", s.service.Id)
|
return database.Model(&types.Hit{}).Where("service = ?", s.service.Id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,11 +37,11 @@ class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
async service_hits(id, start, end, group) {
|
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) {
|
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) {
|
async service_heatmap(id, start, end, group) {
|
||||||
|
|
|
@ -132,15 +132,15 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async chartHits(group) {
|
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)
|
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group)
|
||||||
|
|
||||||
if (this.data.length === 0 && group !== "minute") {
|
if (this.data.length === 0 && group !== "hour") {
|
||||||
await this.chartHits("minute")
|
await this.chartHits("hour")
|
||||||
}
|
}
|
||||||
this.series = [{
|
this.series = [{
|
||||||
name: this.service.name,
|
name: this.service.name,
|
||||||
...this.data
|
...this.convertToChartData(this.data)
|
||||||
}]
|
}]
|
||||||
this.ready = true
|
this.ready = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
|
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
|
||||||
Failed {{ago(parseTime(failure.created_at))}}<br>
|
Failed {{failure.created_at}}<br>
|
||||||
{{failure.issue}}
|
{{failure.issue}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
@ -49,9 +49,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.set1 = await this.getHits(24, "minute")
|
this.set1 = await this.getHits(24, "hour")
|
||||||
this.set1_name = this.calc(this.set1)
|
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.set2_name = this.calc(this.set2)
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
|
@ -62,15 +62,16 @@
|
||||||
this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5)
|
this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5)
|
||||||
return [{name: "None", data: []}]
|
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) {
|
if (!data) {
|
||||||
return [{name: "None", data: []}]
|
return [{name: "None", data: []}]
|
||||||
}
|
}
|
||||||
return [{name: "Latency", data: data.data}]
|
const data = this.convertToChartData(fetched, 1000, true)
|
||||||
|
return [{name: "Latency", data}]
|
||||||
},
|
},
|
||||||
calc(s) {
|
calc(s) {
|
||||||
let data = s[0].data
|
let data = s[0].data
|
||||||
if (data.length > 1) {
|
if (data) {
|
||||||
let total = 0
|
let total = 0
|
||||||
data.forEach((f) => {
|
data.forEach((f) => {
|
||||||
total += f.y
|
total += f.y
|
||||||
|
|
|
@ -149,7 +149,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
|
<button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
|
||||||
{{service.id ? "Update Service" : "Create Service"}}
|
{{service.id ? "Update Service" : "Create Service"}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -208,6 +208,7 @@
|
||||||
methods: {
|
methods: {
|
||||||
async saveService() {
|
async saveService() {
|
||||||
let s = this.service
|
let s = this.service
|
||||||
|
this.loading = true
|
||||||
delete s.failures
|
delete s.failures
|
||||||
delete s.created_at
|
delete s.created_at
|
||||||
delete s.updated_at
|
delete s.updated_at
|
||||||
|
@ -219,6 +220,10 @@
|
||||||
} else {
|
} else {
|
||||||
await this.createService(s)
|
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) {
|
async createService(s) {
|
||||||
await Api.service_create(s)
|
await Api.service_create(s)
|
||||||
|
|
|
@ -93,5 +93,19 @@ export default Vue.mixin({
|
||||||
return "bars"
|
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}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -24,6 +24,7 @@ require (
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
github.com/lib/pq v1.2.0 // indirect
|
github.com/lib/pq v1.2.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // 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/rendon/testcli v0.0.0-20161027181003-6283090d169f
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
|
|
|
@ -30,7 +30,7 @@ func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
checkins := core.AllCheckins()
|
checkins := core.AllCheckins()
|
||||||
for _, c := range checkins {
|
for _, c := range checkins {
|
||||||
c.Hits = c.AllHits()
|
c.Hits = c.AllHits()
|
||||||
c.Failures = c.LimitedFailures(64)
|
c.Failures = c.GetFailures(64)
|
||||||
}
|
}
|
||||||
returnJson(checkins, w, r)
|
returnJson(checkins, w, r)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkin.Hits = checkin.LimitedHits(32)
|
checkin.Hits = checkin.LimitedHits(32)
|
||||||
checkin.Failures = checkin.LimitedFailures(32)
|
checkin.Failures = checkin.GetFailures(32)
|
||||||
returnJson(checkin, w, r)
|
returnJson(checkin, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -4,12 +4,23 @@ import (
|
||||||
"github.com/hunterlong/statping/core"
|
"github.com/hunterlong/statping/core"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
basePath = "/"
|
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 {
|
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
|
||||||
return template.FuncMap{
|
return template.FuncMap{
|
||||||
"VERSION": func() string {
|
"VERSION": func() string {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
package handlers
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/hunterlong/statping/core"
|
"github.com/hunterlong/statping/core"
|
||||||
|
"github.com/hunterlong/statping/database"
|
||||||
"github.com/hunterlong/statping/types"
|
"github.com/hunterlong/statping/types"
|
||||||
"github.com/hunterlong/statping/utils"
|
"github.com/hunterlong/statping/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -151,56 +152,41 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
|
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
service := core.SelectService(utils.ToInt(vars["id"]))
|
service, err := database.Service(utils.ToInt(vars["id"]))
|
||||||
if service == nil {
|
if err != nil {
|
||||||
sendErrorJson(errors.New("service data not found"), w, r)
|
sendErrorJson(errors.New("service data not found"), w, r)
|
||||||
return
|
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)
|
returnJson(obj, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
|
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
service := core.SelectService(utils.ToInt(vars["id"]))
|
service, err := database.Service(utils.ToInt(vars["id"]))
|
||||||
if service == nil {
|
if err != nil {
|
||||||
sendErrorJson(errors.New("service data not found"), w, r)
|
sendErrorJson(errors.New("service data not found"), w, r)
|
||||||
return
|
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)
|
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) {
|
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
service := core.SelectService(utils.ToInt(vars["id"]))
|
service, err := database.Service(utils.ToInt(vars["id"]))
|
||||||
if service == nil {
|
if err != nil {
|
||||||
sendErrorJson(errors.New("service not found"), w, r)
|
sendErrorJson(errors.New("service data not found"), w, r)
|
||||||
return
|
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)
|
returnJson(obj, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,24 +287,26 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func apiServiceFailuresHandler(r *http.Request) interface{} {
|
func apiServiceFailuresHandler(r *http.Request) interface{} {
|
||||||
vars := mux.Vars(r)
|
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")
|
return errors.New("service not found")
|
||||||
}
|
}
|
||||||
fails := servicer.LimitedFailures(100)
|
|
||||||
|
var fails []types.Failure
|
||||||
|
service.Failures().Requests(r).Find(&fails)
|
||||||
return fails
|
return fails
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiServiceHitsHandler(r *http.Request) interface{} {
|
func apiServiceHitsHandler(r *http.Request) interface{} {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
servicer := core.SelectService(utils.ToInt(vars["id"]))
|
service, err := database.Service(utils.ToInt(vars["id"]))
|
||||||
if servicer == nil {
|
if err != nil {
|
||||||
return errors.New("service not found")
|
return errors.New("service not found")
|
||||||
}
|
}
|
||||||
hits, err := servicer.HitsDb(r).Hits()
|
|
||||||
if err != nil {
|
var hits []types.Hit
|
||||||
return err
|
service.Hits().Find(&hits)
|
||||||
}
|
|
||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
// Statping
|
|
||||||
// Copyright (C) 2018. Hunter Long and the project contributors
|
|
||||||
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -21,19 +21,19 @@ 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 Checkin struct {
|
type Checkin 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"`
|
||||||
Name string `gorm:"column:name" json:"name"`
|
Name string `gorm:"column:name" json:"name"`
|
||||||
Interval int64 `gorm:"column:check_interval" json:"interval"`
|
Interval int64 `gorm:"column:check_interval" json:"interval"`
|
||||||
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
|
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
|
||||||
ApiKey string `gorm:"column:api_key" json:"api_key"`
|
ApiKey string `gorm:"column:api_key" json:"api_key"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
Running chan bool `gorm:"-" json:"-"`
|
Running chan bool `gorm:"-" json:"-"`
|
||||||
Failing bool `gorm:"-" json:"failing"`
|
Failing bool `gorm:"-" json:"failing"`
|
||||||
LastHit time.Time `gorm:"-" json:"last_hit"`
|
LastHit time.Time `gorm:"-" json:"last_hit"`
|
||||||
Hits []*CheckinHit `gorm:"-" json:"hits"`
|
Hits []*CheckinHit `gorm:"-" json:"hits"`
|
||||||
Failures []FailureInterface `gorm:"-" json:"failures"`
|
Failures []*Failure `gorm:"-" json:"failures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckinInterface interface {
|
type CheckinInterface interface {
|
||||||
|
|
|
@ -35,8 +35,6 @@ type Failure struct {
|
||||||
|
|
||||||
type FailureInterface interface {
|
type FailureInterface interface {
|
||||||
Select() *Failure
|
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
|
// BeforeCreate for Failure will set CreatedAt to UTC
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,11 +28,41 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
NOW = func() time.Time { return time.Now().UTC() }()
|
Second = time.Second
|
||||||
//HOUR_1_AGO = time.Now().Add(-1 * time.Hour)
|
Minute = time.Minute
|
||||||
//HOUR_24_AGO = time.Now().Add(-24 * time.Hour)
|
Hour = time.Hour
|
||||||
//HOUR_72_AGO = time.Now().Add(-72 * time.Hour)
|
Day = Hour * 24
|
||||||
//DAY_7_AGO = NOW.AddDate(0, 0, -7)
|
Week = Day * 7
|
||||||
//MONTH_1_AGO = NOW.AddDate(0, -1, 0)
|
Month = Week * 4
|
||||||
//YEAR_1_AGO = NOW.AddDate(-1, 0, 0)
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,13 +19,6 @@ import (
|
||||||
"time"
|
"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.
|
// Hit struct is a 'successful' ping or web response entry for a service.
|
||||||
type Hit struct {
|
type Hit struct {
|
||||||
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
Id int64 `gorm:"primary_key;column:id" json:"id"`
|
||||||
|
@ -49,7 +42,7 @@ type DbConfig struct {
|
||||||
DbHost string `yaml:"host" json:"-"`
|
DbHost string `yaml:"host" json:"-"`
|
||||||
DbUser string `yaml:"user" json:"-"`
|
DbUser string `yaml:"user" json:"-"`
|
||||||
DbPass string `yaml:"password" json:"-"`
|
DbPass string `yaml:"password" json:"-"`
|
||||||
DbData string `yaml:"Db" json:"-"`
|
DbData string `yaml:"database" json:"-"`
|
||||||
DbPort int64 `yaml:"port" json:"-"`
|
DbPort int64 `yaml:"port" json:"-"`
|
||||||
ApiKey string `yaml:"api_key" json:"-"`
|
ApiKey string `yaml:"api_key" json:"-"`
|
||||||
ApiSecret string `yaml:"api_secret" json:"-"`
|
ApiSecret string `yaml:"api_secret" json:"-"`
|
||||||
|
|
Loading…
Reference in New Issue