notifiers - split interfaces/packages

pull/29/head
Hunter Long 2018-07-13 19:37:39 -07:00
parent 5ee6fe62ab
commit 4ce7ca7530
29 changed files with 432 additions and 327 deletions

42
cli.go
View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/plugin" "github.com/hunterlong/statup/plugin"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"io/ioutil" "io/ioutil"
@ -105,8 +106,9 @@ func RunOnce() {
if err != nil { if err != nil {
utils.Log(4, err) utils.Log(4, err)
} }
for _, s := range core.CoreApp.Services { for _, ser := range core.CoreApp.Services {
out := s.Check() s := ser.ToService()
out := core.ServiceCheck(s)
fmt.Printf(" Service %v | URL: %v | Latency: %0.0fms | Online: %v\n", out.Name, out.Domain, (out.Latency * 1000), out.Online) fmt.Printf(" Service %v | URL: %v | Latency: %0.0fms | Online: %v\n", out.Name, out.Domain, (out.Latency * 1000), out.Online)
} }
} }
@ -148,33 +150,33 @@ func TestPlugin(plug plugin.PluginActions) {
core.OnLoad(core.DbSession) core.OnLoad(core.DbSession)
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnSuccess(Service)'") fmt.Println(POINT + "Sending 'OnSuccess(Service)'")
core.OnSuccess(core.SelectService(1)) core.OnSuccess(core.SelectService(1).ToService())
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnFailure(Service, FailureData)'") fmt.Println(POINT + "Sending 'OnFailure(Service, FailureData)'")
fakeFailD := core.FailureData{ fakeFailD := core.FailureData{
Issue: "No issue, just testing this plugin. This would include HTTP failure information though", Issue: "No issue, just testing this plugin. This would include HTTP failure information though",
} }
core.OnFailure(core.SelectService(1), fakeFailD) core.OnFailure(core.SelectService(1).ToService(), fakeFailD)
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnSettingsSaved(Core)'") fmt.Println(POINT + "Sending 'OnSettingsSaved(Core)'")
fmt.Println(BRAKER) fmt.Println(BRAKER)
core.OnSettingsSaved(core.CoreApp) core.OnSettingsSaved(core.CoreApp)
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnNewService(Service)'") fmt.Println(POINT + "Sending 'OnNewService(Service)'")
core.OnNewService(core.SelectService(2)) core.OnNewService(core.SelectService(2).ToService())
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnNewUser(User)'") fmt.Println(POINT + "Sending 'OnNewUser(User)'")
user, _ := core.SelectUser(1) user, _ := core.SelectUser(1)
core.OnNewUser(user) core.OnNewUser(user)
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnUpdateService(Service)'") fmt.Println(POINT + "Sending 'OnUpdateService(Service)'")
srv := core.SelectService(2) srv := core.SelectService(2).ToService()
srv.Type = "http" srv.Type = "http"
srv.Domain = "https://yahoo.com" srv.Domain = "https://yahoo.com"
core.OnUpdateService(srv) core.OnUpdateService(srv)
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
fmt.Println(POINT + "Sending 'OnDeletedService(Service)'") fmt.Println(POINT + "Sending 'OnDeletedService(Service)'")
core.OnDeletedService(core.SelectService(1)) core.OnDeletedService(core.SelectService(1).ToService())
fmt.Println("\n" + BRAKER) fmt.Println("\n" + BRAKER)
} }
@ -212,21 +214,21 @@ func FakeSeed(plug plugin.PluginActions) {
core.CoreApp.ApiSecret = "0x0x0x0x0" core.CoreApp.ApiSecret = "0x0x0x0x0"
core.CoreApp.ApiKey = "abcdefg12345" core.CoreApp.ApiKey = "abcdefg12345"
fakeSrv := &core.Service{ fakeSrv := &types.Service{
Name: "Test Plugin Service", Name: "Test Plugin Service",
Domain: "https://google.com", Domain: "https://google.com",
Method: "GET", Method: "GET",
} }
fakeSrv.Create() core.CreateService(fakeSrv)
fakeSrv2 := &core.Service{ fakeSrv2 := &types.Service{
Name: "Awesome Plugin Service", Name: "Awesome Plugin Service",
Domain: "https://netflix.com", Domain: "https://netflix.com",
Method: "GET", Method: "GET",
} }
fakeSrv2.Create() core.CreateService(fakeSrv2)
fakeUser := &core.User{ fakeUser := &types.User{
Id: 6334, Id: 6334,
Username: "Bulbasaur", Username: "Bulbasaur",
Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy", Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy",
@ -234,35 +236,37 @@ func FakeSeed(plug plugin.PluginActions) {
Admin: true, Admin: true,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
fakeUser.Create() core.CreateUser(fakeUser)
fakeUser = &core.User{ fakeUser = &types.User{
Id: 6335, Id: 6335,
Username: "Billy", Username: "Billy",
Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy", Password: "$2a$14$NzT/fLdE3f9iB1Eux2C84O6ZoPhI4NfY0Ke32qllCFo8pMTkUPZzy",
Email: "info@awesome.com", Email: "info@awesome.com",
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
fakeUser.Create() core.CreateUser(fakeUser)
for i := 0; i <= 50; i++ { for i := 0; i <= 50; i++ {
dd := core.HitData{ dd := core.HitData{
Latency: rand.Float64(), Latency: rand.Float64(),
} }
fakeSrv.CreateHit(dd) core.CreateServiceHit(fakeSrv, dd)
dd = core.HitData{ dd = core.HitData{
Latency: rand.Float64(), Latency: rand.Float64(),
} }
fakeSrv2.CreateHit(dd) core.CreateServiceHit(fakeSrv2, dd)
fail := core.FailureData{ fail := core.FailureData{
Issue: "This is not an issue, but it would container HTTP response errors.", Issue: "This is not an issue, but it would container HTTP response errors.",
} }
fakeSrv.CreateFailure(fail) core.CreateServiceFailure(fakeSrv, fail)
fail = core.FailureData{ fail = core.FailureData{
Issue: "HTTP Status Code 521 did not match 200", Issue: "HTTP Status Code 521 did not match 200",
} }
fakeSrv2.CreateFailure(fail) core.CreateServiceFailure(fakeSrv, fail)
} }
fmt.Println("Seeding example data is complete, running Plugin Tests") fmt.Println("Seeding example data is complete, running Plugin Tests")

View File

@ -18,21 +18,22 @@ type FailureData types.FailureData
func CheckServices() { func CheckServices() {
CoreApp.Services, _ = SelectAllServices() CoreApp.Services, _ = SelectAllServices()
utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services))) utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
for _, v := range CoreApp.Services { for _, ser := range CoreApp.Services {
obj := v s := ser.ToService()
obj := s
//go obj.StartCheckins() //go obj.StartCheckins()
obj.stopRoutine = make(chan struct{}) obj.StopRoutine = make(chan struct{})
go obj.CheckQueue() go CheckQueue(obj)
} }
} }
func (s *Service) CheckQueue() { func CheckQueue(s *types.Service) {
for { for {
select { select {
case <-s.stopRoutine: case <-s.StopRoutine:
return return
default: default:
s.Check() ServiceCheck(s)
if s.Interval < 1 { if s.Interval < 1 {
s.Interval = 1 s.Interval = 1
} }
@ -43,7 +44,7 @@ func (s *Service) CheckQueue() {
} }
} }
func (s *Service) DNSCheck() (float64, error) { func DNSCheck(s *types.Service) (float64, error) {
t1 := time.Now() t1 := time.Now()
url, err := url.Parse(s.Domain) url, err := url.Parse(s.Domain)
if err != nil { if err != nil {
@ -58,13 +59,13 @@ func (s *Service) DNSCheck() (float64, error) {
return subTime, err return subTime, err
} }
func (s *Service) Check() *Service { func ServiceCheck(s *types.Service) *types.Service {
dnsLookup, err := s.DNSCheck() dnsLookup, err := DNSCheck(s)
if err != nil { if err != nil {
s.Failure(fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
return s return s
} }
s.dnsLookup = dnsLookup s.DnsLookup = dnsLookup
t1 := time.Now() t1 := time.Now()
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
@ -77,14 +78,14 @@ func (s *Service) Check() *Service {
response, err = client.Get(s.Domain) response, err = client.Get(s.Domain)
} }
if err != nil { if err != nil {
s.Failure(fmt.Sprintf("HTTP Error %v", err)) RecordFailure(s, fmt.Sprintf("HTTP Error %v", err))
return s return s
} }
response.Header.Set("User-Agent", "StatupMonitor") response.Header.Set("User-Agent", "StatupMonitor")
t2 := time.Now() t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds() s.Latency = t2.Sub(t1).Seconds()
if err != nil { if err != nil {
s.Failure(fmt.Sprintf("HTTP Error %v", err)) RecordFailure(s, fmt.Sprintf("HTTP Error %v", err))
return s return s
} }
defer response.Body.Close() defer response.Body.Close()
@ -100,20 +101,20 @@ func (s *Service) Check() *Service {
if !match { if !match {
s.LastResponse = string(contents) s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode s.LastStatusCode = response.StatusCode
s.Failure(fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected)) RecordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected))
return s return s
} }
} }
if s.ExpectedStatus != response.StatusCode { if s.ExpectedStatus != response.StatusCode {
s.LastResponse = string(contents) s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode s.LastStatusCode = response.StatusCode
s.Failure(fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus)) RecordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus))
return s return s
} }
s.LastResponse = string(contents) s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode s.LastStatusCode = response.StatusCode
s.Online = true s.Online = true
s.Record(response) RecordSuccess(s, response)
return s return s
} }
@ -121,23 +122,23 @@ type HitData struct {
Latency float64 Latency float64
} }
func (s *Service) Record(response *http.Response) { func RecordSuccess(s *types.Service, response *http.Response) {
s.Online = true s.Online = true
s.LastOnline = time.Now() s.LastOnline = time.Now()
data := HitData{ data := HitData{
Latency: s.Latency, Latency: s.Latency,
} }
s.CreateHit(data) CreateServiceHit(s, data)
OnSuccess(s) OnSuccess(s)
} }
func (s *Service) Failure(issue string) { func RecordFailure(s *types.Service, issue string) {
s.Online = false s.Online = false
data := FailureData{ data := FailureData{
Issue: issue, Issue: issue,
} }
utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue)) utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue))
s.CreateFailure(data) CreateServiceFailure(s, data)
//SendFailureEmail(s) //SendFailureEmail(s)
OnFailure(s, data) OnFailure(s, data)
} }

