pull/429/head
hunterlong 2020-02-24 23:41:28 -08:00
parent 7cf239125f
commit 7d097eb16e
37 changed files with 1046 additions and 1165 deletions

View File

@ -19,6 +19,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/hunterlong/statping/core/notifier" "github.com/hunterlong/statping/core/notifier"
"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"
"github.com/tatsushid/go-fastping" "github.com/tatsushid/go-fastping"
@ -32,30 +33,31 @@ import (
// checkServices will start the checking go routine for each service // checkServices will start the checking go routine for each service
func checkServices() { func checkServices() {
log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services))) log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.services)))
for _, ser := range CoreApp.Services { for _, ser := range CoreApp.services {
//go obj.StartCheckins() //go CheckinRoutine()
go ser.CheckQueue(true) go ServiceCheckQueue(ser, true)
} }
} }
// Check will run checkHttp for HTTP services and checkTcp for TCP services // Check will run checkHttp for HTTP services and checkTcp for TCP services
// if record param is set to true, it will add a record into the database. // if record param is set to true, it will add a record into the database.
func (s *Service) Check(record bool) { func CheckService(srv database.Servicer, record bool) {
switch s.Type { switch srv.Model().Type {
case "http": case "http":
s.CheckHttp(record) CheckHttp(srv, record)
case "tcp", "udp": case "tcp", "udp":
s.CheckTcp(record) CheckTcp(srv, record)
case "icmp": case "icmp":
s.CheckIcmp(record) CheckIcmp(srv, record)
} }
} }
// CheckQueue is the main go routine for checking a service // CheckQueue is the main go routine for checking a service
func (s *Service) CheckQueue(record bool) { func ServiceCheckQueue(srv database.Servicer, record bool) {
s := srv.Model()
s.Checkpoint = time.Now() s.Checkpoint = time.Now()
s.SleepDuration = time.Duration((time.Duration(s.Id) * 100) * time.Millisecond) s.SleepDuration = (time.Duration(s.Id) * 100) * time.Millisecond
CheckLoop: CheckLoop:
for { for {
select { select {
@ -63,11 +65,11 @@ CheckLoop:
log.Infoln(fmt.Sprintf("Stopping service: %v", s.Name)) log.Infoln(fmt.Sprintf("Stopping service: %v", s.Name))
break CheckLoop break CheckLoop
case <-time.After(s.SleepDuration): case <-time.After(s.SleepDuration):
s.Check(record) CheckService(srv, record)
s.Checkpoint = s.Checkpoint.Add(s.duration()) s.Checkpoint = s.Checkpoint.Add(srv.Interval())
sleep := s.Checkpoint.Sub(time.Now()) sleep := s.Checkpoint.Sub(time.Now())
if !s.Online { if !s.Online {
s.SleepDuration = s.duration() s.SleepDuration = srv.Interval()
} else { } else {
s.SleepDuration = sleep s.SleepDuration = sleep
} }
@ -77,17 +79,18 @@ CheckLoop:
} }
// duration returns the amount of duration for a service to check its status // duration returns the amount of duration for a service to check its status
func (s *Service) duration() time.Duration { func duration(s database.Servicer) time.Duration {
var amount time.Duration var amount time.Duration
if s.Interval >= 10000 { if s.Interval() >= 10000 {
amount = time.Duration(s.Interval) * time.Microsecond amount = s.Interval() * time.Microsecond
} else { } else {
amount = time.Duration(s.Interval) * time.Second amount = s.Interval() * time.Second
} }
return amount return amount
} }
func (s *Service) parseHost() string { func parseHost(srv database.Servicer) string {
s := srv.Model()
if s.Type == "tcp" || s.Type == "udp" { if s.Type == "tcp" || s.Type == "udp" {
return s.Domain return s.Domain
} else { } else {
@ -100,10 +103,11 @@ func (s *Service) parseHost() string {
} }
// dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took // dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took
func (s *Service) dnsCheck() (float64, error) { func dnsCheck(srv database.Servicer) (float64, error) {
s := srv.Model()
var err error var err error
t1 := time.Now() t1 := time.Now()
host := s.parseHost() host := parseHost(srv)
if s.Type == "tcp" { if s.Type == "tcp" {
_, err = net.LookupHost(host) _, err = net.LookupHost(host)
} else { } else {
@ -122,7 +126,8 @@ func isIPv6(address string) bool {
} }
// checkIcmp will send a ICMP ping packet to the service // checkIcmp will send a ICMP ping packet to the service
func (s *Service) CheckIcmp(record bool) *Service { func CheckIcmp(srv database.Servicer, record bool) *types.Service {
s := srv.Model()
p := fastping.NewPinger() p := fastping.NewPinger()
resolveIP := "ip4:icmp" resolveIP := "ip4:icmp"
if isIPv6(s.Domain) { if isIPv6(s.Domain) {
@ -130,7 +135,7 @@ func (s *Service) CheckIcmp(record bool) *Service {
} }
ra, err := net.ResolveIPAddr(resolveIP, s.Domain) ra, err := net.ResolveIPAddr(resolveIP, s.Domain)
if err != nil { if err != nil {
recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err)) recordFailure(srv, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
return s return s
} }
p.AddIPAddr(ra) p.AddIPAddr(ra)
@ -140,7 +145,7 @@ func (s *Service) CheckIcmp(record bool) *Service {
} }
err = p.Run() err = p.Run()
if err != nil { if err != nil {
recordFailure(s, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err)) recordFailure(srv, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err))
return s return s
} }
s.LastResponse = "" s.LastResponse = ""
@ -148,11 +153,12 @@ func (s *Service) CheckIcmp(record bool) *Service {
} }
// checkTcp will check a TCP service // checkTcp will check a TCP service
func (s *Service) CheckTcp(record bool) *Service { func CheckTcp(srv database.Servicer, record bool) *types.Service {
dnsLookup, err := s.dnsCheck() s := srv.Model()
dnsLookup, err := dnsCheck(srv)
if err != nil { if err != nil {
if record { if record {
recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err)) recordFailure(srv, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err))
} }
return s return s
} }
@ -168,13 +174,13 @@ func (s *Service) CheckTcp(record bool) *Service {
conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second) conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second)
if err != nil { if err != nil {
if record { if record {
recordFailure(s, fmt.Sprintf("Dial Error %v", err)) recordFailure(srv, fmt.Sprintf("Dial Error %v", err))
} }
return s return s
} }
if err := conn.Close(); err != nil { if err := conn.Close(); err != nil {
if record { if record {
recordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err)) recordFailure(srv, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err))
} }
return s return s
} }
@ -188,11 +194,12 @@ func (s *Service) CheckTcp(record bool) *Service {
} }
// checkHttp will check a HTTP service // checkHttp will check a HTTP service
func (s *Service) CheckHttp(record bool) *Service { func CheckHttp(srv database.Servicer, record bool) *types.Service {
dnsLookup, err := s.dnsCheck() s := srv.Model()
dnsLookup, err := dnsCheck(srv)
if err != nil { if err != nil {
if record { if record {
recordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) recordFailure(srv, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
} }
return s return s
} }
@ -217,7 +224,7 @@ func (s *Service) CheckHttp(record bool) *Service {
} }
if err != nil { if err != nil {
if record { if record {
recordFailure(s, fmt.Sprintf("HTTP Error %v", err)) recordFailure(srv, fmt.Sprintf("HTTP Error %v", err))
} }
return s return s
} }
@ -233,14 +240,14 @@ func (s *Service) CheckHttp(record bool) *Service {
} }
if !match { if !match {
if record { if record {
recordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) recordFailure(srv, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected))
} }
return s return s
} }
} }
if s.ExpectedStatus != res.StatusCode { if s.ExpectedStatus != res.StatusCode {
if record { if record {
recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) recordFailure(srv, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus))
} }
return s return s
} }
@ -251,7 +258,7 @@ func (s *Service) CheckHttp(record bool) *Service {
} }
// recordSuccess will create a new 'hit' record in the database for a successful/online service // recordSuccess will create a new 'hit' record in the database for a successful/online service
func recordSuccess(s *Service) { func recordSuccess(s *types.Service) {
s.LastOnline = time.Now().UTC() s.LastOnline = time.Now().UTC()
hit := &types.Hit{ hit := &types.Hit{
Service: s.Id, Service: s.Id,
@ -259,15 +266,16 @@ func recordSuccess(s *Service) {
PingTime: s.PingTime, PingTime: s.PingTime,
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),
} }
s.CreateHit(hit) database.Create(hit)
log.WithFields(utils.ToFields(hit, s.Select())).Infoln(fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000)) log.WithFields(utils.ToFields(hit, s)).Infoln(fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
notifier.OnSuccess(s.Service) notifier.OnSuccess(s)
s.Online = true s.Online = true
s.SuccessNotified = true s.SuccessNotified = true
} }
// recordFailure will create a new 'Failure' record in the database for a offline service // recordFailure will create a new 'Failure' record in the database for a offline service
func recordFailure(s *Service, issue string) { func recordFailure(srv database.Servicer, issue string) {
s := srv.Model()
fail := &types.Failure{ fail := &types.Failure{
Service: s.Id, Service: s.Id,
Issue: issue, Issue: issue,
@ -275,11 +283,11 @@ func recordFailure(s *Service, issue string) {
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),
ErrorCode: s.LastStatusCode, ErrorCode: s.LastStatusCode,
} }
log.WithFields(utils.ToFields(fail, s.Select())). log.WithFields(utils.ToFields(fail, s)).
Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000)) Warnln(fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
s.CreateFailure(fail) database.Create(fail)
s.Online = false s.Online = false
s.SuccessNotified = false s.SuccessNotified = false
s.DownText = s.DowntimeText() s.DownText = srv.DowntimeText()
notifier.OnFailure(s.Service, fail) notifier.OnFailure(s, fail)
} }

View File