View File

@ -14,8 +14,9 @@ func (c *Checkin) String() string {
return c.Api return c.Api
} }
func FindCheckin(api string) *Checkin { func FindCheckin(api string) *types.Checkin {
for _, s := range CoreApp.Services { for _, ser := range CoreApp.Services {
s := ser.ToService()
for _, c := range s.Checkins { for _, c := range s.Checkins {
if c.Api == api { if c.Api == api {
return c return c
@ -25,8 +26,8 @@ func FindCheckin(api string) *Checkin {
return nil return nil
} }
func (s *Service) SelectAllCheckins() []*Checkin { func SelectAllCheckins(s *types.Service) []*types.Checkin {
var checkins []*Checkin var checkins []*types.Checkin
col := DbSession.Collection("checkins").Find("service", s.Id).OrderBy("-id") col := DbSession.Collection("checkins").Find("service", s.Id).OrderBy("-id")
col.All(&checkins) col.All(&checkins)
s.Checkins = checkins s.Checkins = checkins
@ -78,33 +79,33 @@ func (f *Checkin) Ago() string {
return got return got
} }
func (c *Checkin) Run() { //func (c *Checkin) Run() {
if c.Interval == 0 { // if c.Interval == 0 {
return // return
} // }
fmt.Println("checking: ", c.Api) // fmt.Println("checking: ", c.Api)
between := time.Now().Sub(c.Last).Seconds() // between := time.Now().Sub(c.Last).Seconds()
if between > float64(c.Interval) { // if between > float64(c.Interval) {
guard := make(chan struct{}) // guard := make(chan struct{})
c.RecheckCheckinFailure(guard) // c.RecheckCheckinFailure(guard)
<-guard // <-guard
} // }
time.Sleep(1 * time.Second) // time.Sleep(1 * time.Second)
c.Run() // c.Run()
} //}
//
func (s *Service) StartCheckins() { //func (s *Service) StartCheckins() {
for _, c := range s.Checkins { // for _, c := range s.Checkins {
checkin := c // checkin := c.(*Checkin)
go checkin.Run() // go checkin.Run()
} // }
} //}
//
func CheckinProcess() { //func CheckinProcess() {
for _, s := range CoreApp.Services { // for _, s := range CoreApp.Services {
for _, c := range s.Checkins { // for _, c := range s.Checkins {
checkin := c // checkin := c
go checkin.Run() // go checkin.Run()
} // }
} // }
} //}

View File

@ -14,7 +14,7 @@ import (
func LoadConfig() (*types.Config, error) { func LoadConfig() (*types.Config, error) {
if os.Getenv("DB_CONN") != "" { if os.Getenv("DB_CONN") != "" {
utils.Log(1, "DB_CONN environment variable was found, sleeping for 30 seconds") utils.Log(1, "DB_CONN environment variable was found, sleeping for 30 seconds")
time.Sleep(30 * time.Second) //time.Sleep(30 * time.Second)
return LoadUsingEnv() return LoadUsingEnv()
} }
Configs = new(types.Config) Configs = new(types.Config)
@ -102,13 +102,13 @@ func LoadUsingEnv() (*types.Config, error) {
utils.Log(3, err) utils.Log(3, err)
} }
admin := &User{ admin := &types.User{
Username: "admin", Username: "admin",
Password: "admin", Password: "admin",
Email: "info@admin.com", Email: "info@admin.com",
Admin: true, Admin: true,
} }
admin.Create() CreateUser(admin)
LoadSampleData() LoadSampleData()

View File

@ -105,7 +105,8 @@ func (c Core) MobileSASS() string {
} }
func (c Core) AllOnline() bool { func (c Core) AllOnline() bool {
for _, s := range CoreApp.Services { for _, ser := range CoreApp.Services {
s := ser.ToService()
if !s.Online { if !s.Online {
return false return false
} }

View File

@ -4,6 +4,7 @@ import (
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/hunterlong/statup/notifiers" "github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/plugin" "github.com/hunterlong/statup/plugin"
"github.com/hunterlong/statup/types"
"upper.io/db.v3/lib/sqlbuilder" "upper.io/db.v3/lib/sqlbuilder"
) )
@ -13,18 +14,18 @@ func OnLoad(db sqlbuilder.Database) {
} }
} }
func OnSuccess(s *Service) { func OnSuccess(s *types.Service) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnSuccess(structs.Map(s)) p.OnSuccess(structs.Map(s))
} }
notifiers.OnSuccess(structs.Map(s)) notifiers.OnSuccess(s)
} }
func OnFailure(s *Service, f FailureData) { func OnFailure(s *types.Service, f FailureData) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnFailure(structs.Map(s)) p.OnFailure(structs.Map(s))
} }
notifiers.OnFailure(structs.Map(s)) notifiers.OnFailure(s)
} }
func OnSettingsSaved(c *Core) { func OnSettingsSaved(c *Core) {
@ -33,25 +34,25 @@ func OnSettingsSaved(c *Core) {
} }
} }
func OnNewUser(u *User) { func OnNewUser(u *types.User) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnNewUser(structs.Map(u)) p.OnNewUser(structs.Map(u))
} }
} }
func OnNewService(s *Service) { func OnNewService(s *types.Service) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnNewService(structs.Map(s)) p.OnNewService(structs.Map(s))
} }
} }
func OnDeletedService(s *Service) { func OnDeletedService(s *types.Service) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnDeletedService(structs.Map(s)) p.OnDeletedService(structs.Map(s))
} }
} }
func OnUpdateService(s *Service) { func OnUpdateService(s *types.Service) {
for _, p := range CoreApp.AllPlugins { for _, p := range CoreApp.AllPlugins {
p.OnUpdatedService(structs.Map(s)) p.OnUpdatedService(structs.Map(s))
} }

View File

@ -3,13 +3,14 @@ package core
import ( import (
"fmt" "fmt"
"github.com/ararog/timeago" "github.com/ararog/timeago"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"strings" "strings"
"time" "time"
) )
func (s *Service) CreateFailure(data FailureData) (int64, error) { func CreateServiceFailure(s *types.Service, data FailureData) (int64, error) {
fail := &Failure{ fail := &types.Failure{
Issue: data.Issue, Issue: data.Issue,
Service: s.Id, Service: s.Id,
CreatedAt: time.Now(), CreatedAt: time.Now(),
@ -26,8 +27,8 @@ func (s *Service) CreateFailure(data FailureData) (int64, error) {
return uuid.(int64), err return uuid.(int64), err
} }
func (s *Service) SelectAllFailures() []*Failure { func SelectAllFailures(s *types.Service) []*types.Failure {
var fails []*Failure var fails []*types.Failure
col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id") col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id")
err := col.All(&fails) err := col.All(&fails)
if err != nil { if err != nil {
@ -36,7 +37,7 @@ func (s *Service) SelectAllFailures() []*Failure {
return fails return fails
} }
func (u *Service) DeleteFailures() { func DeleteFailures(u *types.Service) {
var fails []*Failure var fails []*Failure
col := DbSession.Collection("failures") col := DbSession.Collection("failures")
col.Find("service", u.Id).All(&fails) col.Find("service", u.Id).All(&fails)
@ -45,26 +46,29 @@ func (u *Service) DeleteFailures() {
} }
} }
func (s *Service) LimitedFailures() []*Failure { func (ser *Service) LimitedFailures() []*types.Failure {
var fails []*Failure s := ser.ToService()
var fails []*types.Failure
col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id").Limit(10) col := DbSession.Collection("failures").Find("service", s.Id).OrderBy("-id").Limit(10)
col.All(&fails) col.All(&fails)
return fails return fails
} }
func reverseFailures(input []*Failure) []*Failure { func reverseFailures(input []*types.Failure) []*types.Failure {
if len(input) == 0 { if len(input) == 0 {
return input return input
} }
return append(reverseFailures(input[1:]), input[0]) return append(reverseFailures(input[1:]), input[0])
} }
func (f *Failure) Ago() string { func (fail *Failure) Ago() string {
f := fail.ToFailure()
got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt) got, _ := timeago.TimeAgoWithTime(time.Now(), f.CreatedAt)
return got return got
} }
func (f *Failure) Delete() error { func (fail *Failure) Delete() error {
f := fail.ToFailure()
col := DbSession.Collection("failures").Find("id", f.Id) col := DbSession.Collection("failures").Find("id", f.Id)
return col.Delete() return col.Delete()
} }
@ -79,19 +83,31 @@ func CountFailures() uint64 {
return amount return amount
} }
func (s *Service) TotalFailures() (uint64, error) { func (ser *Service) TotalFailures() (uint64, error) {
s := ser.ToService()
col := DbSession.Collection("failures").Find("service", s.Id) col := DbSession.Collection("failures").Find("service", s.Id)
amount, err := col.Count() amount, err := col.Count()
return amount, err return amount, err
} }
func (s *Service) TotalFailures24Hours() (uint64, error) { func (ser *Service) TotalFailures24Hours() (uint64, error) {
s := ser.ToService()
col := DbSession.Collection("failures").Find("service", s.Id) col := DbSession.Collection("failures").Find("service", s.Id)
amount, err := col.Count() amount, err := col.Count()
return amount, err return amount, err
} }
func (f *Failure) ParseError() string { func (f *Failure) ToFailure() *types.Failure {
return f.F.(*types.Failure)
}
func MakeFailure(f *types.Failure) *Failure {
fail := &Failure{f}
return fail
}
func (fail *Failure) ParseError() string {
f := fail.ToFailure()
err := strings.Contains(f.Issue, "operation timed out") err := strings.Contains(f.Issue, "operation timed out")
if err { if err {
return fmt.Sprintf("HTTP Request Timed Out") return fmt.Sprintf("HTTP Request Timed Out")

View File

@ -13,7 +13,7 @@ func hitCol() db.Collection {
return DbSession.Collection("hits") return DbSession.Collection("hits")
} }
func (s *Service) CreateHit(d HitData) (int64, error) { func CreateServiceHit(s *types.Service, d HitData) (int64, error) {
h := Hit{ h := Hit{
Service: s.Id, Service: s.Id,
Latency: d.Latency, Latency: d.Latency,
@ -27,14 +27,16 @@ func (s *Service) CreateHit(d HitData) (int64, error) {
return uuid.(int64), err return uuid.(int64), err
} }
func (s *Service) Hits() ([]Hit, error) { func (ser *Service) Hits() ([]Hit, error) {
s := ser.ToService()
var hits []Hit var hits []Hit
col := hitCol().Find("service", s.Id).OrderBy("-id") col := hitCol().Find("service", s.Id).OrderBy("-id")
err := col.All(&hits) err := col.All(&hits)
return hits, err return hits, err
} }
func (s *Service) LimitedHits() ([]*Hit, error) { func (ser *Service) LimitedHits() ([]*Hit, error) {
s := ser.ToService()
var hits []*Hit var hits []*Hit
col := hitCol().Find("service", s.Id).OrderBy("-id").Limit(1024) col := hitCol().Find("service", s.Id).OrderBy("-id").Limit(1024)
err := col.All(&hits) err := col.All(&hits)
@ -48,14 +50,16 @@ func reverseHits(input []*Hit) []*Hit {
return append(reverseHits(input[1:]), input[0]) return append(reverseHits(input[1:]), input[0])
} }
func (s *Service) SelectHitsGroupBy(group string) ([]Hit, error) { func (ser *Service) SelectHitsGroupBy(group string) ([]Hit, error) {
s := ser.ToService()
var hits []Hit var hits []Hit
col := hitCol().Find("service", s.Id) col := hitCol().Find("service", s.Id)
err := col.All(&hits) err := col.All(&hits)
return hits, err return hits, err
} }
func (s *Service) TotalHits() (uint64, error) { func (ser *Service) TotalHits() (uint64, error) {
s := ser.ToService()
col := hitCol().Find("service", s.Id) col := hitCol().Find("service", s.Id)
amount, err := col.Count() amount, err := col.Count()
return amount, err return amount, err

View File

@ -10,33 +10,12 @@ import (
"upper.io/db.v3" "upper.io/db.v3"
) )
type Failure types.Failure
type Service struct { type Service struct {
Id int64 `db:"id,omitempty" json:"id"` S interface{}
Name string `db:"name" json:"name"` }
Domain string `db:"domain" json:"domain"`
Expected string `db:"expected" json:"expected"` type Failure struct {
ExpectedStatus int `db:"expected_status" json:"expected_status"` F interface{}
Interval int `db:"check_interval" json:"check_interval"`
Type string `db:"check_type" json:"type"`
Method string `db:"method" json:"method"`
PostData string `db:"post_data" json:"post_data"`
Port int `db:"port" json:"port"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Online bool `json:"online"`
Latency float64 `json:"latency"`
Online24Hours float32 `json:"24_hours_online"`
AvgResponse string `json:"avg_response"`
TotalUptime string `json:"uptime"`
OrderId int64 `json:"order_id"`
Failures []*Failure `json:"failures"`
Checkins []*Checkin `json:"checkins"`
stopRoutine chan struct{}
LastResponse string
LastStatusCode int
LastOnline time.Time
dnsLookup float64 `json:"dns_lookup_time"`
} }
func serviceCol() db.Collection { func serviceCol() db.Collection {
@ -44,28 +23,32 @@ func serviceCol() db.Collection {
} }
func SelectService(id int64) *Service { func SelectService(id int64) *Service {
for _, s := range CoreApp.Services { for _, ser := range CoreApp.Services {
s := ser.ToService()
if s.Id == id { if s.Id == id {
return s return ser
} }
} }
return nil return nil
} }
func SelectAllServices() ([]*Service, error) { func SelectAllServices() ([]*Service, error) {
var srvcs []*Service var services []*types.Service
var sers []*Service
col := serviceCol().Find() col := serviceCol().Find()
err := col.All(&srvcs) err := col.All(&services)
if err != nil { if err != nil {
utils.Log(3, err) utils.Log(3, err)
return nil, err return nil, err
} }
for _, s := range srvcs { for _, s := range services {
s.Checkins = s.SelectAllCheckins() ser := NewService(s)
s.Failures = s.SelectAllFailures() sers = append(sers, ser)
s.Checkins = SelectAllCheckins(s)
s.Failures = SelectAllFailures(s)
} }
CoreApp.Services = srvcs CoreApp.Services = sers
return srvcs, err return sers, err
} }
func (s *Service) AvgTime() float64 { func (s *Service) AvgTime() float64 {
@ -80,9 +63,10 @@ func (s *Service) AvgTime() float64 {
return val return val
} }
func (s *Service) Online24() float32 { func (ser *Service) Online24() float32 {
total, _ := s.TotalHits() s := ser.S.(*types.Service)
failed, _ := s.TotalFailures24Hours() total, _ := ser.TotalHits()
failed, _ := ser.TotalFailures24Hours()
if failed == 0 { if failed == 0 {
s.Online24Hours = 100.00 s.Online24Hours = 100.00
return s.Online24Hours return s.Online24Hours
@ -106,12 +90,23 @@ type DateScan struct {
Value int64 `json:"y"` Value int64 `json:"y"`
} }
func (s *Service) SmallText() string { func (s *Service) ToService() *types.Service {
last := s.LimitedFailures() return s.S.(*types.Service)
hits, _ := s.LimitedHits() }
func NewService(s *types.Service) *Service {
ser := &Service{s}
return ser
}
func (ser *Service) SmallText() string {
s := ser.ToService()
last := ser.LimitedFailures()
hits, _ := ser.LimitedHits()
if !s.Online { if !s.Online {
if len(last) > 0 { if len(last) > 0 {
return fmt.Sprintf("%v on %v", last[0].ParseError(), last[0].CreatedAt.Format("Monday 3:04PM, Jan _2 2006")) lastFailure := MakeFailure(last[0])
return fmt.Sprintf("%v on %v", lastFailure.ParseError(), last[0].CreatedAt.Format("Monday 3:04PM, Jan _2 2006"))
} else { } else {
return fmt.Sprintf("%v is currently offline", s.Name) return fmt.Sprintf("%v is currently offline", s.Name)
} }
@ -138,7 +133,8 @@ func GroupDataBy(column string, id int64, tm time.Time, increment string) string
return sql return sql
} }
func (s *Service) GraphData() string { func (ser *Service) GraphData() string {
s := ser.ToService()
var d []*DateScan var d []*DateScan
since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0) since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0)
@ -172,9 +168,10 @@ func (s *Service) GraphData() string {
return string(data) return string(data)
} }
func (s *Service) AvgUptime() string { func (ser *Service) AvgUptime() string {
failed, _ := s.TotalFailures() s := ser.ToService()
total, _ := s.TotalHits() failed, _ := ser.TotalFailures()
total, _ := ser.TotalHits()
if failed == 0 { if failed == 0 {
s.TotalUptime = "100" s.TotalUptime = "100"
return s.TotalUptime return s.TotalUptime
@ -195,18 +192,19 @@ func (s *Service) AvgUptime() string {
return s.TotalUptime return s.TotalUptime
} }
func (u *Service) RemoveArray() []*Service { func RemoveArray(u *types.Service) []*Service {
var srvcs []*Service var srvcs []*Service
for _, s := range CoreApp.Services { for _, ser := range CoreApp.Services {
s := ser.ToService()
if s.Id != u.Id { if s.Id != u.Id {
srvcs = append(srvcs, s) srvcs = append(srvcs, ser)
} }
} }
CoreApp.Services = srvcs CoreApp.Services = srvcs
return srvcs return srvcs
} }
func (u *Service) Delete() error { func DeleteService(u *types.Service) error {
res := serviceCol().Find("id", u.Id) res := serviceCol().Find("id", u.Id)
err := res.Delete() err := res.Delete()
if err != nil { if err != nil {
@ -214,28 +212,27 @@ func (u *Service) Delete() error {
return err return err
} }
utils.Log(1, fmt.Sprintf("Stopping %v Monitoring...", u.Name)) utils.Log(1, fmt.Sprintf("Stopping %v Monitoring...", u.Name))
if u.stopRoutine != nil { if u.StopRoutine != nil {
close(u.stopRoutine) close(u.StopRoutine)
} }
utils.Log(1, fmt.Sprintf("Stopped %v Monitoring Service", u.Name)) utils.Log(1, fmt.Sprintf("Stopped %v Monitoring Service", u.Name))
u.RemoveArray() RemoveArray(u)
OnDeletedService(u) OnDeletedService(u)
return err return err
} }
func (u *Service) Update(s *Service) *Service { func UpdateService(u *types.Service) *types.Service {
s.CreatedAt = time.Now() u.CreatedAt = time.Now()
res := serviceCol().Find("id", u.Id) res := serviceCol().Find("id", u.Id)
err := res.Update(s) err := res.Update(u)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", u.Name, err)) utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", u.Name, err))
} }
*u = *s
OnUpdateService(u) OnUpdateService(u)
return u return u
} }
func (u *Service) Create() (int64, error) { func CreateService(u *types.Service) (int64, error) {
u.CreatedAt = time.Now() u.CreatedAt = time.Now()
uuid, err := serviceCol().Insert(u) uuid, err := serviceCol().Insert(u)
if uuid == nil { if uuid == nil {
@ -243,15 +240,17 @@ func (u *Service) Create() (int64, error) {
return 0, err return 0, err
} }
u.Id = uuid.(int64) u.Id = uuid.(int64)
u.stopRoutine = make(chan struct{}) u.StopRoutine = make(chan struct{})
CoreApp.Services = append(CoreApp.Services, u) nn := &Service{u}
CoreApp.Services = append(CoreApp.Services, nn)
return uuid.(int64), err return uuid.(int64), err
} }
func CountOnline() int { func CountOnline() int {
amount := 0 amount := 0
for _, v := range CoreApp.Services { for _, ser := range CoreApp.Services {
if v.Online { s := ser.ToService()
if s.Online {
amount++ amount++
} }
} }

View File

@ -2,6 +2,7 @@ package core
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"os" "os"
) )
@ -19,7 +20,7 @@ type ErrorResponse struct {
func LoadSampleData() error { func LoadSampleData() error {
utils.Log(1, "Inserting Sample Data...") utils.Log(1, "Inserting Sample Data...")
s1 := &Service{ s1 := &types.Service{
Name: "Google", Name: "Google",
Domain: "https://google.com", Domain: "https://google.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -28,7 +29,7 @@ func LoadSampleData() error {
Type: "http", Type: "http",
Method: "GET", Method: "GET",
} }
s2 := &Service{ s2 := &types.Service{
Name: "Statup Github", Name: "Statup Github",
Domain: "https://github.com/hunterlong/statup", Domain: "https://github.com/hunterlong/statup",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -37,7 +38,7 @@ func LoadSampleData() error {
Type: "http", Type: "http",
Method: "GET", Method: "GET",
} }
s3 := &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,
@ -46,7 +47,7 @@ func LoadSampleData() error {
Type: "http", Type: "http",
Method: "GET", Method: "GET",
} }
s4 := &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,
@ -56,19 +57,19 @@ func LoadSampleData() error {
Method: "POST", Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`, PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
} }
id, err := s1.Create() id, err := CreateService(s1)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
} }
id, err = s2.Create() id, err = CreateService(s2)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
} }
id, err = s3.Create() id, err = CreateService(s3)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
} }
id, err = s4.Create() id, err = CreateService(s4)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
} }

View File

@ -10,36 +10,36 @@ import (
type User types.User type User types.User
func SelectUser(id int64) (*User, error) { func SelectUser(id int64) (*types.User, error) {
var user User var user *types.User
col := DbSession.Collection("users") col := DbSession.Collection("users")
res := col.Find("id", id) res := col.Find("id", id)
err := res.One(&user) err := res.One(&user)
return &user, err return user, err
} }
func SelectUsername(username string) (*User, error) { func SelectUsername(username string) (*types.User, error) {
var user User var user *types.User
col := DbSession.Collection("users") col := DbSession.Collection("users")
res := col.Find("username", username) res := col.Find("username", username)
err := res.One(&user) err := res.One(&user)
return &user, err return user, err
} }
func (u *User) Delete() error { func DeleteUser(u *types.User) error {
col := DbSession.Collection("users") col := DbSession.Collection("users")
user := col.Find("id", u.Id) user := col.Find("id", u.Id)
return user.Delete() return user.Delete()
} }
func (u *User) Update() error { func UpdateUser(u *types.User) error {
u.CreatedAt = time.Now() u.CreatedAt = time.Now()
col := DbSession.Collection("users") col := DbSession.Collection("users")
user := col.Find("id", u.Id) user := col.Find("id", u.Id)
return user.Update(u) return user.Update(u)
} }
func (u *User) Create() (int64, error) { func CreateUser(u *types.User) (int64, error) {
u.CreatedAt = time.Now() u.CreatedAt = time.Now()
u.Password = utils.HashPassword(u.Password) u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5) u.ApiKey = utils.NewSHA1Hash(5)
@ -63,7 +63,7 @@ func SelectAllUsers() ([]User, error) {
return users, err return users, err
} }
func AuthUser(username, password string) (*User, bool) { func AuthUser(username, password string) (*types.User, bool) {
var auth bool var auth bool
user, err := SelectUsername(username) user, err := SelectUsername(username)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
) )
@ -23,7 +24,7 @@ func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) {
} }
vars := mux.Vars(r) vars := mux.Vars(r)
checkin := core.FindCheckin(vars["api"]) checkin := core.FindCheckin(vars["api"])
checkin.Receivehit() //checkin.Receivehit()
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(checkin) json.NewEncoder(w).Encode(checkin)
} }
@ -44,11 +45,12 @@ func ApiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
var s core.Service var s *types.Service
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
decoder.Decode(&s) decoder.Decode(&s)
service.Update(&s) service := serv.ToService()
core.UpdateService(service)
json.NewEncoder(w).Encode(s) json.NewEncoder(w).Encode(s)
} }

View File

@ -6,8 +6,7 @@ import (
) )
type index struct { type index struct {
Core core.Core Core *core.Core
Services []*core.Service
} }
func IndexHandler(w http.ResponseWriter, r *http.Request) { func IndexHandler(w http.ResponseWriter, r *http.Request) {
@ -15,6 +14,5 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/setup", http.StatusSeeOther) http.Redirect(w, r, "/setup", http.StatusSeeOther)
return return
} }
out := index{*core.CoreApp, core.CoreApp.Services} ExecuteResponse(w, r, "index.html", core.CoreApp)
ExecuteResponse(w, r, "index.html", out)
} }

View File

@ -28,7 +28,8 @@ func PrometheusHandler(w http.ResponseWriter, r *http.Request) {
system := fmt.Sprintf("statup_total_failures %v\n", core.CountFailures()) system := fmt.Sprintf("statup_total_failures %v\n", core.CountFailures())
system += fmt.Sprintf("statup_total_services %v", len(core.CoreApp.Services)) system += fmt.Sprintf("statup_total_services %v", len(core.CoreApp.Services))
metrics = append(metrics, system) metrics = append(metrics, system)
for _, v := range core.CoreApp.Services { for _, ser := range core.CoreApp.Services {
v := ser.ToService()
online := 1 online := 1
if !v.Online { if !v.Online {
online = 0 online = 0

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
"strconv" "strconv"
@ -40,7 +41,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
checkType := r.PostForm.Get("check_type") checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data") postData := r.PostForm.Get("post_data")
service := &core.Service{ service := &types.Service{
Name: name, Name: name,
Domain: domain, Domain: domain,
Method: method, Method: method,
@ -51,12 +52,12 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
Port: port, Port: port,
PostData: postData, PostData: postData,
} }
_, err := service.Create() _, err := core.CreateService(service)
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error starting %v check routine. %v", service.Name, err)) utils.Log(3, fmt.Sprintf("Error starting %v check routine. %v", service.Name, err))
} }
go service.CheckQueue() go core.CheckQueue(service)
core.OnNewService(service) core.OnNewService(service)
http.Redirect(w, r, "/services", http.StatusSeeOther) http.Redirect(w, r, "/services", http.StatusSeeOther)
@ -68,21 +69,22 @@ func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
service.Delete() service := serv.ToService()
core.DeleteService(service)
http.Redirect(w, r, "/services", http.StatusSeeOther) http.Redirect(w, r, "/services", http.StatusSeeOther)
} }
func ServicesViewHandler(w http.ResponseWriter, r *http.Request) { func ServicesViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
ExecuteResponse(w, r, "service.html", service) ExecuteResponse(w, r, "service.html", serv)
} }
func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) { func ServicesBadgeHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
service := serv.ToService()
var badge []byte var badge []byte
if service.Online { if service.Online {
badge = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="104" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h50v20H54z"/><path fill="url(#b)" d="M0 0h104v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="28" y="15" fill="#010101" fill-opacity=".3">` + service.Name + `</text><text x="28" y="14">` + service.Name + `</text><text x="78" y="15" fill="#010101" fill-opacity=".3">online</text><text x="78" y="14">online</text></g></svg>`) badge = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="104" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h54v20H0z"/><path fill="#4c1" d="M54 0h50v20H54z"/><path fill="url(#b)" d="M0 0h104v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="28" y="15" fill="#010101" fill-opacity=".3">` + service.Name + `</text><text x="28" y="14">` + service.Name + `</text><text x="78" y="15" fill="#010101" fill-opacity=".3">online</text><text x="78" y="14">online</text></g></svg>`)
@ -103,7 +105,8 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
service := serv.ToService()
r.ParseForm() r.ParseForm()
name := r.PostForm.Get("name") name := r.PostForm.Get("name")
domain := r.PostForm.Get("domain") domain := r.PostForm.Get("domain")
@ -114,7 +117,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
port, _ := strconv.Atoi(r.PostForm.Get("port")) port, _ := strconv.Atoi(r.PostForm.Get("port"))
checkType := r.PostForm.Get("check_type") checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data") postData := r.PostForm.Get("post_data")
serviceUpdate := &core.Service{ serviceUpdate := &types.Service{
Id: service.Id, Id: service.Id,
Name: name, Name: name,
Domain: domain, Domain: domain,
@ -126,7 +129,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
Port: port, Port: port,
PostData: postData, PostData: postData,
} }
service = service.Update(serviceUpdate) service = core.UpdateService(serviceUpdate)
ExecuteResponse(w, r, "service.html", service) ExecuteResponse(w, r, "service.html", service)
} }
@ -136,9 +139,9 @@ func ServicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
vars := mux.Vars(r) vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
service := serv.ToService()
service.DeleteFailures() core.DeleteFailures(service)
core.CoreApp.Services, _ = core.SelectAllServices() core.CoreApp.Services, _ = core.SelectAllServices()
http.Redirect(w, r, "/services", http.StatusSeeOther) http.Redirect(w, r, "/services", http.StatusSeeOther)
} }
@ -150,7 +153,8 @@ func CheckinCreateUpdateHandler(w http.ResponseWriter, r *http.Request) {
} }
vars := mux.Vars(r) vars := mux.Vars(r)
interval := utils.StringInt(r.PostForm.Get("interval")) interval := utils.StringInt(r.PostForm.Get("interval"))
service := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
service := serv.ToService()
checkin := &core.Checkin{ checkin := &core.Checkin{
Service: service.Id, Service: service.Id,
Interval: interval, Interval: interval,

View File

@ -104,13 +104,13 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
admin := &core.User{ admin := &types.User{
Username: config.Username, Username: config.Username,
Password: config.Password, Password: config.Password,
Email: config.Email, Email: config.Email,
Admin: true, Admin: true,
} }
admin.Create() core.CreateUser(admin)
if sample == "on" { if sample == "on" {
core.LoadSampleData() core.LoadSampleData()

View File

@ -64,7 +64,7 @@ func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
if password != "##########" { if password != "##########" {
user.Password = utils.HashPassword(password) user.Password = utils.HashPassword(password)
} }
user.Update() core.UpdateUser(user)
users, _ := core.SelectAllUsers() users, _ := core.SelectAllUsers()
ExecuteResponse(w, r, "users.html", users) ExecuteResponse(w, r, "users.html", users)
} }
@ -80,13 +80,13 @@ func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
email := r.PostForm.Get("email") email := r.PostForm.Get("email")
admin := r.PostForm.Get("admin") admin := r.PostForm.Get("admin")
user := &core.User{ user := &types.User{
Username: username, Username: username,
Password: password, Password: password,
Email: email, Email: email,
Admin: (admin == "on"), Admin: (admin == "on"),
} }
_, err := user.Create() _, err := core.CreateUser(user)
if err != nil { if err != nil {
utils.Log(2, err) utils.Log(2, err)
} }
@ -108,6 +108,6 @@ func UsersDeleteHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/users", http.StatusSeeOther) http.Redirect(w, r, "/users", http.StatusSeeOther)
return return
} }
user.Delete() core.DeleteUser(user)
http.Redirect(w, r, "/users", http.StatusSeeOther) http.Redirect(w, r, "/users", http.StatusSeeOther)
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/handlers" "github.com/hunterlong/statup/handlers"
"github.com/hunterlong/statup/notifiers" "github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/types"
"github.com/rendon/testcli" "github.com/rendon/testcli"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http" "net/http"
@ -31,7 +32,7 @@ func RunInit(t *testing.T) {
core.CoreApp = core.NewCore() core.CoreApp = core.NewCore()
} }
var forceSequential chan bool = make(chan bool, 1) var forceSequential = make(chan bool, 1)
func TestRunAll(t *testing.T) { func TestRunAll(t *testing.T) {
@ -267,22 +268,22 @@ func RunUser_SelectAll(t *testing.T) {
} }
func RunUser_Create(t *testing.T) { func RunUser_Create(t *testing.T) {
user := &core.User{ user := &types.User{
Username: "admin", Username: "admin",
Password: "admin", Password: "admin",
Email: "info@testuser.com", Email: "info@testuser.com",
Admin: true, Admin: true,
} }
id, err := user.Create() id, err := core.CreateUser(user)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(1), id) assert.Equal(t, int64(1), id)
user2 := &core.User{ user2 := &types.User{
Username: "superadmin", Username: "superadmin",
Password: "admin", Password: "admin",
Email: "info@adminer.com", Email: "info@adminer.com",
Admin: true, Admin: true,
} }
id, err = user2.Create() id, err = core.CreateUser(user2)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(2), id) assert.Equal(t, int64(2), id)
} }
@ -291,7 +292,7 @@ func RunUser_Update(t *testing.T) {
user, err := core.SelectUser(1) user, err := core.SelectUser(1)
user.Email = "info@updatedemail.com" user.Email = "info@updatedemail.com"
assert.Nil(t, err) assert.Nil(t, err)
err = user.Update() err = core.UpdateUser(user)
assert.Nil(t, err) assert.Nil(t, err)
updatedUser, err := core.SelectUser(1) updatedUser, err := core.SelectUser(1)
assert.Nil(t, err) assert.Nil(t, err)
@ -299,12 +300,12 @@ func RunUser_Update(t *testing.T) {
} }
func RunUser_NonUniqueCreate(t *testing.T) { func RunUser_NonUniqueCreate(t *testing.T) {
user := &core.User{ user := &types.User{
Username: "admin", Username: "admin",
Password: "admin", Password: "admin",
Email: "info@testuser.com", Email: "info@testuser.com",
} }
_, err := user.Create() _, err := core.CreateUser(user)
assert.NotNil(t, err) assert.NotNil(t, err)
} }
@ -312,7 +313,7 @@ func RunUser_Delete(t *testing.T) {
user, err := core.SelectUser(2) user, err := core.SelectUser(2)
assert.Nil(t, err) assert.Nil(t, err)
assert.NotNil(t, user) assert.NotNil(t, user)
err = user.Delete() err = core.DeleteUser(user)
assert.Nil(t, err) assert.Nil(t, err)
} }
@ -327,11 +328,11 @@ func RunOneService_Check(t *testing.T) {
service := core.SelectService(1) service := core.SelectService(1)
assert.NotNil(t, service) assert.NotNil(t, service)
t.Log(service) t.Log(service)
assert.Equal(t, "Google", service.Name) assert.Equal(t, "Google", service.ToService().Name)
} }
func RunService_Create(t *testing.T) { func RunService_Create(t *testing.T) {
service := &core.Service{ service := &types.Service{
Name: "test service", Name: "test service",
Domain: "https://google.com", Domain: "https://google.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -340,7 +341,7 @@ func RunService_Create(t *testing.T) {
Type: "http", Type: "http",
Method: "GET", Method: "GET",
} }
id, err := service.Create() id, err := core.CreateService(service)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(5), id) assert.Equal(t, int64(5), id)
t.Log(service) t.Log(service)
@ -371,7 +372,7 @@ func RunService_GraphData(t *testing.T) {
} }
func RunBadService_Create(t *testing.T) { func RunBadService_Create(t *testing.T) {
service := &core.Service{ service := &types.Service{
Name: "Bad Service", Name: "Bad Service",
Domain: "https://9839f83h72gey2g29278hd2od2d.com", Domain: "https://9839f83h72gey2g29278hd2od2d.com",
ExpectedStatus: 200, ExpectedStatus: 200,
@ -380,7 +381,7 @@ func RunBadService_Create(t *testing.T) {
Type: "http", Type: "http",
Method: "GET", Method: "GET",
} }
id, err := service.Create() id, err := core.CreateService(service)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(6), id) assert.Equal(t, int64(6), id)
} }
@ -388,14 +389,14 @@ func RunBadService_Create(t *testing.T) {
func RunBadService_Check(t *testing.T) { func RunBadService_Check(t *testing.T) {
service := core.SelectService(4) service := core.SelectService(4)
assert.NotNil(t, service) assert.NotNil(t, service)
assert.Equal(t, "JSON API Tester", service.Name) assert.Equal(t, "JSON API Tester", service.ToService().Name)
} }
func RunDeleteService(t *testing.T) { func RunDeleteService(t *testing.T) {
service := core.SelectService(4) service := core.SelectService(4)
assert.NotNil(t, service) assert.NotNil(t, service)
assert.Equal(t, "JSON API Tester", service.Name) assert.Equal(t, "JSON API Tester", service.ToService().Name)
err := service.Delete() err := core.DeleteService(service.ToService())
assert.Nil(t, err) assert.Nil(t, err)
} }
@ -405,7 +406,7 @@ func RunCreateService_Hits(t *testing.T) {
assert.NotNil(t, services) assert.NotNil(t, services)
for i := 0; i <= 10; i++ { for i := 0; i <= 10; i++ {
for _, s := range services { for _, s := range services {
service := s.Check() service := core.ServiceCheck(s.ToService())
assert.NotNil(t, service) assert.NotNil(t, service)
} }
} }
@ -423,7 +424,7 @@ func RunService_Failures(t *testing.T) {
t.SkipNow() t.SkipNow()
service := core.SelectService(6) service := core.SelectService(6)
assert.NotNil(t, service) assert.NotNil(t, service)
assert.NotEmpty(t, service.Failures) assert.NotEmpty(t, service.ToService().Failures)
} }
func RunService_LimitedHits(t *testing.T) { func RunService_LimitedHits(t *testing.T) {

View File

@ -144,7 +144,7 @@ func (u *Email) Run() error {
} }
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Email) OnFailure(data map[string]interface{}) error { func (u *Email) OnFailure(s *types.Service) error {
if u.Enabled { if u.Enabled {
utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method))
// Do failing stuff here! // Do failing stuff here!
@ -153,7 +153,7 @@ func (u *Email) OnFailure(data map[string]interface{}) error {
} }
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS
func (u *Email) OnSuccess(data map[string]interface{}) error { func (u *Email) OnSuccess(s *types.Service) error {
if u.Enabled { if u.Enabled {
utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method)) utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method))
// Do failing stuff here! // Do failing stuff here!

View File

@ -2,6 +2,7 @@ package notifiers
import ( import (
"fmt" "fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"strings" "strings"
"time" "time"
@ -54,8 +55,8 @@ type Notifier interface {
Init() error Init() error
Install() error Install() error
Run() error Run() error
OnFailure(map[string]interface{}) error OnFailure(*types.Service) error
OnSuccess(map[string]interface{}) error OnSuccess(*types.Service) error
Select() *Notification Select() *Notification
Test() error Test() error
} }
@ -81,7 +82,6 @@ func SelectNotification(id int64) (*Notification, error) {
func (n *Notification) Update() (*Notification, error) { func (n *Notification) Update() (*Notification, error) {
n.CreatedAt = time.Now() n.CreatedAt = time.Now()
err := Collections.Find("id", n.Id).Update(n) err := Collections.Find("id", n.Id).Update(n)
return n, err return n, err
} }
@ -148,26 +148,36 @@ func (n *Notification) GetValue(dbField string) string {
return "" return ""
} }
func OnFailure(data map[string]interface{}) { func OnFailure(s *types.Service) {
for _, comm := range AllCommunications { for _, comm := range AllCommunications {
n := comm.(Notifier) n := comm.(Notifier)
n.OnFailure(data) n.OnFailure(s)
} }
} }
func OnSuccess(data map[string]interface{}) { func OnSuccess(s *types.Service) {
for _, comm := range AllCommunications { for _, comm := range AllCommunications {
n := comm.(Notifier) n := comm.(Notifier)
n.OnSuccess(data) n.OnSuccess(s)
} }
} }
func uniqueMessages(arr []string, v string) []string { func uniqueStrings(elements []string) []string {
var newArray []string result := []string{}
for _, i := range arr {
if i != v { for i := 0; i < len(elements); i++ {
newArray = append(newArray, v) // Scan slice for a previous element of the same value.
exists := false
for v := 0; v < i; v++ {
if elements[v][:10] == elements[i][:10] {
exists = true
break
}
}
// If no previous element exists, append this one.
if !exists {
result = append(result, elements[i])
} }
} }
return newArray return result
} }

View File

@ -3,28 +3,37 @@ package notifiers
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
"time" "sync"
"text/template" "text/template"
"time"
) )
const ( const (
SLACK_ID = 2 SLACK_ID = 2
SLACK_METHOD = "slack" SLACK_METHOD = "slack"
SERVICE_TEMPLATE = `{ "attachments": [ { "fallback": "ReferenceError - UI is not defined: https://honeybadger.io/path/to/event/", "text": "<https://honeybadger.io/path/to/event/|Google> - Your Statup service 'Google' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Response", "value": "production", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": 123456789 } ] }` FAILING_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected", "value": "{{.Service.Expected}}", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#FF0000", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": {{.Time}} } ] }`
TEST_TEMPLATE = `{"text":"%{{.Message}}"}` SUCCESS_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#00FF00", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": {{.Time}} } ] }`
TEST_TEMPLATE = `{"text":"{{.}}"}`
) )
var ( var (
slacker *Slack slacker *Slack
slackMessages []string slackMessages []string
messageLock *sync.Mutex
) )
type Slack struct { type Slack struct {
*Notification *Notification
} }
type slackMessage struct {
Service *types.Service
Time int64
}
// DEFINE YOUR NOTIFICATION HERE. // DEFINE YOUR NOTIFICATION HERE.
func init() { func init() {
slacker = &Slack{&Notification{ slacker = &Slack{&Notification{
@ -40,6 +49,7 @@ func init() {
}}}, }}},
} }
add(slacker) add(slacker)
messageLock = new(sync.Mutex)
} }
// Select Obj // Select Obj
@ -49,9 +59,7 @@ func (u *Slack) Select() *Notification {
// WHEN NOTIFIER LOADS // WHEN NOTIFIER LOADS
func (u *Slack) Init() error { func (u *Slack) Init() error {
err := u.Install() err := u.Install()
if err == nil { if err == nil {
notifier, _ := SelectNotification(u.Id) notifier, _ := SelectNotification(u.Id)
forms := u.Form forms := u.Form
@ -66,21 +74,26 @@ func (u *Slack) Init() error {
} }
func (u *Slack) Test() error { func (u *Slack) Test() error {
SendSlack(TEST_TEMPLATE, nil) msg := fmt.Sprintf("You're Statup Slack Notifier is working correctly!")
SendSlack(TEST_TEMPLATE, msg)
return nil return nil
} }
// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS // AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS
func (u *Slack) Run() error { func (u *Slack) Run() error {
messageLock.Lock()
slackMessages = uniqueStrings(slackMessages)
for _, msg := range slackMessages { for _, msg := range slackMessages {
utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook: %v", msg)) utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook"))
client := http.Client{Timeout: 15 * time.Second} client := http.Client{Timeout: 15 * time.Second}
_, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(msg))) _, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(msg)))
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err)) utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err))
} }
slackMessages = uniqueMessages(slackMessages, msg)
fmt.Println(msg)
} }
messageLock.Unlock()
time.Sleep(60 * time.Second) time.Sleep(60 * time.Second)
if u.Enabled { if u.Enabled {
u.Run() u.Run()
@ -89,37 +102,35 @@ func (u *Slack) Run() error {
} }
// CUSTOM FUNCTION FO SENDING SLACK MESSAGES // CUSTOM FUNCTION FO SENDING SLACK MESSAGES
func SendSlack(temp string, data ...interface{}) error { func SendSlack(temp string, data interface{}) error {
messageLock.Lock()
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
slackTemp, _ := template.New("slack").Parse(temp) slackTemp, _ := template.New("slack").Parse(temp)
slackTemp.Execute(buf, data) slackTemp.Execute(buf, data)
slackMessages = append(slackMessages, buf.String()) slackMessages = append(slackMessages, buf.String())
messageLock.Unlock()
return nil return nil
} }
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Slack) OnFailure(data map[string]interface{}) error { func (u *Slack) OnFailure(s *types.Service) error {
if u.Enabled { if u.Enabled {
utils.Log(1, fmt.Sprintf("Notification %v is receiving a failure notification.", u.Method))
// Do failing stuff here! // Do failing stuff here!
message := slackMessage{
Service: s,
Time: time.Now().Unix(),
}
SendSlack(FAILING_TEMPLATE, message)
} }
return nil return nil
} }
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS
func (u *Slack) OnSuccess(data map[string]interface{}) error { func (u *Slack) OnSuccess(s *types.Service) error {
if u.Enabled { if u.Enabled {
utils.Log(1, fmt.Sprintf("Notification %v is receiving a successful notification. %v", u.Method, data)) SendSlack(SUCCESS_TEMPLATE, s)
//domain := data["Domain"]
//expected := data["Expected"]
//expectedStatus := data["ExpectedStatus"]
failures := data["Failures"]
response := data["LastResponse"]
fullMessage := fmt.Sprintf(`{ "attachments": [ { "fallback": "Service is currently offline", "text": "Service is currently offline", "fields": [ { "title": "Issue", "value": "%v", "short": true }, { "title": "Response", "value": "%v", "short": true } ], "color": "#FF0000", "thumb_url": "http://example.com/path/to/thumb.png", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png", "ts": %v } ] }`, failures, response, time.Now().Unix())
slackMessages = append(slackMessages, fullMessage)
// Do checking or any successful things here // Do checking or any successful things here
} }
return nil return nil

View File

@ -28,22 +28,22 @@ services:
- internet - internet
- database - database
depends_on: depends_on:
- postgres - postgres_statup
volumes: volumes:
- ./statup/app:/app - ./statup/app:/app
environment: environment:
VIRTUAL_HOST: localhost VIRTUAL_HOST: localhost
VIRTUAL_PORT: 8080 VIRTUAL_PORT: 8080
DB_CONN: postgres DB_CONN: postgres
DB_HOST: postgres DB_HOST: postgres_statup
DB_USER: statup DB_USER: statup
DB_PASS: password123 DB_PASS: password123
DB_DATABASE: statup DB_DATABASE: statup
NAME: EC2 Example NAME: EC2 Example
DESCRIPTION: This is a Statup Docker Compose instance DESCRIPTION: This is a Statup Docker Compose instance
postgres: postgres_statup:
container_name: postgres container_name: postgres_statup
image: postgres image: postgres
restart: always restart: always
networks: networks:

View File

@ -1,6 +1,6 @@
{{ range . }}{{ if .AvgTime }}var ctx_{{.Id}}=document.getElementById("service_{{.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y} {{ range . }}{{$s := .ToService}}{{ if .AvgTime }}var ctx_{{$s.Id}}=document.getElementById("service_{{$s.Id}}").getContext('2d');var chartdata=new Chart(ctx_{{$s.Id}},{type:'line',data:{datasets:[{label:'Response Time (Milliseconds)',data:{{safe .GraphData}},backgroundColor:['rgba(47, 206, 30, 0.92)'],borderColor:['rgb(47, 171, 34)'],borderWidth:1}]},options:{maintainAspectRatio:!1,scaleShowValues:!0,layout:{padding:{left:0,right:0,top:0,bottom:-10}},hover:{animationDuration:0,},responsiveAnimationDuration:0,animation:{duration:3500,onComplete:function(){var chartInstance=this.chart,ctx=chartInstance.ctx;var controller=this.chart.controller;var xAxis=controller.scales['x-axis-0'];var yAxis=controller.scales['y-axis-0'];ctx.font=Chart.helpers.fontString(Chart.defaults.global.defaultFontSize,Chart.defaults.global.defaultFontStyle,Chart.defaults.global.defaultFontFamily);ctx.textAlign='center';ctx.textBaseline='bottom';var numTicks=xAxis.ticks.length;var yOffsetStart=xAxis.width/numTicks;var halfBarWidth=(xAxis.width/(numTicks*2));xAxis.ticks.forEach(function(value,index){var xOffset=20;var yOffset=(yOffsetStart*index)+halfBarWidth;ctx.fillStyle='#e2e2e2';ctx.fillText(value,yOffset,xOffset)});this.data.datasets.forEach(function(dataset,i){var meta=chartInstance.controller.getDatasetMeta(i);var hxH=0;var hyH=0;var hxL=0;var hyL=0;var highestNum=0;var lowestnum=999999999999;meta.data.forEach(function(bar,index){var data=dataset.data[index];if(lowestnum>data.y){lowestnum=data.y;hxL=bar._model.x;hyL=bar._model.y}
if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50} if(data.y>highestNum){highestNum=data.y;hxH=bar._model.x;hyH=bar._model.y}});if(hxH>=820){hxH=820}else if(50>=hxH){hxH=50}
if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70} if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70}
ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}}) ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{$s.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}})
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@ -48,10 +48,11 @@
<div class="list-group mb-5 mt-3"> <div class="list-group mb-5 mt-3">
{{ range .Services }} {{ range .Services }}
{{ $s := .ToService }}
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start"> <a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{.Name}}</h5> <h5 class="mb-1">{{$s.Name}}</h5>
<small>{{if .Online}} <span class="badge badge-success">ONLINE</span> {{else}} <span class="badge badge-danger">OFFLINE</span> {{end}}</small> <small>{{if $s.Online}} <span class="badge badge-success">ONLINE</span> {{else}} <span class="badge badge-danger">OFFLINE</span> {{end}}</small>
</div> </div>
<p class="mb-1">{{.SmallText}}</p> <p class="mb-1">{{.SmallText}}</p>
</a> </a>
@ -62,7 +63,7 @@
<h3>Latest Failures</h3> <h3>Latest Failures</h3>
{{ range .Services }} {{ range .Services }}
{{ $s := .ToService }}
{{ if .LimitedFailures }} {{ if .LimitedFailures }}
<div class="list-group mt-3"> <div class="list-group mt-3">
{{ range .LimitedFailures }} {{ range .LimitedFailures }}