@ -18,13 +18,14 @@ 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"
"github.com/hunterlong/statping/utils" "github.com/hunterlong/statping/utils"
"time" "time"
) )
type Checkin struct { type Checkin struct {
*types.Checkin *database.CheckinObj
} }
type CheckinHit struct { type CheckinHit struct {
@ -37,8 +38,9 @@ func (c *Checkin) Select() *types.Checkin {
} }
// Routine for checking if the last Checkin was within its interval // Routine for checking if the last Checkin was within its interval
func (c *Checkin) Routine() { func CheckinRoutine(checkin database.Checkiner) {
if c.Last() == nil { c := checkin.Object()
if c.Hits().Last() == nil {
return return
} }
reCheck := c.Period() reCheck := c.Period()
@ -52,9 +54,10 @@ CheckinLoop:
case <-time.After(reCheck): case <-time.After(reCheck):
log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period()))) log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())))
if c.Expected().Seconds() <= 0 { if c.Expected().Seconds() <= 0 {
issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, c.Last().CreatedAt) issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, c.Hits().Last().CreatedAt)
log.Errorln(issue) log.Errorln(issue)
c.CreateFailure()
CreateCheckinFailure(c)
} }
reCheck = c.Period() reCheck = c.Period()
} }
@ -67,98 +70,44 @@ func (c *Checkin) String() string {
return c.ApiKey return c.ApiKey
} }
// ReturnCheckin converts *types.Checking to *core.Checkin func CreateCheckinFailure(checkin database.Checkiner) (int64, error) {
func ReturnCheckin(c *types.Checkin) *Checkin { c := checkin.Object()
return &Checkin{Checkin: c}
}
// ReturnCheckinHit converts *types.checkinHit to *core.checkinHit
func ReturnCheckinHit(c *types.CheckinHit) *CheckinHit {
return &CheckinHit{CheckinHit: c}
}
func (c *Checkin) Service() *Service {
return SelectService(c.ServiceId)
}
func (c *Checkin) CreateFailure() (int64, error) {
service := c.Service() service := c.Service()
c.Failing = true c.Failing = true
fail := &Failure{&types.Failure{ fail := &types.Failure{
Issue: fmt.Sprintf("Checkin %v was not reported %v ago, it expects a request every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())), Issue: fmt.Sprintf("Checkin %v was not reported %v ago, it expects a request every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())),
Method: "checkin", Method: "checkin",
MethodId: c.Id, MethodId: c.Id,
Service: service.Id, Service: service.Id,
Checkin: c.Id, Checkin: c.Id,
PingTime: c.Expected().Seconds(), PingTime: c.Expected().Seconds(),
}}
row := Database(&Failure{}).Create(&fail)
//sort.Sort(types.FailSort(c.Failures))
//c.Failures = append(c.Failures, fail)
if len(c.Failures) > limitedFailures {
c.Failures = c.Failures[1:]
} }
return fail.Id, row.Error() _, err := database.Create(fail)
} if err != nil {
return 0, err
// LimitedHits will return the last amount of successful hits from a checkin }
func (c *Checkin) LimitedHits(amount int) []*types.CheckinHit { //sort.Sort(types.FailSort(c.Failures()))
var hits []*types.CheckinHit return fail.Id, err
Database(&CheckinHit{}).Where("checkin = ?", c.Id).Order("id desc").Limit(amount).Find(&hits)
return hits
} }
// AllCheckins returns all checkin in system // AllCheckins returns all checkin in system
func AllCheckins() []*Checkin { func AllCheckins() []*database.CheckinObj {
var checkins []*Checkin checkins := database.AllCheckins()
Database(&types.Checkin{}).Find(&checkins)
return checkins return checkins
} }
// SelectCheckin will find a Checkin based on the API supplied // SelectCheckin will find a Checkin based on the API supplied
func SelectCheckin(api string) *Checkin { func SelectCheckin(api string) *Checkin {
for _, s := range Services() { for _, s := range Services() {
for _, c := range s.Select().Checkins { for _, c := range s.AllCheckins() {
if c.Select().ApiKey == api { if c.ApiKey == api {
return c.(*Checkin) return &Checkin{c}
} }
} }
} }
return nil return nil
} }
// Period will return the duration of the Checkin interval
func (c *Checkin) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval))
return duration
}
// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response)
func (c *Checkin) Grace() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod))
return duration
}
// Expected returns the duration of when the serviec should receive a Checkin
func (c *Checkin) Expected() time.Duration {
last := c.Last().CreatedAt
now := utils.Now()
lastDir := now.Sub(last)
sub := time.Duration(c.Period() - lastDir)
return sub
}
// Last returns the last checkinHit for a Checkin
func (c *Checkin) Last() *CheckinHit {
var hit CheckinHit
Database(c).Where("checkin = ?", c.Id).Last(&hit)
return &hit
}
func (c *Checkin) Link() string {
return fmt.Sprintf("%v/checkin/%v", CoreApp.Domain, c.ApiKey)
}
// AllHits returns all of the CheckinHits for a given Checkin // AllHits returns all of the CheckinHits for a given Checkin
func (c *Checkin) AllHits() []*types.CheckinHit { func (c *Checkin) AllHits() []*types.CheckinHit {
var checkins []*types.CheckinHit var checkins []*types.CheckinHit
@ -204,7 +153,7 @@ func (c *Checkin) Delete() error {
// index returns a checkin index int for updating the *checkin.Service slice // index returns a checkin index int for updating the *checkin.Service slice
func (c *Checkin) index() int { func (c *Checkin) index() int {
for k, checkin := range c.Service().Checkins { for k, checkin := range c.Service().Checkins {
if c.Id == checkin.Select().Id { if c.Id == checkin.Model().Id {
return k return k
} }
} }
@ -214,16 +163,16 @@ func (c *Checkin) index() int {
// Create will create a new Checkin // Create will create a new Checkin
func (c *Checkin) Create() (int64, error) { func (c *Checkin) Create() (int64, error) {
c.ApiKey = utils.RandomString(7) c.ApiKey = utils.RandomString(7)
row := Database(c).Create(&c) _, err := database.Create(c)
if row.Error() != nil { if err != nil {
log.Warnln(row.Error()) log.Warnln(err)
return 0, row.Error() return 0, err
} }
service := SelectService(c.ServiceId) service := SelectService(c.ServiceId)
service.Checkins = append(service.Checkins, c) service.Checkins = append(service.Checkins, c)
c.Start() c.Start()
go c.Routine() go CheckinRoutine(c)
return c.Id, row.Error() return c.Id, err
} }
// Update will update a Checkin // Update will update a Checkin

View File

@ -19,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/go-yaml/yaml" "github.com/go-yaml/yaml"
"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"
"io/ioutil" "io/ioutil"
@ -71,7 +72,9 @@ func LoadUsingEnv() (*types.DbConfig, error) {
log.Errorln(err) log.Errorln(err)
return nil, err return nil, err
} }
CoreApp.SaveConfig(Configs) if _, err := CoreApp.SaveConfig(Configs); err != nil {
return nil, err
}
exists := DbSession.HasTable("core") exists := DbSession.HasTable("core")
if !exists { if !exists {
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!")) log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))
@ -91,15 +94,20 @@ func LoadUsingEnv() (*types.DbConfig, error) {
password = "admin" password = "admin"
} }
admin := ReturnUser(&types.User{ admin := &types.User{
Username: username, Username: username,
Password: password, Password: password,
Email: "info@admin.com", Email: "info@admin.com",
Admin: types.NewNullBool(true), Admin: types.NewNullBool(true),
}) }
_, err := admin.Create() if _, err := database.Create(admin); err != nil {
return nil, err
}
if err := SampleData(); err != nil {
return nil, err
}
SampleData()
return Configs, err return Configs, err
} }
return Configs, nil return Configs, nil

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"github.com/hunterlong/statping/core/integrations" "github.com/hunterlong/statping/core/integrations"
"github.com/hunterlong/statping/core/notifier" "github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/notifiers" "github.com/hunterlong/statping/notifiers"
"github.com/hunterlong/statping/source" "github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
@ -34,6 +35,7 @@ type PluginRepos types.PluginRepos
type Core struct { type Core struct {
*types.Core *types.Core
services []database.Servicer
} }
var ( var (
@ -48,7 +50,7 @@ func init() {
// NewCore return a new *core.Core struct // NewCore return a new *core.Core struct
func NewCore() *Core { func NewCore() *Core {
CoreApp = &Core{&types.Core{ CoreApp = &Core{Core: &types.Core{
Started: time.Now().UTC(), Started: time.Now().UTC(),
}, },
} }
@ -65,7 +67,7 @@ func InitApp() {
SelectCore() SelectCore()
InsertNotifierDB() InsertNotifierDB()
InsertIntegratorDB() InsertIntegratorDB()
CoreApp.SelectAllServices(true) SelectAllServices(true)
checkServices() checkServices()
AttachNotifiers() AttachNotifiers()
AddIntegrations() AddIntegrations()
@ -150,16 +152,6 @@ func (c Core) MobileSASS() string {
return source.OpenAsset("scss/mobile.scss") return source.OpenAsset("scss/mobile.scss")
} }
// AllOnline will be true if all services are online
func (c Core) AllOnline() bool {
for _, s := range CoreApp.Services {
if !s.Select().Online {
return false
}
}
return true
}
// SelectCore will return the CoreApp global variable and the settings/configs for Statping // SelectCore will return the CoreApp global variable and the settings/configs for Statping
func SelectCore() (*Core, error) { func SelectCore() (*Core, error) {
if DbSession == nil { if DbSession == nil {
@ -222,9 +214,9 @@ func AddIntegrations() error {
} }
// ServiceOrder will reorder the services based on 'order_id' (Order) // ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []types.ServiceInterface type ServiceOrder []database.Servicer
// Sort interface for resroting the Services in order // Sort interface for resroting the Services in order
func (c ServiceOrder) Len() int { return len(c) } func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].(*Service).Order < c[j].(*Service).Order } func (c ServiceOrder) Less(i, j int) bool { return c[i].Model().Order < c[j].Model().Order }

View File

@ -77,26 +77,6 @@ func Database(obj interface{}) database.Database {
} }
} }
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) database.Database {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return Database(&Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
} else {
return Database(&Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME_DAY), t2.UTC().Format(types.TIME_DAY))
}
}
// FailuresBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) FailuresBetween(t1, t2 time.Time, group string, column string) database.Database {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return Database(&Failure{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
} else {
return Database(&Failure{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME_DAY), t2.UTC().Format(types.TIME_DAY))
}
}
// CloseDB will close the database connection if available // CloseDB will close the database connection if available
func CloseDB() { func CloseDB() {
if DbSession != nil { if DbSession != nil {

View File

@ -16,20 +16,19 @@
package core package core
import ( import (
"encoding/json"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
) )
// ExportChartsJs renders the charts for the index page // ExportChartsJs renders the charts for the index page
type ExportData struct { type ExportData struct {
Core *types.Core `json:"core"` Core *types.Core `json:"core"`
Services []types.ServiceInterface `json:"services"` Services []*types.Service `json:"services"`
Messages []*Message `json:"messages"` Messages []*types.Message `json:"messages"`
Checkins []*Checkin `json:"checkins"` Checkins []*types.Checkin `json:"checkins"`
Users []*User `json:"users"` Users []*types.User `json:"users"`
Groups []*Group `json:"groups"` Groups []*types.Group `json:"groups"`
Notifiers []types.AllNotifiers `json:"notifiers"` Notifiers []types.AllNotifiers `json:"notifiers"`
} }
// ExportSettings will export a JSON file containing all of the settings below: // ExportSettings will export a JSON file containing all of the settings below:
@ -40,21 +39,21 @@ type ExportData struct {
// - Services // - Services
// - Groups // - Groups
// - Messages // - Messages
func ExportSettings() ([]byte, error) { //func ExportSettings() ([]byte, error) {
users, err := SelectAllUsers() // users, err := SelectAllUsers()
messages, err := SelectMessages() // messages, err := SelectMessages()
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
data := ExportData{ // data := ExportData{
Core: CoreApp.Core, // Core: CoreApp.Core,
Notifiers: CoreApp.Notifications, // Notifiers: CoreApp.Notifications,
Checkins: AllCheckins(), // Checkins: database.AllCheckins(),
Users: users, // Users: database.AllUsers(),
Services: CoreApp.Services, // Services: CoreApp.Services,
Groups: SelectGroups(true, true), // Groups: SelectGroups(true, true),
Messages: messages, // Messages: messages,
} // }
export, err := json.Marshal(data) // export, err := json.Marshal(data)
return export, err // return export, err
} //}

View File

@ -16,192 +16,18 @@
package core package core
import ( import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
"sort"
"strings"
"time"
) )
type Failure struct { type Failure struct{}
*types.Failure
}
const ( const (
limitedFailures = 32 limitedFailures = 32
limitedHits = 32 limitedHits = 32
) )
// CreateFailure will create a new Failure record for a service
func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
f.Service = s.Id
row := Database(&types.Failure{}).Create(f)
if row.Error() != nil {
log.Errorln(row.Error())
return 0, row.Error()
}
sort.Sort(types.FailSort(s.Failures))
//s.Failures = append(s.Failures, f)
if len(s.Failures) > limitedFailures {
s.Failures = s.Failures[1:]
}
return f.Id, row.Error()
}
// AllFailures will return all failures attached to a service
func (s *Service) AllFailures() []types.Failure {
var fails []types.Failure
err := Database(&types.Failure{}).Find(&fails)
if err.Error() != nil {
log.Errorln(fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
return nil
}
return fails
}
// DeleteFailures will delete all failures for a service
func (s *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
if err.Error() != nil {
log.Errorln(fmt.Sprintf("failed to delete all failures: %v", err))
}
s.Failures = nil
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedCheckinFailures(amount int) []*Failure {
var failArr []*Failure
Database(&types.Failure{}).Where("service = ?", s.Id).Where("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
}
// Ago returns a human readable timestamp for a Failure
func (f *Failure) Ago() string {
got, _ := timeago.TimeAgoWithTime(time.Now().UTC(), f.CreatedAt)
return got
}
// Select returns a *types.Failure
func (f *Failure) Select() *types.Failure {
return f.Failure
}
// Delete will remove a Failure record from the database // Delete will remove a Failure record from the database
func (f *Failure) Delete() error { func (f *Failure) Delete() error {
db := Database(&types.Failure{}).Delete(f) db := Database(&types.Failure{}).Delete(f)
return db.Error() return db.Error()
} }
// Count24HFailures returns the amount of failures for a service within the last 24 hours
func (c *Core) Count24HFailures() uint64 {
var count uint64
for _, s := range CoreApp.Services {
service := s.(*Service)
fails, _ := service.TotalFailures24()
count += fails
}
return count
}
// CountFailures returns the total count of failures for all services
func CountFailures() uint64 {
var count uint64
err := Database(&types.Failure{}).Count(&count)
if err.Error() != nil {
log.Warnln(err.Error())
return 0
}
return count
}
// TotalFailuresOnDate returns the total amount of failures for a service on a specific time/date
func (s *Service) TotalFailuresOnDate(ago time.Time) (uint64, error) {
var count uint64
date := ago.UTC().Format("2006-01-02 00:00:00")
dateend := ago.UTC().Format("2006-01-02") + " 23:59:59"
rows := Database(&types.Failure{}).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, date, dateend).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error()
}
// TotalFailures24 returns the amount of failures for a service within the last 24 hours
func (s *Service) TotalFailures24() (uint64, error) {
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.TotalFailuresSince(ago)
}
// TotalFailures returns the total amount of failures for a service
func (s *Service) TotalFailures() (uint64, error) {
var count uint64
rows := Database(&types.Failure{}).Where("service = ?", s.Id)
err := rows.Count(&count)
return count, err.Error()
}
// FailuresDaysAgo returns the amount of failures since days ago
func (s *Service) FailuresDaysAgo(days int) uint64 {
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
count, _ := s.TotalFailuresSince(ago)
return count
}
// TotalFailuresSince returns the total amount of failures for a service since a specific time/date
func (s *Service) TotalFailuresSince(ago time.Time) (uint64, error) {
var count uint64
rows := Database(&types.Failure{}).Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Not("method = 'checkin'")
err := rows.Count(&count)
return count, err.Error()
}
// ParseError returns a human readable error for a Failure
func (f *Failure) ParseError() string {
if f.Method == "checkin" {
return fmt.Sprintf("Checkin is Offline")
}
err := strings.Contains(f.Issue, "connection reset by peer")
if err {
return fmt.Sprintf("Connection Reset")
}
err = strings.Contains(f.Issue, "operation timed out")
if err {
return fmt.Sprintf("HTTP Request Timed Out")
}
err = strings.Contains(f.Issue, "x509: certificate is valid")
if err {
return fmt.Sprintf("SSL Certificate invalid")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "no such host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "HTTP Status Code")
if err {
return fmt.Sprintf("Incorrect HTTP Status Code")
}
err = strings.Contains(f.Issue, "connection refused")
if err {
return fmt.Sprintf("Connection Failed")
}
err = strings.Contains(f.Issue, "can't assign requested address")
if err {
return fmt.Sprintf("Unable to Request Address")
}
err = strings.Contains(f.Issue, "no route to host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "i/o timeout")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while reading body")
if err {
return fmt.Sprintf("Timed Out on Response Body")
}
return f.Issue
}

View File

@ -1,72 +1,23 @@
package core package core
import ( import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
"sort" "sort"
"time"
) )
type Group struct { type Group struct {
*types.Group database.Grouper
}
// Delete will remove a group
func (g *Group) Delete() error {
for _, s := range g.Services() {
s.GroupId = 0
s.Update(false)
}
err := Database(&Group{}).Delete(g)
return err.Error()
}
// Create will create a group and insert it into the database
func (g *Group) Create() (int64, error) {
g.CreatedAt = time.Now().UTC()
db := Database(&Group{}).Create(g)
return g.Id, db.Error()
}
// Update will update a group
func (g *Group) Update() (int64, error) {
g.UpdatedAt = time.Now().UTC()
db := Database(&Group{}).Update(g)
return g.Id, db.Error()
}
// Services returns all services belonging to a group
func (g *Group) Services() []*Service {
var services []*Service
for _, s := range Services() {
if s.Select().GroupId == int(g.Id) {
services = append(services, s.(*Service))
}
}
return services
}
// VisibleServices returns all services based on authentication
func (g *Group) VisibleServices(auth bool) []*Service {
var services []*Service
for _, g := range g.Services() {
if !g.Public.Bool {
if auth {
services = append(services, g)
}
} else {
services = append(services, g)
}
}
return services
} }
// SelectGroups returns all groups // SelectGroups returns all groups
func SelectGroups(includeAll bool, auth bool) []*Group { func SelectGroups(includeAll bool, auth bool) []database.Grouper {
var groups []*Group var validGroups []database.Grouper
var validGroups []*Group
Database(&Group{}).Find(&groups).Order("order_id desc") groups := database.AllGroups()
for _, g := range groups { for _, g := range groups {
if !g.Public.Bool { if !g.Model().Public.Bool {
if auth { if auth {
validGroups = append(validGroups, g) validGroups = append(validGroups, g)
} }
@ -76,26 +27,26 @@ func SelectGroups(includeAll bool, auth bool) []*Group {
} }
sort.Sort(GroupOrder(validGroups)) sort.Sort(GroupOrder(validGroups))
if includeAll { if includeAll {
emptyGroup := &Group{&types.Group{Id: 0, Public: types.NewNullBool(true)}} emptyGroup := &Group{}
validGroups = append(validGroups, emptyGroup) validGroups = append(validGroups, emptyGroup)
} }
return validGroups return validGroups
} }
// SelectGroup returns a *core.Group // SelectGroup returns a *core.Group
func SelectGroup(id int64) *Group { func SelectGroup(id int64) *types.Group {
for _, g := range SelectGroups(true, true) { for _, g := range SelectGroups(true, true) {
if g.Id == id { if g.Model().Id == id {
return g return g.Model()
} }
} }
return nil return nil
} }
// GroupOrder will reorder the groups based on 'order_id' (Order) // GroupOrder will reorder the groups based on 'order_id' (Order)
type GroupOrder []*Group type GroupOrder []database.Grouper
// Sort interface for resorting the Groups in order // Sort interface for resorting the Groups in order
func (c GroupOrder) Len() int { return len(c) } func (c GroupOrder) Len() int { return len(c) }
func (c GroupOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c GroupOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c GroupOrder) Less(i, j int) bool { return c[i].Order < c[j].Order } func (c GroupOrder) Less(i, j int) bool { return c[i].Model().Order < c[j].Model().Order }

View File

@ -16,90 +16,9 @@
package core package core
import ( import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
"net/http"
"time"
) )
type Hit struct { type Hit struct {
*types.Hit *types.Hit
} }
// CreateHit will create a new 'hit' record in the database for a successful/online service
func (s *Service) CreateHit(h *types.Hit) (int64, error) {
db := Database(&types.Hit{}).Create(&h)
if db.Error() != nil {
log.Errorln(db.Error())
return 0, db.Error()
}
return h.Id, db.Error()
}
// CountHits returns a int64 for all hits for a service
func (s *Service) CountHits() (int64, error) {
var hits int64
col := Database(&types.Hit{}).Where("service = ?", s.Id)
err := col.Count(&hits)
return hits, err.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).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).Requests(r).Order("id desc")
}
// Hits returns all successful hits for a service
func (s *Service) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
col := Database(&types.Hit{}).Where("service = ?", s.Id).Order("id desc")
err := col.Find(&hits)
return hits, err.Error()
}
// LimitedHits returns the last 1024 successful/online 'hit' records for a service
func (s *Service) LimitedHits(amount int) ([]*types.Hit, error) {
var hits []*types.Hit
col := Database(&types.Hit{}).Where("service = ?", s.Id).Order("id desc").Limit(amount)
err := col.Find(&hits)
return reverseHits(hits), err.Error()
}
// reverseHits will reverse the service's hit slice
func reverseHits(input []*types.Hit) []*types.Hit {
if len(input) == 0 {
return input
}
return append(reverseHits(input[1:]), input[0])
}
// TotalHits returns the total amount of successful hits a service has
func (s *Service) TotalHits() (uint64, error) {
var count uint64
col := Database(&types.Hit{}).Where("service = ?", s.Id).Count(&count)
return count, col.Error()
}
// TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64
rows := Database(&types.Hit{}).Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
return count, rows.Error()
}
// Sum returns the added value Latency for all of the services successful hits.
func (s *Service) Sum() float64 {
var sum float64
rows, _ := Database(&types.Hit{}).Where("service = ?", s.Id).Select("sum(latency) as total").Rows()
for rows.Next() {
rows.Scan(&sum)
}
return sum
}

View File

@ -51,13 +51,6 @@ func SelectMessage(id int64) (*Message, error) {
return &message, db.Error() return &message, db.Error()
} }
func (m *Message) Service() *Service {
if m.ServiceId == 0 {
return nil
}
return SelectService(m.ServiceId)
}
// Create will create a Message and insert it into the database // Create will create a Message and insert it into the database
func (m *Message) Create() (int64, error) { func (m *Message) Create() (int64, error) {
m.CreatedAt = time.Now().UTC() m.CreatedAt = time.Now().UTC()

View File

@ -18,6 +18,7 @@ package core
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statping/core/notifier" "github.com/hunterlong/statping/core/notifier"
"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"
"sync" "sync"
@ -35,7 +36,7 @@ func InsertSampleData() error {
insertSampleGroups() insertSampleGroups()
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC() createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
s1 := ReturnService(&types.Service{ s1 := &types.Service{
Name: "Google", Name: "Google",
Domain: "https://google.com", Domain: "https://google.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -48,8 +49,8 @@ func InsertSampleData() error {
Permalink: types.NewNullString("google"), Permalink: types.NewNullString("google"),
VerifySSL: types.NewNullBool(true), VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s2 := ReturnService(&types.Service{ s2 := &types.Service{
Name: "Statping Github", Name: "Statping Github",
Domain: "https://github.com/hunterlong/statping", Domain: "https://github.com/hunterlong/statping",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -61,8 +62,8 @@ func InsertSampleData() error {
Permalink: types.NewNullString("statping_github"), Permalink: types.NewNullString("statping_github"),
VerifySSL: types.NewNullBool(true), VerifySSL: types.NewNullBool(true),
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s3 := ReturnService(&types.Service{ s3 := &types.Service{
Name: "JSON Users Test", Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users", Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -75,8 +76,8 @@ func InsertSampleData() error {
VerifySSL: types.NewNullBool(true), VerifySSL: types.NewNullBool(true),
GroupId: 2, GroupId: 2,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s4 := ReturnService(&types.Service{ s4 := &types.Service{
Name: "JSON API Tester", Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts", Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201, ExpectedStatus: 201,
@ -91,8 +92,8 @@ func InsertSampleData() error {
VerifySSL: types.NewNullBool(true), VerifySSL: types.NewNullBool(true),
GroupId: 2, GroupId: 2,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s5 := ReturnService(&types.Service{ s5 := &types.Service{
Name: "Google DNS", Name: "Google DNS",
Domain: "8.8.8.8", Domain: "8.8.8.8",
Interval: 20, Interval: 20,
@ -103,13 +104,13 @@ func InsertSampleData() error {
Public: types.NewNullBool(true), Public: types.NewNullBool(true),
GroupId: 1, GroupId: 1,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s1.Create(false) database.Create(s1)
s2.Create(false) database.Create(s2)
s3.Create(false) database.Create(s3)
s4.Create(false) database.Create(s4)
s5.Create(false) database.Create(s5)
insertMessages() insertMessages()
@ -121,85 +122,111 @@ func InsertSampleData() error {
} }
func insertSampleIncidents() error { func insertSampleIncidents() error {
incident1 := &Incident{&types.Incident{ incident1 := &types.Incident{
Title: "Github Downtime", Title: "Github Downtime",
Description: "This is an example of a incident for a service.", Description: "This is an example of a incident for a service.",
ServiceId: 2, ServiceId: 2,
}} }
_, err := incident1.Create() if _, err := database.Create(incident1); err != nil {
return err
}
incidentUpdate1 := &IncidentUpdate{&types.IncidentUpdate{ incidentUpdate1 := &types.IncidentUpdate{
IncidentId: incident1.Id, IncidentId: incident1.Id,
Message: "Github's page for Statping seems to be sending a 501 error.", Message: "Github's page for Statping seems to be sending a 501 error.",
Type: "Investigating", Type: "Investigating",
}} }
_, err = incidentUpdate1.Create() if _, err := database.Create(incidentUpdate1); err != nil {
return err
}
incidentUpdate2 := &IncidentUpdate{&types.IncidentUpdate{ incidentUpdate2 := &types.IncidentUpdate{
IncidentId: incident1.Id, IncidentId: incident1.Id,
Message: "Problem is continuing and we are looking at the issues.", Message: "Problem is continuing and we are looking at the issues.",
Type: "Update", Type: "Update",
}} }
_, err = incidentUpdate2.Create() if _, err := database.Create(incidentUpdate2); err != nil {
return err
}
incidentUpdate3 := &IncidentUpdate{&types.IncidentUpdate{ incidentUpdate3 := &types.IncidentUpdate{
IncidentId: incident1.Id, IncidentId: incident1.Id,
Message: "Github is now back online and everything is working.", Message: "Github is now back online and everything is working.",
Type: "Resolved", Type: "Resolved",
}} }
_, err = incidentUpdate3.Create() if _, err := database.Create(incidentUpdate3); err != nil {
return err
}
return err return nil
} }
func insertSampleGroups() error { func insertSampleGroups() error {
group1 := &Group{&types.Group{ group1 := &types.Group{
Name: "Main Services", Name: "Main Services",
Public: types.NewNullBool(true), Public: types.NewNullBool(true),
Order: 2, Order: 2,
}} }
_, err := group1.Create() if _, err := database.Create(group1); err != nil {
group2 := &Group{&types.Group{ return err
}
group2 := &types.Group{
Name: "Linked Services", Name: "Linked Services",
Public: types.NewNullBool(false), Public: types.NewNullBool(false),
Order: 1, Order: 1,
}} }
_, err = group2.Create() if _, err := database.Create(group2); err != nil {
group3 := &Group{&types.Group{ return err
}
group3 := &types.Group{
Name: "Empty Group", Name: "Empty Group",
Public: types.NewNullBool(false), Public: types.NewNullBool(false),
Order: 3, Order: 3,
}} }
_, err = group3.Create() if _, err := database.Create(group3); err != nil {
return err return err
}
return nil
} }
// insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin // insertSampleCheckins will create 2 checkins with 60 successful hits per Checkin
func insertSampleCheckins() error { func insertSampleCheckins() error {
s1 := SelectService(1) s1 := SelectService(1)
checkin1 := ReturnCheckin(&types.Checkin{ checkin1 := &types.Checkin{
ServiceId: s1.Id, ServiceId: s1.Id,
Interval: 300, Interval: 300,
GracePeriod: 300, GracePeriod: 300,
}) }
checkin1.Update()
if _, err := database.Create(checkin1); err != nil {
return err
}
s2 := SelectService(1) s2 := SelectService(1)
checkin2 := ReturnCheckin(&types.Checkin{ checkin2 := &types.Checkin{
ServiceId: s2.Id, ServiceId: s2.Id,
Interval: 900, Interval: 900,
GracePeriod: 300, GracePeriod: 300,
}) }
checkin2.Update()
if _, err := database.Create(checkin2); err != nil {
return err
}
checkTime := time.Now().UTC().Add(-24 * time.Hour) checkTime := time.Now().UTC().Add(-24 * time.Hour)
for i := 0; i <= 60; i++ { for i := 0; i <= 60; i++ {
checkHit := ReturnCheckinHit(&types.CheckinHit{ checkHit := &types.CheckinHit{
Checkin: checkin1.Id, Checkin: checkin1.Id,
From: "192.168.0.1", From: "192.168.0.1",
CreatedAt: checkTime.UTC(), CreatedAt: checkTime.UTC(),
}) }
checkHit.Create()
if _, err := database.Create(checkHit); err != nil {
return err
}
checkTime = checkTime.Add(10 * time.Minute) checkTime = checkTime.Add(10 * time.Minute)
} }
return nil return nil
@ -250,50 +277,61 @@ func insertSampleCore() error {
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),
UseCdn: types.NewNullBool(false), UseCdn: types.NewNullBool(false),
} }
query := Database(&Core{}).Create(core)
return query.Error() _, err := database.Create(core)
return err
} }
// insertSampleUsers will create 2 admin users for a seed database // insertSampleUsers will create 2 admin users for a seed database
func insertSampleUsers() error { func insertSampleUsers() error {
u2 := ReturnUser(&types.User{ u2 := &types.User{
Username: "testadmin", Username: "testadmin",
Password: "password123", Password: "password123",
Email: "info@betatude.com", Email: "info@betatude.com",
Admin: types.NewNullBool(true), Admin: types.NewNullBool(true),
}) }
u3 := ReturnUser(&types.User{ if _, err := database.Create(u2); err != nil {
return err
}
u3 := &types.User{
Username: "testadmin2", Username: "testadmin2",
Password: "password123", Password: "password123",
Email: "info@adminhere.com", Email: "info@adminhere.com",
Admin: types.NewNullBool(true), Admin: types.NewNullBool(true),
}) }
_, err := u2.Create() if _, err := database.Create(u3); err != nil {
_, err = u3.Create() return err
return err }
return nil
} }
func insertMessages() error { func insertMessages() error {
m1 := ReturnMessage(&types.Message{ m1 := &types.Message{
Title: "Routine Downtime", Title: "Routine Downtime",
Description: "This is an example a upcoming message for a service!", Description: "This is an example a upcoming message for a service!",
ServiceId: 1, ServiceId: 1,
StartOn: time.Now().UTC().Add(15 * time.Minute), StartOn: time.Now().UTC().Add(15 * time.Minute),
EndOn: time.Now().UTC().Add(2 * time.Hour), EndOn: time.Now().UTC().Add(2 * time.Hour),
}) }
if _, err := m1.Create(); err != nil {
if _, err := database.Create(m1); err != nil {
return err return err
} }
m2 := ReturnMessage(&types.Message{
m2 := &types.Message{
Title: "Server Reboot", Title: "Server Reboot",
Description: "This is another example a upcoming message for a service!", Description: "This is another example a upcoming message for a service!",
ServiceId: 3, ServiceId: 3,
StartOn: time.Now().UTC().Add(15 * time.Minute), StartOn: time.Now().UTC().Add(15 * time.Minute),
EndOn: time.Now().UTC().Add(2 * time.Hour), EndOn: time.Now().UTC().Add(2 * time.Hour),
}) }
if _, err := m2.Create(); err != nil {
if _, err := database.Create(m2); err != nil {
return err return err
} }
return nil return nil
@ -317,7 +355,7 @@ func InsertLargeSampleData() error {
return err return err
} }
createdOn := time.Now().UTC().Add((-24 * 90) * time.Hour) createdOn := time.Now().UTC().Add((-24 * 90) * time.Hour)
s6 := ReturnService(&types.Service{ s6 := &types.Service{
Name: "JSON Lint", Name: "JSON Lint",
Domain: "https://jsonlint.com", Domain: "https://jsonlint.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -327,9 +365,13 @@ func InsertLargeSampleData() error {
Timeout: 10, Timeout: 10,
Order: 6, Order: 6,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s7 := ReturnService(&types.Service{ if _, err := database.Create(s6); err != nil {
return err
}
s7 := &types.Service{
Name: "Demo Page", Name: "Demo Page",
Domain: "https://demo.statping.com", Domain: "https://demo.statping.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -339,9 +381,13 @@ func InsertLargeSampleData() error {
Timeout: 15, Timeout: 15,
Order: 7, Order: 7,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s8 := ReturnService(&types.Service{ if _, err := database.Create(s7); err != nil {
return err
}
s8 := &types.Service{
Name: "Golang", Name: "Golang",
Domain: "https://golang.org", Domain: "https://golang.org",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -350,9 +396,13 @@ func InsertLargeSampleData() error {
Method: "GET", Method: "GET",
Timeout: 10, Timeout: 10,
Order: 8, Order: 8,
}) }
s9 := ReturnService(&types.Service{ if _, err := database.Create(s8); err != nil {
return err
}
s9 := &types.Service{
Name: "Santa Monica", Name: "Santa Monica",
Domain: "https://www.santamonica.com", Domain: "https://www.santamonica.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -362,9 +412,13 @@ func InsertLargeSampleData() error {
Timeout: 10, Timeout: 10,
Order: 9, Order: 9,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s10 := ReturnService(&types.Service{ if _, err := database.Create(s9); err != nil {
return err
}
s10 := &types.Service{
Name: "Oeschs Die Dritten", Name: "Oeschs Die Dritten",
Domain: "https://www.oeschs-die-dritten.ch/en/", Domain: "https://www.oeschs-die-dritten.ch/en/",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -374,9 +428,13 @@ func InsertLargeSampleData() error {
Timeout: 10, Timeout: 10,
Order: 10, Order: 10,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s11 := ReturnService(&types.Service{ if _, err := database.Create(s10); err != nil {
return err
}
s11 := &types.Service{
Name: "XS Project - Bochka, Bass, Kolbaser", Name: "XS Project - Bochka, Bass, Kolbaser",
Domain: "https://www.youtube.com/watch?v=VLW1ieY4Izw", Domain: "https://www.youtube.com/watch?v=VLW1ieY4Izw",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -386,9 +444,13 @@ func InsertLargeSampleData() error {
Timeout: 20, Timeout: 20,
Order: 11, Order: 11,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s12 := ReturnService(&types.Service{ if _, err := database.Create(s11); err != nil {
return err
}
s12 := &types.Service{
Name: "Github", Name: "Github",
Domain: "https://github.com/hunterlong", Domain: "https://github.com/hunterlong",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -398,9 +460,13 @@ func InsertLargeSampleData() error {
Timeout: 20, Timeout: 20,
Order: 12, Order: 12,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s13 := ReturnService(&types.Service{ if _, err := database.Create(s12); err != nil {
return err
}
s13 := &types.Service{
Name: "Failing URL", Name: "Failing URL",
Domain: "http://thisdomainisfakeanditsgoingtofail.com", Domain: "http://thisdomainisfakeanditsgoingtofail.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -410,9 +476,13 @@ func InsertLargeSampleData() error {
Timeout: 10, Timeout: 10,
Order: 13, Order: 13,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s14 := ReturnService(&types.Service{ if _, err := database.Create(s13); err != nil {
return err
}
s14 := &types.Service{
Name: "Oesch's die Dritten - Die Jodelsprache", Name: "Oesch's die Dritten - Die Jodelsprache",
Domain: "https://www.youtube.com/watch?v=k3GTxRt4iao", Domain: "https://www.youtube.com/watch?v=k3GTxRt4iao",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -422,9 +492,13 @@ func InsertLargeSampleData() error {
Timeout: 12, Timeout: 12,
Order: 14, Order: 14,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s15 := ReturnService(&types.Service{ if _, err := database.Create(s14); err != nil {
return err
}
s15 := &types.Service{
Name: "Gorm", Name: "Gorm",
Domain: "http://gorm.io/", Domain: "http://gorm.io/",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -434,18 +508,11 @@ func InsertLargeSampleData() error {
Timeout: 12, Timeout: 12,
Order: 15, Order: 15,
CreatedAt: createdOn, CreatedAt: createdOn,
}) }
s6.Create(false) if _, err := database.Create(s15); err != nil {
s7.Create(false) return err
s8.Create(false) }
s9.Create(false)
s10.Create(false)
s11.Create(false)
s12.Create(false)
s13.Create(false)
s14.Create(false)
s15.Create(false)
var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour) var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour)
@ -472,7 +539,7 @@ func insertFailureRecords(since time.Time, amount int) {
CreatedAt: createdAt, CreatedAt: createdAt,
} }
service.CreateFailure(failure) database.Create(failure)
} }
} }
} }
@ -492,7 +559,7 @@ func insertHitRecords(since time.Time, amount int) {
CreatedAt: createdAt.UTC(), CreatedAt: createdAt.UTC(),
Latency: latency, Latency: latency,
} }
service.CreateHit(hit) database.Create(hit)
} }
} }
@ -554,7 +621,7 @@ func TmpRecords(dbFile string) error {
return err return err
} }
log.Infoln("loading all services") log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil { if _, err := SelectAllServices(false); err != nil {
return err return err
} }
if err := AttachNotifiers(); err != nil { if err := AttachNotifiers(); err != nil {
@ -597,7 +664,7 @@ func TmpRecords(dbFile string) error {
return err return err
} }
log.Infoln("loading all services") log.Infoln("loading all services")
if _, err := CoreApp.SelectAllServices(false); err != nil { if _, err := SelectAllServices(false); err != nil {
return err return err
} }
log.Infoln("copying sql database file to: " + tmpSqlFile) log.Infoln("copying sql database file to: " + tmpSqlFile)

View File

@ -20,9 +20,7 @@ import (
"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"
"github.com/hunterlong/statping/utils"
"sort" "sort"
"strconv"
"time" "time"
) )
@ -30,224 +28,63 @@ type Service struct {
*types.Service *types.Service
} }
// Select will return the *types.Service struct for Service type Servicer interface{}
func (s *Service) Select() *types.Service {
return s.Service
}
// ReturnService will convert *types.Service to *core.Service func Services() []database.Servicer {
func ReturnService(s *types.Service) *Service { return CoreApp.services
return &Service{s}
}
func Services() []types.ServiceInterface {
return CoreApp.Services
} }
// SelectService returns a *core.Service from in memory // SelectService returns a *core.Service from in memory
func SelectService(id int64) *Service { func SelectService(id int64) *types.Service {
for _, s := range Services() { for _, s := range Services() {
if s.Select().Id == id { if s.Model().Id == id {
service := s.(*Service) fmt.Println("service: ", s.Model())
service = service.UpdateStats() return s.Model()
return 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)
s.AvgResponse = s.AvgTime()
s.FailuresLast24Hours = s.FailuresDaysAgo(1)
s.LastFailure = s.lastFailure()
return s
}
func SelectServices(auth bool) []*Service {
var validServices []*Service
for _, sr := range CoreApp.Services {
s := sr.(*Service)
if !s.Public.Bool {
if auth {
validServices = append(validServices, s)
}
} else {
validServices = append(validServices, s)
}
}
return validServices
}
// SelectServiceLink returns a *core.Service from the service permalink
func SelectServiceLink(permalink string) *Service {
for _, s := range Services() {
if s.Select().Permalink.String == permalink {
return s.(*Service)
} }
} }
return nil return nil
} }
// CheckinProcess runs the checkin routine for each checkin attached to service // CheckinProcess runs the checkin routine for each checkin attached to service
func (s *Service) CheckinProcess() { func CheckinProcess(s database.Servicer) {
checkins := s.AllCheckins() for _, c := range s.AllCheckins() {
for _, c := range checkins {
c.Start() c.Start()
go c.Routine() go CheckinRoutine(c)
} }
} }
// AllCheckins will return a slice of AllCheckins for a Service
func (s *Service) AllCheckins() []*Checkin {
var checkin []*Checkin
Database(&Checkin{}).Where("service = ?", s.Id).Find(&checkin)
return checkin
}
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services // SelectAllServices returns a slice of *core.Service to be store on []*core.Services
// should only be called once on startup. // should only be called once on startup.
func (c *Core) SelectAllServices(start bool) ([]*Service, error) { func SelectAllServices(start bool) ([]*database.ServiceObj, error) {
var services []*Service srvs := database.Services()
db := Database(&Service{}).Find(&services).Order("order_id desc") for _, s := range srvs {
if db.Error() != nil { fmt.Println("services: ", s.Id, s.Name)
log.Errorln(fmt.Sprintf("service error: %v", db.Error()))
return nil, db.Error()
} }
CoreApp.Services = nil
for _, service := range services { for _, s := range srvs {
if start { if start {
service := s.Model()
service.Start() service.Start()
service.CheckinProcess() CheckinProcess(s)
} }
fails := service.GetFailures(limitedFailures) //fails := service.Service (limitedFailures)
for _, f := range fails { //for _, f := range fails {
service.Failures = append(service.Failures, f) // service.Failures = append(service.Failures, f)
} //}
checkins := service.AllCheckins() for _, c := range s.AllCheckins() {
for _, c := range checkins { s.Checkins = append(s.Checkins, c)
c.Failures = c.GetFailures(limitedFailures)
c.Hits = c.LimitedHits(limitedHits)
service.Checkins = append(service.Checkins, c)
} }
// collect initial service stats // collect initial service stats
service = service.UpdateStats() s.Service.Stats = s.UpdateStats()
CoreApp.Services = append(CoreApp.Services, service) CoreApp.services = append(CoreApp.services, s)
} }
reorderServices() reorderServices()
return services, db.Error() return srvs, nil
} }
// reorderServices will sort the services based on 'order_id' // reorderServices will sort the services based on 'order_id'
func reorderServices() { func reorderServices() {
sort.Sort(ServiceOrder(CoreApp.Services)) sort.Sort(ServiceOrder(CoreApp.services))
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() float64 {
total, _ := s.TotalHits()
if total == 0 {
return 0
}
avg := s.Sum() / float64(total) * 100
f, _ := strconv.ParseFloat(fmt.Sprintf("%0.0f", avg*10), 32)
return f
}
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *Service) OnlineDaysPercent(days int) float32 {
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
return s.OnlineSince(ago)
}
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
s.Online24Hours = 100.00
return s.Online24Hours
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
s.Online24Hours = 0
return s.Online24Hours
}
avg := float64(failed) / float64(total) * 100
avg = 100 - avg
if avg < 0 {
avg = 0
}
amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10)
s.Online24Hours = float32(amount)
return s.Online24Hours
}
// lastFailure returns the last Failure a service had
func (s *Service) lastFailure() types.FailureInterface {
limited := s.GetFailures(1)
if len(limited) == 0 {
return nil
}
last := limited[len(limited)-1]
return last
}
// DowntimeText will return the amount of downtime for a service based on the duration
// service.DowntimeText()
// // Service has been offline for 15 minutes
func (s *Service) DowntimeText() string {
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
}
// Dbtimestamp will return a SQL query for grouping by date
func Dbtimestamp(group string, column string) string {
seconds := 3600
switch group {
case "minute":
seconds = 60
case "hour":
seconds = 3600
case "day":
seconds = 86400
case "week":
seconds = 604800
case "month":
seconds = 2592000
case "year":
seconds = 31557600
default:
seconds = 60
}
switch CoreApp.Config.DbConn {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(%v) AS value", column)
case "postgres":
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(%v) AS value", group, column)
default:
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(%v) as value", seconds, seconds, column)
}
}
// Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration {
hits, _ := s.Hits()
fail := s.lastFailure()
if fail == nil {
return time.Duration(0)
}
if len(hits) == 0 {
return time.Now().UTC().Sub(fail.Select().CreatedAt.UTC())
}
since := fail.Select().CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
} }
// GraphData will return all hits or failures // GraphData will return all hits or failures
@ -265,9 +102,9 @@ func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*da
} }
// index returns a services index int for updating the []*core.Services slice // index returns a services index int for updating the []*core.Services slice
func (s *Service) index() int { func index(s database.Servicer) int {
for k, service := range CoreApp.Services { for k, service := range CoreApp.services {
if s.Id == service.(*Service).Id { if s.Model().Id == service.Model().Id {
return k return k
} }
} }
@ -275,32 +112,34 @@ func (s *Service) index() int {
} }
// updateService will update a service in the []*core.Services slice // updateService will update a service in the []*core.Services slice
func updateService(s *Service) { func updateService(s database.Servicer) {
CoreApp.Services[s.index()] = s CoreApp.services[index(s)] = s
} }
// Delete will remove a service from the database, it will also end the service checking go routine // Delete will remove a service from the database, it will also end the service checking go routine
func (s *Service) Delete() error { func Delete(srv database.Servicer) error {
i := s.index() i := index(srv)
err := Database(&Service{}).Delete(s) s := srv.Model()
if err.Error() != nil { err := database.Delete(s)
log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err.Error())) if err != nil {
return err.Error() log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err))
return err
} }
s.Close() s.Close()
slice := CoreApp.Services slice := CoreApp.services
CoreApp.Services = append(slice[:i], slice[i+1:]...) CoreApp.services = append(slice[:i], slice[i+1:]...)
reorderServices() reorderServices()
notifier.OnDeletedService(s.Service) notifier.OnDeletedService(s)
return err.Error() return err
} }
// Update will update a service in the database, the service's checking routine can be restarted by passing true // Update will update a service in the database, the service's checking routine can be restarted by passing true
func (s *Service) Update(restart bool) error { func Update(srv database.Servicer, restart bool) error {
err := Database(&Service{}).Update(&s) s := srv.Model()
if err.Error() != nil { err := database.Update(s)
if err != nil {
log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err)) log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
return err.Error() return err
} }
// clear the notification queue for a service // clear the notification queue for a service
if !s.AllowNotifications.Bool { if !s.AllowNotifications.Bool {
@ -313,55 +152,27 @@ func (s *Service) Update(restart bool) error {
s.Close() s.Close()
s.Start() s.Start()
s.SleepDuration = time.Duration(s.Interval) * time.Second s.SleepDuration = time.Duration(s.Interval) * time.Second
go s.CheckQueue(true) go ServiceCheckQueue(srv, true)
} }
reorderServices() reorderServices()
updateService(s) updateService(srv)
notifier.OnUpdatedService(s.Service) notifier.OnUpdatedService(s)
return err.Error() return err
} }
// Create will create a service and insert it into the database // Create will create a service and insert it into the database
func (s *Service) Create(check bool) (int64, error) { func Create(srv database.Servicer, check bool) (int64, error) {
s := srv.Model()
s.CreatedAt = time.Now().UTC() s.CreatedAt = time.Now().UTC()
db := Database(&Service{}).Create(s) _, err := database.Create(s)
if db.Error() != nil { if err != nil {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, db.Error())) log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err))
return 0, db.Error() return 0, err
} }
s.Start() s.Start()
go s.CheckQueue(check) go ServiceCheckQueue(srv, check)
CoreApp.Services = append(CoreApp.Services, s) CoreApp.services = append(CoreApp.services, srv)
reorderServices() reorderServices()
notifier.OnNewService(s.Service) notifier.OnNewService(s)
return s.Id, nil return s.Id, nil
} }
// Messages returns all Messages for a Service
func (s *Service) Messages() []*Message {
messages := SelectServiceMessages(s.Id)
return messages
}
// ActiveMessages returns all service messages that are available based on the current time
func (s *Service) ActiveMessages() []*Message {
var messages []*Message
msgs := SelectServiceMessages(s.Id)
for _, m := range msgs {
if m.StartOn.UTC().After(time.Now().UTC()) {
messages = append(messages, m)
}
}
return messages
}
// CountOnline returns the amount of services online
func (c *Core) CountOnline() int {
amount := 0
for _, s := range CoreApp.Services {
if s.Select().Online {
amount++
}
}
return amount
}

View File

@ -139,38 +139,12 @@ func TestServiceAvgUptime(t *testing.T) {
assert.NotEqual(t, "0", service4.AvgUptime(since)) assert.NotEqual(t, "0", service4.AvgUptime(since))
} }
func TestServiceHits(t *testing.T) {
service := SelectService(5)
hits, err := service.Hits()
assert.Nil(t, err)
assert.True(t, len(hits) > 1400)
}
func TestServiceLimitedHits(t *testing.T) {
service := SelectService(5)
hits, err := service.LimitedHits(1024)
assert.Nil(t, err)
assert.Equal(t, int(1024), len(hits))
}
func TestServiceTotalHits(t *testing.T) {
service := SelectService(5)
hits, err := service.TotalHits()
assert.Nil(t, err)
assert.NotZero(t, hits)
}
func TestServiceSum(t *testing.T) { func TestServiceSum(t *testing.T) {
service := SelectService(5) service := SelectService(5)
sum := service.Sum() sum := service.Sum()
assert.NotZero(t, sum) assert.NotZero(t, sum)
} }
func TestCountOnline(t *testing.T) {
amount := CoreApp.CountOnline()
assert.True(t, amount >= 2)
}
func TestCreateService(t *testing.T) { func TestCreateService(t *testing.T) {
s := ReturnService(&types.Service{ s := ReturnService(&types.Service{
Name: "That'll do 🐢", Name: "That'll do 🐢",
@ -375,31 +349,3 @@ func TestSelectGroups(t *testing.T) {
groups = SelectGroups(true, true) groups = SelectGroups(true, true)
assert.Equal(t, int(5), len(groups)) assert.Equal(t, int(5), len(groups))
} }
func TestService_TotalFailures(t *testing.T) {
service := SelectService(8)
failures, err := service.TotalFailures()
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestService_TotalFailures24(t *testing.T) {
service := SelectService(8)
failures, err := service.TotalFailures24()
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestService_TotalFailuresOnDate(t *testing.T) {
t.SkipNow()
ago := utils.Now().UTC()
service := SelectService(8)
failures, err := service.TotalFailuresOnDate(ago)
assert.Nil(t, err)
assert.Equal(t, uint64(1), failures)
}
func TestCountFailures(t *testing.T) {
failures := CountFailures()
assert.NotEqual(t, uint64(0), failures)
}

View File

@ -18,18 +18,17 @@ package core
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statping/database" "github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils" "github.com/hunterlong/statping/utils"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"time" "time"
) )
type User struct { type User struct {
*types.User *database.UserObj
} }
// ReturnUser returns *core.User based off a *types.User // ReturnUser returns *core.User based off a *types.User
func ReturnUser(u *types.User) *User { func uwrap(u *database.UserObj) *User {
return &User{u} return &User{u}
} }
@ -42,29 +41,32 @@ func CountUsers() int64 {
// SelectUser returns the User based on the User's ID. // SelectUser returns the User based on the User's ID.
func SelectUser(id int64) (*User, error) { func SelectUser(id int64) (*User, error) {
var user User user, err := database.User(id)
err := Database(&User{}).Where("id = ?", id).First(&user) if err != nil {
return &user, err.Error() return nil, err
}
return uwrap(user), err
} }
// SelectUsername returns the User based on the User's username // SelectUsername returns the User based on the User's username
func SelectUsername(username string) (*User, error) { func SelectUsername(username string) (*User, error) {
var user User user, err := database.UserByUsername(username)
res := Database(&User{}).Where("username = ?", username) if err != nil {
err := res.First(&user) return nil, err
return &user, err.Error() }
return uwrap(user), err
} }
// Delete will remove the User record from the database // Delete will remove the User record from the database
func (u *User) Delete() error { func (u *User) Delete() error {
return database.Delete(&u) return database.Delete(u)
} }
// Update will update the User's record in database // Update will update the User's record in database
func (u *User) Update() error { func (u *User) Update() error {
u.ApiKey = utils.NewSHA1Hash(5) u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10) u.ApiSecret = utils.NewSHA1Hash(10)
return database.Update(&u) return database.Update(u)
} }
// Create will insert a new User into the database // Create will insert a new User into the database
@ -74,7 +76,7 @@ func (u *User) Create() (int64, error) {
u.ApiKey = utils.NewSHA1Hash(5) u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10) u.ApiSecret = utils.NewSHA1Hash(10)
user, err := database.Create(&u) user, err := database.Create(u)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -88,12 +90,12 @@ func (u *User) Create() (int64, error) {
// SelectAllUsers returns all users // SelectAllUsers returns all users
func SelectAllUsers() ([]*User, error) { func SelectAllUsers() ([]*User, error) {
var users []*User var users []*User
db := Database(&User{}).Find(&users) err := database.AllUsers(&users)
if db.Error() != nil { if err != nil {
log.Errorln(fmt.Sprintf("Failed to load all users. %v", db.Error())) log.Errorln(fmt.Sprintf("Failed to load all users. %v", err))
return nil, db.Error() return nil, err
} }
return users, db.Error() return users, err
} }
// AuthUser will return the User and a boolean if authentication was correct. // AuthUser will return the User and a boolean if authentication was correct.

8
database/checkin_hits.go Normal file
View File

@ -0,0 +1,8 @@
package database
import "github.com/hunterlong/statping/types"
type CheckinHitObj struct {
hits []*types.CheckinHit
o *Object
}

View File

@ -1,33 +1,119 @@
package database package database
import "github.com/hunterlong/statping/types" import (
"fmt"
"github.com/hunterlong/statping/types"
"time"
)
type CheckinObj struct { type CheckinObj struct {
*types.Checkin *types.Checkin
failures o *Object
Checkiner
} }
func (o *Object) AsCheckin() HitsFailures { type Checkiner interface {
return &CheckinObj{ Hits() *CheckinHitObj
Checkin: o.model.(*types.Checkin), Failures() *FailureObj
} Model() *types.Checkin
Object() *CheckinObj
} }
func Checkin(id int64) (HitsFailures, error) { func Checkin(id int64) (*CheckinObj, error) {
var checkin types.Checkin var checkin types.Checkin
query := database.Model(&types.Checkin{}).Where("id = ?", id).Find(&checkin) query := database.Checkins().Where("id = ?", id)
return &CheckinObj{Checkin: &checkin}, query.Error() finder := query.Find(&checkin)
return &CheckinObj{Checkin: &checkin, o: wrapObject(id, &checkin, query)}, finder.Error()
} }
func (c *CheckinObj) Hits() *hits { func CheckinByKey(api string) (*CheckinObj, error) {
return &hits{ var checkin types.Checkin
database.Model(&types.Checkin{}).Where("checkin = ?", c.Id), query := database.Checkins().Where("api = ?", api)
finder := query.Find(&checkin)
return &CheckinObj{Checkin: &checkin, o: wrapObject(checkin.Id, &checkin, query)}, finder.Error()
}
func wrapCheckins(all []*types.Checkin, db Database) []*CheckinObj {
var arr []*CheckinObj
for _, v := range all {
arr = append(arr, &CheckinObj{Checkin: v, o: wrapObject(v.Id, v, db)})
}
return arr
}
func AllCheckins() []*CheckinObj {
var checkins []*types.Checkin
query := database.Checkins()
query.Find(&checkins)
return wrapCheckins(checkins, query)
}
func (s *CheckinObj) Service() *ServiceObj {
var srv *types.Service
q := database.Checkins().Where("service = ?", s.ServiceId)
q.Find(&srv)
return &ServiceObj{
Service: srv,
o: wrapObject(srv.Id, srv, q),
} }
} }
func (c *CheckinObj) Failures() *failures { func (s *CheckinObj) Failures() *FailureObj {
return &failures{ q := database.Failures().
database.Model(&types.Failure{}). Where("method = 'checkin' AND id = ?", s.Id).
Where("method = 'checkin' AND service = ?", c.Id).Order("id desc"), Where("method = 'checkin'")
} return &FailureObj{wrapObject(s.Id, nil, q)}
}
func (s *CheckinObj) object() *Object {
return s.o
}
func (c *CheckinObj) Model() *types.Checkin {
return c.Checkin
}
func (c *CheckinObj) Object() *CheckinObj {
return c
}
// Period will return the duration of the Checkin interval
func (c *CheckinObj) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval))
return duration
}
// Grace will return the duration of the Checkin Grace Period (after service hasn't responded, wait a bit for a response)
func (c *CheckinObj) Grace() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.GracePeriod))
return duration
}
// Expected returns the duration of when the serviec should receive a Checkin
func (c *CheckinObj) Expected() time.Duration {
last := c.Hits().Last()
now := time.Now().UTC()
lastDir := now.Sub(last.CreatedAt)
sub := time.Duration(c.Period() - lastDir)
return sub
}
// Last returns the last checkinHit for a Checkin
func (c *CheckinObj) Hits() *CheckinHitObj {
var checkinHits []*types.CheckinHit
query := database.CheckinHits().Where("checkin = ?", c.Id)
query.Find(&checkinHits)
return &CheckinHitObj{checkinHits, wrapObject(c.Id, checkinHits, query)}
}
// Last returns the last checkinHit for a Checkin
func (c *CheckinHitObj) Last() *types.CheckinHit {
var last types.CheckinHit
c.o.db.Last(&last)
return &last
}
func (c *CheckinObj) Link() string {
return fmt.Sprintf("%v/checkin/%v", "DOMAINHERE", c.ApiKey)
} }

View File

@ -1,6 +1,8 @@
package database package database
import ( import (
"fmt"
"github.com/hunterlong/statping/types"
"reflect" "reflect"
) )
@ -10,19 +12,32 @@ type Object struct {
db Database db Database
} }
type HitsFailures interface { type isObject interface {
Hits() *hits object() *Object
Failures() *failures }
func wrapObject(id int64, model interface{}, db Database) *Object {
return &Object{
Id: id,
model: model,
db: db,
}
} }
func modelId(model interface{}) int64 { func modelId(model interface{}) int64 {
fmt.Printf("%T\n", model)
iface := reflect.ValueOf(model) iface := reflect.ValueOf(model)
field := iface.Elem().FieldByName("Id") field := iface.Elem().FieldByName("Id")
return field.Int() return field.Int()
} }
func toModel(model interface{}) Database { func toModel(model interface{}) Database {
return database.Model(&model) switch model.(type) {
case *types.Core:
return database.Model(&types.Core{}).Table("core")
default:
return database.Model(&model)
}
} }
func Create(data interface{}) (*Object, error) { func Create(data interface{}) (*Object, error) {

View File

@ -104,7 +104,6 @@ type Database interface {
Since(time.Time) Database Since(time.Time) Database
Between(time.Time, time.Time) Database Between(time.Time, time.Time) Database
ToChart() ([]*DateScan, error)
SelectByTime(string) string SelectByTime(string) string
MultipleSelects(args ...string) Database MultipleSelects(args ...string) Database
@ -112,13 +111,73 @@ 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)
Requests(*http.Request) Database Requests(*http.Request, isObject) Database
GroupQuery(query *GroupQuery, by By) GroupByer GroupQuery(query *GroupQuery, by By) GroupByer
Objects
} }
func (it *Db) Requests(r *http.Request) Database { type Objects interface {
g := ParseQueries(r, it) Services() Database
Users() Database
Groups() Database
Incidents() Database
IncidentUpdates() Database
Hits() Database
Failures() Database
Checkins() Database
CheckinHits() Database
Messages() Database
Integrations() Database
}
func (d *Db) Services() Database {
return d.Model(&types.Service{})
}
func (d *Db) Users() Database {
return d.Model(&types.User{})
}
func (d *Db) Groups() Database {
return d.Model(&types.Group{})
}
func (d *Db) Incidents() Database {
return d.Model(&types.Incident{})
}
func (d *Db) IncidentUpdates() Database {
return d.Model(&types.IncidentUpdate{})
}
func (d *Db) Hits() Database {
return d.Model(&types.Hit{})
}
func (d *Db) Integrations() Database {
return d.Model(&types.Integration{})
}
func (d *Db) Failures() Database {
return d.Model(&types.Failure{})
}
func (d *Db) Checkins() Database {
return d.Model(&types.Checkin{})
}
func (d *Db) CheckinHits() Database {
return d.Model(&types.CheckinHit{})
}
func (d *Db) Messages() Database {
return d.Model(&types.Message{})
}
func (it *Db) Requests(r *http.Request, o isObject) Database {
g := ParseQueries(r, o)
return g.db return g.db
} }
@ -446,12 +505,6 @@ func (it *Db) Error() error {
return it.Database.Error return it.Database.Error
} }
func (it *Db) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
err := it.Find(&hits)
return hits, err.Error()
}
func (it *Db) Since(ago time.Time) Database { func (it *Db) Since(ago time.Time) Database {
return it.Where("created_at > ?", it.FormatTime(ago)) return it.Where("created_at > ?", it.FormatTime(ago))
} }
@ -460,37 +513,7 @@ func (it *Db) Between(t1 time.Time, t2 time.Time) Database {
return it.Where("created_at BETWEEN ? AND ?", it.FormatTime(t1), it.FormatTime(t2)) return it.Where("created_at BETWEEN ? AND ?", it.FormatTime(t1), it.FormatTime(t2))
} }
// DateScan struct is for creating the charts.js graph JSON array
type DateScan struct {
CreatedAt string `json:"x,omitempty"`
Value int64 `json:"y"`
}
type TimeValue struct { type TimeValue struct {
Timeframe string `json:"timeframe"` Timeframe string `json:"timeframe"`
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
} }
func (it *Db) ToChart() ([]*DateScan, error) {
rows, err := it.Database.Rows()
if err != nil {
return nil, err
}
var data []*DateScan
for rows.Next() {
gd := new(DateScan)
var createdAt string
var value float64
if err := rows.Scan(&createdAt, &value); err != nil {
return nil, err
}
createdTime, err := time.Parse(TIME, createdAt)
if err != nil {
return nil, err
}
gd.CreatedAt = createdTime.UTC().String()
gd.Value = int64(value * 1000)
data = append(data, gd)
}
return data, err
}

View File

@ -2,31 +2,50 @@ package database
import ( import (
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
"time"
) )
type failures struct { type FailureObj struct {
DB Database o *Object
} }
func (f *failures) All() []*types.Failure { type Failurer interface {
Model() []*types.Failure
}
func (f *FailureObj) Model() []*types.Failure {
return f.All()
}
func (f *FailureObj) All() []*types.Failure {
var fails []*types.Failure var fails []*types.Failure
f.DB = f.DB.Find(&fails) f.o.db.Find(&fails)
return fails return fails
} }
func (f *failures) Last(amount int) *types.Failure { func (f *FailureObj) DeleteAll() error {
query := database.Exec(`DELETE FROM failures WHERE service = ?`, f.o.Id)
return query.Error()
}
func (f *FailureObj) Last(amount int) *types.Failure {
var fail types.Failure var fail types.Failure
f.DB = f.DB.Limit(amount).Find(&fail) f.o.db.Limit(amount).Last(&fail)
return &fail return &fail
} }
func (f *failures) Count() int { func (f *FailureObj) Count() int {
var amount int var amount int
f.DB = f.DB.Count(&amount) f.o.db.Count(&amount)
return amount return amount
} }
func (f *failures) Find(data interface{}) error { func (f *FailureObj) Since(t time.Time) []*types.Failure {
q := f.Find(&data) var fails []*types.Failure
return q f.o.db.Since(t).Find(&fails)
return fails
}
func (f *FailureObj) object() *Object {
return f.o
} }

View File

@ -4,23 +4,44 @@ import "github.com/hunterlong/statping/types"
type GroupObj struct { type GroupObj struct {
*types.Group *types.Group
db Database o *Object
Grouper
} }
type Grouper interface { type Grouper interface {
Services() Database Services() []*types.Service
Model() *types.Group
} }
func (o *Object) AsGroup() *types.Group { func AllGroups() []*GroupObj {
return o.model.(*types.Group) var groups []*types.Group
query := database.Groups()
query.Find(&groups)
return wrapGroups(groups, query)
} }
func (it *Db) GetGroup(id int64) (*GroupObj, error) { func (g *Db) GetGroup(id int64) (*GroupObj, error) {
var group types.Group var group types.Group
query := it.Model(&types.Group{}).Where("id = ?", id).Find(&group) query := database.Groups().Where("id = ?", id)
return &GroupObj{&group, it}, query.Error() finder := query.Find(&group)
return &GroupObj{Group: &group, o: wrapObject(id, &group, query)}, finder.Error()
} }
func (it *GroupObj) Services() Database { func (g *GroupObj) Services() []*types.Service {
return it.db.Model(&types.Service{}).Where("service = ?", it.Id) var services []*types.Service
database.Services().Where("group = ?", g.Id).Find(&services)
return services
}
func (g *GroupObj) Model() *types.Group {
return g.Group
}
func wrapGroups(all []*types.Group, db Database) []*GroupObj {
var arr []*GroupObj
for _, v := range all {
arr = append(arr, &GroupObj{Group: v, o: wrapObject(v.Id, v, db)})
}
return arr
} }

View File

@ -3,7 +3,6 @@ package database
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -36,15 +35,16 @@ type GroupQuery struct {
FillEmpty bool FillEmpty bool
} }
func (b GroupQuery) Find(data interface{}) error {
return b.db.Find(&data).Error()
}
func (b GroupQuery) Database() Database { func (b GroupQuery) Database() Database {
return b.db return b.db
} }
var ( var (
ByCount = By("COUNT(id) as amount") ByCount = By("COUNT(id) as amount")
BySum = func(column string) By {
return By(fmt.Sprintf("SUM(%s) as amount", column))
}
ByAverage = func(column string) By { ByAverage = func(column string) By {
return By(fmt.Sprintf("AVG(%s) as amount", column)) return By(fmt.Sprintf("AVG(%s) as amount", column))
} }
@ -154,16 +154,21 @@ func (g *GroupBy) duration() time.Duration {
} }
} }
func ParseQueries(r *http.Request, db Database) *GroupQuery { func toInt(v string) int64 {
val, _ := strconv.Atoi(v)
return int64(val)
}
func ParseQueries(r *http.Request, o isObject) *GroupQuery {
fields := parseGet(r) fields := parseGet(r)
grouping := fields.Get("group") grouping := fields.Get("group")
if grouping == "" { if grouping == "" {
grouping = "hour" grouping = "hour"
} }
startField := utils.ToInt(fields.Get("start")) startField := toInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end")) endField := toInt(fields.Get("end"))
limit := utils.ToInt(fields.Get("limit")) limit := toInt(fields.Get("limit"))
offset := utils.ToInt(fields.Get("offset")) offset := toInt(fields.Get("offset"))
fill, _ := strconv.ParseBool(fields.Get("fill")) fill, _ := strconv.ParseBool(fields.Get("fill"))
orderBy := fields.Get("order") orderBy := fields.Get("order")
if limit == 0 { if limit == 0 {
@ -180,6 +185,8 @@ func ParseQueries(r *http.Request, db Database) *GroupQuery {
FillEmpty: fill, FillEmpty: fill,
} }
db := o.object().db
if query.Limit != 0 { if query.Limit != 0 {
db = db.Limit(query.Limit) db = db.Limit(query.Limit)
} }

View File

@ -1,30 +1,38 @@
package database package database
import "github.com/hunterlong/statping/types" import (
"github.com/hunterlong/statping/types"
"time"
)
type hits struct { type HitObj struct {
DB Database o *Object
} }
func (h *hits) All() []*types.Hit { func (h *HitObj) All() []*types.Hit {
var fails []*types.Hit var fails []*types.Hit
h.DB = h.DB.Find(&fails) h.o.db.Find(&fails)
return fails return fails
} }
func (h *hits) Last(amount int) *types.Hit { func (h *HitObj) Last(amount int) *types.Hit {
var hits types.Hit var hits types.Hit
h.DB = h.DB.Limit(amount).Find(&hits) h.o.db.Limit(amount).Find(&hits)
return &hits return &hits
} }
func (h *hits) Count() int { func (h *HitObj) Since(t time.Time) []*types.Hit {
var hits []*types.Hit
h.o.db.Since(t).Find(&hits)
return hits
}
func (h *HitObj) Count() int {
var amount int var amount int
h.DB = h.DB.Count(&amount) h.o.db.Count(&amount)
return amount return amount
} }
func (h *hits) Find(data interface{}) error { func (h *HitObj) object() *Object {
q := h.Find(&data) return h.o
return q
} }

View File

@ -4,15 +4,28 @@ import "github.com/hunterlong/statping/types"
type IncidentObj struct { type IncidentObj struct {
*types.Incident *types.Incident
db Database o *Object
}
func (o *IncidentObj) AsIncident() *types.Incident {
return o.Incident
} }
func Incident(id int64) (*IncidentObj, error) { func Incident(id int64) (*IncidentObj, error) {
var incident types.Incident var incident types.Incident
query := database.Model(&types.Incident{}).Where("id = ?", id).Find(&incident) query := database.Incidents().Where("id = ?", id)
return &IncidentObj{Incident: &incident, db: query}, query.Error() finder := query.Find(&incident)
return &IncidentObj{Incident: &incident, o: wrapObject(id, &incident, query)}, finder.Error()
}
func AllIncidents() []*types.Incident {
var incidents []*types.Incident
database.Incidents().Find(&incidents)
return incidents
}
func (i *IncidentObj) Updates() []*types.IncidentUpdate {
var incidents []*types.IncidentUpdate
database.IncidentUpdates().Where("incident = ?", i.Id).Find(&incidents)
return incidents
}
func (i *IncidentObj) object() *Object {
return i.o
} }

19
database/integration.go Normal file
View File

@ -0,0 +1,19 @@
package database
import "github.com/hunterlong/statping/types"
type IntegrationObj struct {
*types.Integration
o *Object
}
func Integration(id int64) (*IntegrationObj, error) {
var integration types.Integration
query := database.Model(&types.Integration{}).Where("id = ?", id)
finder := query.Find(&integration)
return &IntegrationObj{Integration: &integration, o: wrapObject(id, &integration, query)}, finder.Error()
}
func (i *IntegrationObj) object() *Object {
return i.o
}

19
database/message.go Normal file
View File

@ -0,0 +1,19 @@
package database
import "github.com/hunterlong/statping/types"
type MessageObj struct {
*types.Message
o *Object
}
func Message(id int64) (*MessageObj, error) {
var message types.Message
query := database.Messages().Where("id = ?", id)
finder := query.Find(&message)
return &MessageObj{Message: &message, o: wrapObject(id, &message, query)}, finder.Error()
}
func (m *MessageObj) object() *Object {
return m.o
}

View File

@ -1,31 +1,217 @@
package database package database
import "github.com/hunterlong/statping/types" import (
"fmt"
"github.com/hunterlong/statping/types"
"strconv"
"strings"
"time"
)
type ServiceObj struct { type ServiceObj struct {
*types.Service *types.Service
failures o *Object
Servicer
} }
func (o *Object) AsService() *types.Service { type Servicer interface {
return o.model.(*types.Service) Hits() *HitObj
Failures() *FailureObj
AllCheckins() []*CheckinObj
Model() *types.Service
Interval() time.Duration
DowntimeText() string
Hittable
} }
func Service(id int64) (HitsFailures, error) { type Hittable interface {
CreateHit(*types.Hit) (int64, error)
}
func Service(id int64) (*ServiceObj, error) {
var service types.Service var service types.Service
query := database.Model(&types.Service{}).Where("id = ?", id).Find(&service) query := database.Services().Where("id = ?", id)
return &ServiceObj{Service: &service}, query.Error() finer := query.Find(&service)
return &ServiceObj{Service: &service, o: wrapObject(id, &service, query)}, finer.Error()
} }
func (s *ServiceObj) Hits() *hits { func wrapServices(all []*types.Service, db Database) []*ServiceObj {
return &hits{ var arr []*ServiceObj
database.Model(&types.Hit{}).Where("service = ?", s.Id), for _, v := range all {
arr = append(arr, &ServiceObj{Service: v, o: wrapObject(v.Id, v, db)})
} }
return arr
} }
func (s *ServiceObj) Failures() *failures { func Services() []*ServiceObj {
return &failures{ var services []*types.Service
database.Model(&types.Failure{}). db := database.Services().Order("order_id desc")
Where("method != 'checkin' AND service = ?", s.Id).Order("id desc"), db.Find(&services)
} return wrapServices(services, db)
}
func (s *ServiceObj) AllCheckins() []*CheckinObj {
var checkins []*types.Checkin
query := database.Checkins().Where("service = ?", s.Id)
query.Find(&checkins)
return wrapCheckins(checkins, query)
}
func (s *ServiceObj) DowntimeText() string {
last := s.Failures().Last(1)
return parseError(last)
}
// ParseError returns a human readable error for a Failure
func parseError(f *types.Failure) string {
if f.Method == "checkin" {
return fmt.Sprintf("Checkin is Offline")
}
err := strings.Contains(f.Issue, "connection reset by peer")
if err {
return fmt.Sprintf("Connection Reset")
}
err = strings.Contains(f.Issue, "operation timed out")
if err {
return fmt.Sprintf("HTTP Request Timed Out")
}
err = strings.Contains(f.Issue, "x509: certificate is valid")
if err {
return fmt.Sprintf("SSL Certificate invalid")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "no such host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "HTTP Status Code")
if err {
return fmt.Sprintf("Incorrect HTTP Status Code")
}
err = strings.Contains(f.Issue, "connection refused")
if err {
return fmt.Sprintf("Connection Failed")
}
err = strings.Contains(f.Issue, "can't assign requested address")
if err {
return fmt.Sprintf("Unable to Request Address")
}
err = strings.Contains(f.Issue, "no route to host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
err = strings.Contains(f.Issue, "i/o timeout")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while reading body")
if err {
return fmt.Sprintf("Timed Out on Response Body")
}
return f.Issue
}
func (s *ServiceObj) Interval() time.Duration {
return time.Duration(s.Service.Interval) * time.Second
}
func (s *ServiceObj) Model() *types.Service {
return s.Service
}
func (s *ServiceObj) Hits() *HitObj {
fmt.Println("hits")
query := database.Hits().Where("service = ?", s.Id)
return &HitObj{wrapObject(s.Id, nil, query)}
}
func (s *ServiceObj) Failures() *FailureObj {
q := database.Failures().Where("method != 'checkin' AND service = ?", s.Id)
return &FailureObj{wrapObject(s.Id, nil, q)}
}
func (s *ServiceObj) Group() *GroupObj {
var group types.Group
q := database.Groups().Where("id = ?", s.GroupId)
finder := q.Find(&group)
if finder.Error() != nil {
return nil
}
return &GroupObj{Group: &group, o: wrapObject(group.Id, &group, q)}
}
func (s *ServiceObj) object() *Object {
return s.o
}
func (s *ServiceObj) UpdateStats() *types.Stats {
s.Online24Hours = s.OnlineDaysPercent(1)
s.Online7Days = s.OnlineDaysPercent(7)
s.AvgResponse = s.AvgTime()
s.FailuresLast24Hours = len(s.Failures().Since(time.Now().Add(-time.Hour * 24)))
return s.Stats
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *ServiceObj) AvgTime() float64 {
var sum float64
database.Hits().
Select("AVG(latency) as amount").
Where("service = ?", s.Id).Pluck("amount", &sum).Debug()
total := s.Hits().Count()
if total == 0 {
return 0
}
avg := sum / float64(total) * 100
f, _ := strconv.ParseFloat(fmt.Sprintf("%0.0f", avg*10), 32)
return f
}
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *ServiceObj) OnlineDaysPercent(days int) float32 {
ago := time.Now().UTC().Add((-24 * time.Duration(days)) * time.Hour)
return s.OnlineSince(ago)
}
// OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *ServiceObj) OnlineSince(ago time.Time) float32 {
failed := s.Failures().Since(ago)
if len(failed) == 0 {
s.Online24Hours = 100.00
return s.Online24Hours
}
total := s.Hits().Since(ago)
if len(total) == 0 {
s.Online24Hours = 0
return s.Online24Hours
}
avg := float64(len(failed)) / float64(len(total)) * 100
avg = 100 - avg
if avg < 0 {
avg = 0
}
amount, _ := strconv.ParseFloat(fmt.Sprintf("%0.2f", avg), 10)
s.Online24Hours = float32(amount)
return s.Online24Hours
}
// Downtime returns the amount of time of a offline service
func (s *ServiceObj) Downtime() time.Duration {
hits := s.Hits().Last(1)
fail := s.Failures().Last(1)
if fail == nil {
return time.Duration(0)
}
if hits == nil {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(hits.CreatedAt.UTC())
return since
} }

View File

@ -4,16 +4,28 @@ import "github.com/hunterlong/statping/types"
type UserObj struct { type UserObj struct {
*types.User *types.User
} o *Object
func (o *Object) AsUser() *UserObj {
return &UserObj{
User: o.model.(*types.User),
}
} }
func User(id int64) (*UserObj, error) { func User(id int64) (*UserObj, error) {
var user types.User var user types.User
query := database.Model(&types.User{}).Where("id = ?", id).Find(&user) query := database.Users().Where("id = ?", id)
return &UserObj{User: &user}, query.Error() finder := query.First(&user)
return &UserObj{User: &user, o: wrapObject(id, &user, query)}, finder.Error()
}
func UserByUsername(username string) (*UserObj, error) {
var user types.User
query := database.Users().Where("username = ?", username)
finder := query.First(&user)
return &UserObj{User: &user, o: wrapObject(user.Id, &user, query)}, finder.Error()
}
func AllUsers(input interface{}) error {
err := database.Users().Find(&input)
return err.Error()
}
func (u *UserObj) object() *Object {
return u.o
} }

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"github.com/hunterlong/statping/core" "github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/core/notifier" "github.com/hunterlong/statping/core/notifier"
"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"
@ -134,9 +135,9 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
var objName string var objName string
var objId int64 var objId int64
switch v := obj.(type) { switch v := obj.(type) {
case types.ServiceInterface: case types.Servicer:
objName = "service" objName = "service"
objId = v.Select().Id objId = v.Model().Id
case *notifier.Notification: case *notifier.Notification:
objName = "notifier" objName = "notifier"
objId = v.Id objId = v.Id
@ -151,9 +152,9 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
case *types.Group: case *types.Group:
objName = "group" objName = "group"
objId = v.Id objId = v.Id
case *core.Group: case database.Grouper:
objName = "group" objName = "group"
objId = v.Id objId = v.Model().Id
case *core.Checkin: case *core.Checkin:
objName = "checkin" objName = "checkin"
objId = v.Id objId = v.Id

View File

@ -20,6 +20,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" "net"
@ -27,23 +28,21 @@ import (
) )
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) { func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
checkins := core.AllCheckins() checkins := database.AllCheckins()
for _, c := range checkins {
c.Hits = c.AllHits()
c.Failures = c.GetFailures(64)
}
returnJson(checkins, w, r) returnJson(checkins, w, r)
} }
func apiCheckinHandler(w http.ResponseWriter, r *http.Request) { func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
checkin := core.SelectCheckin(vars["api"]) checkin, err := database.CheckinByKey(vars["api"])
if checkin == nil { if err != nil {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r) sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
return return
} }
checkin.Hits = checkin.LimitedHits(32) out := checkin.Model()
checkin.Failures = checkin.GetFailures(32)
out.Hits = checkin.Hits()
out.Failures = checkin.Failures(32)
returnJson(checkin, w, r) returnJson(checkin, w, r)
} }

View File

@ -1,7 +1,7 @@
package handlers package handlers
import ( import (
"bytes" "encoding/json"
"github.com/hunterlong/statping/core" "github.com/hunterlong/statping/core"
"html/template" "html/template"
"net/http" "net/http"
@ -12,37 +12,30 @@ var (
basePath = "/" basePath = "/"
) )
type HandlerFunc func(Responder, *Request) type CustomResponseWriter struct {
body []byte
func (f HandlerFunc) ServeHTTP(w Responder, r *Request) { statusCode int
f(w, r) header http.Header
} }
type Handler interface { func NewCustomResponseWriter() *CustomResponseWriter {
ServeHTTP(Responder, *Request) return &CustomResponseWriter{
header: http.Header{},
}
} }
type Responder struct { func (w *CustomResponseWriter) Header() http.Header {
Code int // the HTTP response code from WriteHeader return w.header
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
} }
type Request struct { func (w *CustomResponseWriter) Write(b []byte) (int, error) {
*http.Request w.body = b
// implement it as per your requirement
return 0, nil
} }
func (r Responder) Header() http.Header { func (w *CustomResponseWriter) WriteHeader(statusCode int) {
return r.HeaderMap w.statusCode = statusCode
}
func (r Responder) Write(p []byte) (int, error) {
return r.Body.Write(p)
}
func (r Responder) WriteHeader(statusCode int) {
r.Code = statusCode
} }
func parseForm(r *http.Request) url.Values { func parseForm(r *http.Request) url.Values {
@ -55,6 +48,20 @@ func parseGet(r *http.Request) url.Values {
return r.Form return r.Form
} }
func decodeRequest(r *http.Request, object interface{}) error {
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
return decoder.Decode(&object)
}
type parsedObject struct {
Error Error
}
func serviceFromID(r *http.Request, object interface{}) error {
return nil
}
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 {

View File

@ -4,13 +4,14 @@ import (
"encoding/json" "encoding/json"
"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"
) )
func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) { func apiAllIncidentsHandler(w http.ResponseWriter, r *http.Request) {
incidents := core.AllIncidents() incidents := database.AllIncidents()
returnJson(incidents, w, r) returnJson(incidents, w, r)
} }

View File

@ -130,8 +130,8 @@ func readOnly(handler func(w http.ResponseWriter, r *http.Request), redirect boo
} }
// cached is a middleware function that accepts a duration and content type and will cache the response of the original request // cached is a middleware function that accepts a duration and content type and will cache the response of the original request
func cached(duration, contentType string, handler func(w Responder, r *Request)) Responder { func cached(duration, contentType string, handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
return HandlerFunc(func(w Responder, r *Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
content := CacheStorage.Get(r.RequestURI) content := CacheStorage.Get(r.RequestURI)
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")

View File

@ -150,7 +150,7 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
sendJsonAction(service, "running", w, r) sendJsonAction(service, "running", w, r)
} }
func apiServiceDataHandler(w Responder, r *http.Request) { func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service, err := database.Service(utils.ToInt(vars["id"])) service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil { if err != nil {
@ -158,7 +158,7 @@ func apiServiceDataHandler(w Responder, r *http.Request) {
return return
} }
groupQuery := database.ParseQueries(r, service.Hits().DB) groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency")) obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency"))
returnJson(obj, w, r) returnJson(obj, w, r)
@ -171,7 +171,7 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r) sendErrorJson(errors.New("service data not found"), w, r)
return return
} }
groupQuery := database.ParseQueries(r, service.Hits().DB) groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount) obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount)
returnJson(obj, w, r) returnJson(obj, w, r)
@ -184,7 +184,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r) sendErrorJson(errors.New("service data not found"), w, r)
return return
} }
groupQuery := database.ParseQueries(r, service.Hits().DB) groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time")) obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time"))
returnJson(obj, w, r) returnJson(obj, w, r)
@ -274,7 +274,7 @@ func joinServices(srvs []types.ServiceInterface) []*types.Service {
return services return services
} }
func servicesDeleteFailuresHandler(w Responder, r *http.Request) { func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"])) service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil { if service == nil {
@ -293,12 +293,9 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
return errors.New("service not found") return errors.New("service not found")
} }
service.Hits()
service.Failures()
var fails []types.Failure var fails []types.Failure
service.Failures().DB.Requests(r).Find(&fails) database.ParseQueries(r, service.Failures()).Find(&fails)
return fails return fails
} }
@ -310,7 +307,7 @@ func apiServiceHitsHandler(r *http.Request) interface{} {
} }
var hits []types.Hit var hits []types.Hit
service.Hits().Find(&hits) database.ParseQueries(r, service.Hits()).Find(&hits)
return hits return hits
} }

View File

@ -36,10 +36,6 @@ type Checkin struct {
Failures []*Failure `gorm:"-" json:"failures"` Failures []*Failure `gorm:"-" json:"failures"`
} }
type CheckinInterface interface {
Select() *Checkin
}
// BeforeCreate for Checkin will set CreatedAt to UTC // BeforeCreate for Checkin will set CreatedAt to UTC
func (c *Checkin) BeforeCreate() (err error) { func (c *Checkin) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() { if c.CreatedAt.IsZero() {

View File

@ -29,28 +29,31 @@ type AllNotifiers interface{}
// will be saved into 1 row in the 'core' table. You can use the core.CoreApp // will be saved into 1 row in the 'core' table. You can use the core.CoreApp
// global variable to interact with the attributes to the application, such as services. // global variable to interact with the attributes to the application, such as services.
type Core struct { type Core struct {
Name string `gorm:"not null;column:name" json:"name"` Name string `gorm:"not null;column:name" json:"name"`
Description string `gorm:"not null;column:description" json:"description,omitempty"` Description string `gorm:"not null;column:description" json:"description,omitempty"`
ConfigFile string `gorm:"column:config" json:"-"` ConfigFile string `gorm:"column:config" json:"-"`
ApiKey string `gorm:"column:api_key" json:"api_key" scope:"admin"` ApiKey string `gorm:"column:api_key" json:"api_key" scope:"admin"`
ApiSecret string `gorm:"column:api_secret" json:"api_secret" scope:"admin"` ApiSecret string `gorm:"column:api_secret" json:"api_secret" scope:"admin"`
Style string `gorm:"not null;column:style" json:"style,omitempty"` Style string `gorm:"not null;column:style" json:"style,omitempty"`
Footer NullString `gorm:"column:footer" json:"footer"` Footer NullString `gorm:"column:footer" json:"footer"`
Domain string `gorm:"not null;column:domain" json:"domain"` Domain string `gorm:"not null;column:domain" json:"domain"`
Version string `gorm:"column:version" json:"version"` Version string `gorm:"column:version" json:"version"`
Setup bool `gorm:"-" json:"setup"` Setup bool `gorm:"-" json:"setup"`
MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"` MigrationId int64 `gorm:"column:migration_id" json:"migration_id,omitempty"`
UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"` UseCdn NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"` Timezone float32 `gorm:"column:timezone;default:-8.0" json:"timezone,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"` LoggedIn bool `gorm:"-" json:"logged_in"`
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"`
Started time.Time `gorm:"-" json:"started_on"` Started time.Time `gorm:"-" json:"started_on"`
Services []ServiceInterface `gorm:"-" json:"-"` Plugins []*Info `gorm:"-" json:"-"`
Plugins []*Info `gorm:"-" json:"-"` Repos []PluginJSON `gorm:"-" json:"-"`
Repos []PluginJSON `gorm:"-" json:"-"` AllPlugins []PluginActions `gorm:"-" json:"-"`
AllPlugins []PluginActions `gorm:"-" json:"-"` Notifications []AllNotifiers `gorm:"-" json:"-"`
Notifications []AllNotifiers `gorm:"-" json:"-"` Config *DbConfig `gorm:"-" json:"-"`
Config *DbConfig `gorm:"-" json:"-"` Integrations []Integrator `gorm:"-" json:"-"`
Integrations []Integrator `gorm:"-" json:"-"` }
type Servicer interface {
Model() *Service
} }

View File

@ -33,10 +33,6 @@ type Failure struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }
type FailureInterface interface {
Select() *Failure
}
// BeforeCreate for Failure will set CreatedAt to UTC // BeforeCreate for Failure will set CreatedAt to UTC
func (f *Failure) BeforeCreate() (err error) { func (f *Failure) BeforeCreate() (err error) {
if f.CreatedAt.IsZero() { if f.CreatedAt.IsZero() {
@ -45,7 +41,7 @@ func (f *Failure) BeforeCreate() (err error) {
return return
} }
type FailSort []FailureInterface type FailSort []Failure
func (s FailSort) Len() int { func (s FailSort) Len() int {
return len(s) return len(s)
@ -54,5 +50,5 @@ func (s FailSort) Swap(i, j int) {
s[i], s[j] = s[j], s[i] s[i], s[j] = s[j], s[i]
} }
func (s FailSort) Less(i, j int) bool { func (s FailSort) Less(i, j int) bool {
return s[i].Select().Id < s[j].Select().Id return s[i].Id < s[j].Id
} }

View File

@ -46,8 +46,7 @@ type Service struct {
Online24Hours float32 `gorm:"-" json:"online_24_hours"` Online24Hours float32 `gorm:"-" json:"online_24_hours"`
Online7Days float32 `gorm:"-" json:"online_7_days"` Online7Days float32 `gorm:"-" json:"online_7_days"`
AvgResponse float64 `gorm:"-" json:"avg_response"` AvgResponse float64 `gorm:"-" json:"avg_response"`
FailuresLast24Hours uint64 `gorm:"-" json:"failures_24_hours"` FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"`
LastFailure FailureInterface `gorm:"-" json:"last_failure,omitempty"`
Running chan bool `gorm:"-" json:"-"` Running chan bool `gorm:"-" json:"-"`
Checkpoint time.Time `gorm:"-" json:"-"` Checkpoint time.Time `gorm:"-" json:"-"`
SleepDuration time.Duration `gorm:"-" json:"-"` SleepDuration time.Duration `gorm:"-" json:"-"`
@ -59,9 +58,13 @@ type Service struct {
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code"` LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_success"` LastOnline time.Time `gorm:"-" json:"last_success"`
Failures []FailureInterface `gorm:"-" json:"failures,omitempty" scope:"user,admin"` Failures []Failure `gorm:"-" json:"failures,omitempty" scope:"user,admin"`
Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty" scope:"user,admin"` Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty" scope:"user,admin"`
Stats Stater `gorm:"-" json:"stats,omitempty"` Stats *Stats `gorm:"-" json:"stats,omitempty"`
}
type CheckinInterface interface {
Model() *Checkin
} }
type Stater interface { type Stater interface {
@ -82,15 +85,6 @@ func (s *Service) BeforeCreate() (err error) {
return return
} }
type ServiceInterface interface {
Select() *Service
CheckQueue(bool)
Check(bool)
Create(bool) (int64, error)
Update(bool) error
Delete() error
}
// Start will create a channel for the service checking go routine // Start will create a channel for the service checking go routine
func (s *Service) Start() { func (s *Service) Start() {
s.Running = make(chan bool) s.Running = make(chan bool)