View File

@ -12,25 +12,27 @@
<link rel="stylesheet" href="/css/base.css"> <link rel="stylesheet" href="/css/base.css">
{{end}} {{end}}
<title>{{.Core.Name}} Status</title> <title>{{.Name}} Status</title>
</head> </head>
<body> <body>
<div class="container col-md-7 col-sm-12 mt-2 sm-container"> <div class="container col-md-7 col-sm-12 mt-2 sm-container">
<h1 class="col-12 text-center mb-4 mt-sm-3 header-title">{{.Core.Name}}</h1> <h1 class="col-12 text-center mb-4 mt-sm-3 header-title">{{.Name}}</h1>
{{ if .Core.Description }} {{ if .Description }}
<h5 class="col-12 text-center mb-5 header-desc">{{ .Core.Description }}</h5> <h5 class="col-12 text-center mb-5 header-desc">{{ .Description }}</h5>
{{ end }} {{ end }}
<div class="col-12 full-col-12 mb-5"> <div class="col-12 full-col-12 mb-5">
<div class="list-group online_list"> <div class="list-group online_list">
{{ range .Services }} {{ range .Services }}
<a href="#" class="service_li list-group-item list-group-item-action {{if not .Online}}bg-danger text-white{{ end }}" data-id="{{.Id}}"> {{ $s := .ToService }}
{{ .Name }}
{{if .Online}} <a href="#" class="service_li list-group-item list-group-item-action {{if not $s.Online}}bg-danger text-white{{ end }}" data-id="{{$s.Id}}">
{{ $s.Name }}
{{if $s.Online}}
<span class="badge bg-success float-right pulse-glow">ONLINE</span> <span class="badge bg-success float-right pulse-glow">ONLINE</span>
{{ else }} {{ else }}
<span class="badge bg-white text-black-50 float-right pulse">OFFLINE</span> <span class="badge bg-white text-black-50 float-right pulse">OFFLINE</span>
@ -51,12 +53,13 @@
</div> </div>
{{end}} {{end}}
{{ range .Services }} {{ range .Services }}
<div class="mt-4" id="service_id_{{.Id}}"> {{ $s := .ToService }}
<div class="mt-4" id="service_id_{{$s.Id}}">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="col-12"> <div class="col-12">
<h4 class="mt-3"><a href="/service/{{.Id}}"{{if not .Online}} class="text-danger"{{end}}>{{ .Name }}</a> <h4 class="mt-3"><a href="/service/{{$s.Id}}"{{if not $s.Online}} class="text-danger"{{end}}>{{ $s.Name }}</a>
{{if .Online}} {{if $s.Online}}
<span class="badge bg-success float-right">ONLINE</span> <span class="badge bg-success float-right">ONLINE</span>
{{ else }} {{ else }}
<span class="badge bg-danger float-right pulse">OFFLINE</span> <span class="badge bg-danger float-right pulse">OFFLINE</span>
@ -84,13 +87,13 @@
</div> </div>
{{ if .AvgTime }} {{ if .AvgTime }}
<div class="chart-container"> <div class="chart-container">
<canvas id="service_{{ .Id }}"></canvas> <canvas id="service_{{ $s.Id }}"></canvas>
</div> </div>
{{ end }} {{ end }}
<div class="lower_canvas full-col-12 text-white{{if not .Online}} bg-danger{{end}}"> <div class="lower_canvas full-col-12 text-white{{if not $s.Online}} bg-danger{{end}}">
<div class="col-12"> <div class="col-12">
<span class="col-10 d-none d-md-inline">{{.SmallText}}</span> <span class="col-10 d-none d-md-inline">{{.SmallText}}</span>
<a href="/service/{{ .Id }}" class="btn {{if .Online}}btn-success{{else}}btn-danger{{end}} btn-sm float-right col-sm-4 col-md-2 dyn-dark">View Service</a> <a href="/service/{{ $s.Id }}" class="btn {{if $s.Online}}btn-success{{else}}btn-danger{{end}} btn-sm float-right col-sm-4 col-md-2 dyn-dark">View Service</a>
</div> </div>
</div> </div>
</div> </div>
@ -115,9 +118,9 @@
{{end}} {{end}}
<script src="/charts.js"></script> <script src="/charts.js"></script>
{{ if .Core.Style }} {{ if .Style }}
<style> <style>
{{ safe .Core.Style }} {{ safe .Style }}
</style> </style>
{{ end }} {{ end }}

View File

@ -1,4 +1,4 @@
<!doctype html> {{ $s := .ToService }}<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -13,7 +13,7 @@
<script src="/js/Chart.bundle.min.js"></script> <script src="/js/Chart.bundle.min.js"></script>
{{end}} {{end}}
<title>Statup | {{.Name}} Service</title> <title>Statup | {{$s.Name}} Service</title>
</head> </head>
<body> <body>
@ -25,14 +25,14 @@
<div class="col-12 mb-4"> <div class="col-12 mb-4">
{{if .Online }} {{if $s.Online }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span> <span class="mt-3 mb-3 text-white d-md-none btn bg-success d-block d-md-none">ONLINE</span>
{{ else }} {{ else }}
<span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span> <span class="mt-3 mb-3 text-white d-md-none btn bg-danger d-block d-md-none">OFFLINE</span>
{{end}} {{end}}
<h4 class="mt-2">{{ .Name }} <h4 class="mt-2">{{ $s.Name }}
{{if .Online }} {{if $s.Online }}
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span> <span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
{{ else }} {{ else }}
<span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span> <span class="badge bg-danger float-right d-none d-md-block">OFFLINE</span>
@ -81,65 +81,65 @@
<h3>Edit Service</h3> <h3>Edit Service</h3>
<form action="/service/{{.Id}}" method="POST"> <form action="/service/{{$s.Id}}" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label> <label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="name" class="form-control" id="service_name" value="{{.Name}}" placeholder="Name" required> <input type="text" name="name" class="form-control" id="service_name" value="{{$s.Name}}" placeholder="Name" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="check_type" class="form-control" id="service_type" value="{{.Type}}"> <select name="check_type" class="form-control" id="service_type" value="{{$s.Type}}">
<option value="http" {{if eq .Type "http"}}selected{{end}}>HTTP Service</option> <option value="http" {{if eq $s.Type "http"}}selected{{end}}>HTTP Service</option>
<option value="tcp" {{if eq .Type "tcp"}}selected{{end}}>TCP Service</option> <option value="tcp" {{if eq $s.Type "tcp"}}selected{{end}}>TCP Service</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label> <label for="service_url" class="col-sm-4 col-form-label">Application Endpoint (URL)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="service_url" value="{{.Domain}}" placeholder="https://google.com" required> <input type="text" name="domain" class="form-control" id="service_url" value="{{$s.Domain}}" placeholder="https://google.com" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{.Method}}"> <select name="method" class="form-control" id="service_check_type" value="{{$s.Method}}">
<option value="GET" {{if eq .Method "GET"}}selected{{end}}>GET</option> <option value="GET" {{if eq $s.Method "GET"}}selected{{end}}>GET</option>
<option value="POST" {{if eq .Method "POST"}}selected{{end}}>POST</option> <option value="POST" {{if eq $s.Method "POST"}}selected{{end}}>POST</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="post_data" class="col-sm-4 col-form-label">Post Data (JSON)</label> <label for="post_data" class="col-sm-4 col-form-label">Post Data (JSON)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3">{{.PostData}}</textarea> <textarea name="post_data" class="form-control" id="post_data" rows="3">{{$s.PostData}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label> <label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3">{{.Expected}}</textarea> <textarea name="expected" class="form-control" id="service_response" rows="3">{{$s.Expected}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code"> <input type="number" name="expected_status" class="form-control" value="{{$s.ExpectedStatus}}" id="service_response_code">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label> <label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{.Port}}" id="service_port" placeholder="8080"> <input type="number" name="port" class="form-control" value="{{$s.Port}}" id="service_port" placeholder="8080">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label> <label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" id="service_interval" required> <input type="number" name="interval" class="form-control" value="{{$s.Interval}}" id="service_interval" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -147,7 +147,7 @@
<button type="submit" class="btn btn-success btn-block">Update Service</button> <button type="submit" class="btn btn-success btn-block">Update Service</button>
</div> </div>
<div class="col-6"> <div class="col-6">
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block">Delete All Failures</a> <a href="/service/{{ $s.Id }}/delete_failures" class="btn btn-danger btn-block">Delete All Failures</a>
</div> </div>
</div> </div>
</form> </form>
@ -157,12 +157,12 @@
<div class="col-12 mt-4"> <div class="col-12 mt-4">
<h3>Last Response</h3> <h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>{{ .LastResponse }}</textarea> <textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea>
<div class="form-group row mt-2"> <div class="form-group row mt-2">
<label for="last_status_code" class="col-sm-3 col-form-label">Status Code</label> <label for="last_status_code" class="col-sm-3 col-form-label">Status Code</label>
<div class="col-sm-2"> <div class="col-sm-2">
<input type="text" id="last_status_code" class="form-control" value="{{ .LastStatusCode }}" readonly> <input type="text" id="last_status_code" class="form-control" value="{{ $s.LastStatusCode }}" readonly>
</div> </div>
</div> </div>
@ -171,12 +171,12 @@
<div class="col-12 mt-4"> <div class="col-12 mt-4">
<h3>Service Checkins</h3> <h3>Service Checkins</h3>
{{ range .Checkins }} {{ range $s.Checkins }}
<h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5> <h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5>
<input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}"> <input type="text" class="form-control" value="https://domainhere.com/api/checkin/{{.Api}}">
{{ end }} {{ end }}
<form action="/service/{{.Id}}/checkin" method="POST"> <form action="/service/{{$s.Id}}/checkin" method="POST">
<div class="form-group row"> <div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label> <label for="service_name" class="col-sm-4 col-form-label">Check Interval (in seconds)</label>
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">

View File

@ -33,13 +33,14 @@
</thead> </thead>
<tbody> <tbody>
{{range .}} {{range .}}
{{ $s := .ToService }}
<tr> <tr>
<td>{{.Name}}</td> <td>{{$s.Name}}</td>
<td>{{if .Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}} </td> <td>{{if $s.Online}}<span class="badge badge-success">ONLINE</span>{{else}}<span class="badge badge-danger">OFFLINE</span>{{end}} </td>
<td class="text-right"> <td class="text-right">
<div class="btn-group"> <div class="btn-group">
<a href="/service/{{.Id}}" class="btn btn-primary">View</a> <a href="/service/{{$s.Id}}" class="btn btn-primary">View</a>
<a href="/service/{{.Id}}/delete" class="btn btn-danger">Delete</a> <a href="/service/{{$s.Id}}/delete" class="btn btn-danger">Delete</a>
</div> </div>
</td> </td>
</tr> </tr>
@ -47,7 +48,6 @@
</tbody> </tbody>
</table> </table>
<h3>Create Service</h3> <h3>Create Service</h3>
<form action="/services" method="POST"> <form action="/services" method="POST">

19
types/interfaces.go Normal file
View File

@ -0,0 +1,19 @@
package types
type ServiceInterface interface {
AvgTime() float64
Online24() float32
ToService() *Service
SmallText() string
GraphData() string
AvgUptime() string
LimitedFailures() []*Failure
TotalFailures() (uint64, error)
TotalFailures24Hours() (uint64, error)
}
type FailureInterface interface {
ToFailure() *Failure
Ago() string
ParseError() string
}

View File

@ -4,6 +4,33 @@ import (
"time" "time"
) )
type Service struct {
Id int64 `db:"id,omitempty" json:"id"`
Name string `db:"name" json:"name"`
Domain string `db:"domain" json:"domain"`
Expected string `db:"expected" json:"expected"`
ExpectedStatus int `db:"expected_status" json:"expected_status"`
Interval int `db:"check_interval" json:"check_interval"`
Type string `db:"check_type" json:"type"`
Method string `db:"method" json:"method"`
PostData string `db:"post_data" json:"post_data"`
Port int `db:"port" json:"port"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Online bool `json:"online"`
Latency float64 `json:"latency"`
Online24Hours float32 `json:"24_hours_online"`
AvgResponse string `json:"avg_response"`
TotalUptime string `json:"uptime"`
OrderId int64 `json:"order_id"`
Failures []*Failure `json:"failures"`
Checkins []*Checkin `json:"checkins"`
StopRoutine chan struct{}
LastResponse string
LastStatusCode int
LastOnline time.Time
DnsLookup float64 `json:"dns_lookup_time"`
}
type User struct { type User struct {
Id int64 `db:"id,omitempty" json:"id"` Id int64 `db:"id,omitempty" json:"id"`
Username string `db:"username" json:"username"` Username string `db:"username" json:"username"`