mirror of https://github.com/statping/statping
chart updates - ping time - core updates
parent
6b5660b8d8
commit
baf6ca3d34
|
@ -74,20 +74,34 @@ func (s *Service) duration() time.Duration {
|
||||||
return amount
|
return amount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) parseHost() string {
|
||||||
|
if s.Type == "tcp" {
|
||||||
|
return s.Domain
|
||||||
|
} else {
|
||||||
|
domain := s.Domain
|
||||||
|
hasPort, _ := regexp.MatchString(`\:([0-9]+)`, domain)
|
||||||
|
if hasPort {
|
||||||
|
splitDomain := strings.Split(s.Domain, ":")
|
||||||
|
domain = splitDomain[len(splitDomain)-2]
|
||||||
|
}
|
||||||
|
host, err := url.Parse(domain)
|
||||||
|
if err != nil {
|
||||||
|
return s.Domain
|
||||||
|
}
|
||||||
|
return host.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 (s *Service) dnsCheck() (float64, error) {
|
||||||
|
var err error
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
domain := s.Domain
|
host := s.parseHost()
|
||||||
hasPort, _ := regexp.MatchString(`\:([0-9]+)`, domain)
|
if s.Type == "tcp" {
|
||||||
if hasPort {
|
_, err = net.LookupHost(host)
|
||||||
splitDomain := strings.Split(s.Domain, ":")
|
} else {
|
||||||
domain = splitDomain[len(splitDomain)-2]
|
_, err = net.LookupIP(host)
|
||||||
}
|
}
|
||||||
url, err := url.Parse(domain)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
_, err = net.LookupIP(url.Host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -98,6 +112,14 @@ func (s *Service) dnsCheck() (float64, error) {
|
||||||
|
|
||||||
// checkTcp will check a TCP service
|
// checkTcp will check a TCP service
|
||||||
func (s *Service) checkTcp(record bool) *Service {
|
func (s *Service) checkTcp(record bool) *Service {
|
||||||
|
dnsLookup, err := s.dnsCheck()
|
||||||
|
if err != nil {
|
||||||
|
if record {
|
||||||
|
recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
s.PingTime = dnsLookup
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
domain := fmt.Sprintf("%v", s.Domain)
|
domain := fmt.Sprintf("%v", s.Domain)
|
||||||
if s.Port != 0 {
|
if s.Port != 0 {
|
||||||
|
@ -134,7 +156,7 @@ func (s *Service) checkHttp(record bool) *Service {
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
s.DnsLookup = dnsLookup
|
s.PingTime = dnsLookup
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
timeout := time.Duration(s.Timeout)
|
timeout := time.Duration(s.Timeout)
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
|
@ -213,9 +235,10 @@ func recordSuccess(s *Service) {
|
||||||
hit := &types.Hit{
|
hit := &types.Hit{
|
||||||
Service: s.Id,
|
Service: s.Id,
|
||||||
Latency: s.Latency,
|
Latency: s.Latency,
|
||||||
|
PingTime: s.PingTime,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
utils.Log(1, fmt.Sprintf("Service %v Successful: %0.2f ms", s.Name, hit.Latency*1000))
|
utils.Log(1, fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
|
||||||
s.CreateHit(hit)
|
s.CreateHit(hit)
|
||||||
notifier.OnSuccess(s.Service)
|
notifier.OnSuccess(s.Service)
|
||||||
}
|
}
|
||||||
|
@ -226,9 +249,10 @@ func recordFailure(s *Service, issue string) {
|
||||||
fail := &types.Failure{
|
fail := &types.Failure{
|
||||||
Service: s.Id,
|
Service: s.Id,
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
|
PingTime: s.PingTime,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue))
|
utils.Log(2, fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
|
||||||
s.CreateFailure(fail)
|
s.CreateFailure(fail)
|
||||||
notifier.OnFailure(s.Service, fail)
|
notifier.OnFailure(s.Service, fail)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,8 @@ func checkinDB() *gorm.DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HitsBetween returns the gorm database query for a collection of service hits between a time range
|
// 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) *gorm.DB {
|
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
|
||||||
selector := Dbtimestamp(group)
|
selector := Dbtimestamp(group, column)
|
||||||
return DbSession.Model(&types.Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.Format(types.TIME_DAY), t2.Format(types.TIME_DAY)).Order("timeframe asc", false).Group("timeframe")
|
return DbSession.Model(&types.Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.Format(types.TIME_DAY), t2.Format(types.TIME_DAY)).Order("timeframe asc", false).Group("timeframe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,7 +291,7 @@ func (db *DbConfig) DropDatabase() error {
|
||||||
func (db *DbConfig) CreateDatabase() error {
|
func (db *DbConfig) CreateDatabase() error {
|
||||||
utils.Log(1, "Creating Database Tables...")
|
utils.Log(1, "Creating Database Tables...")
|
||||||
err := DbSession.CreateTable(&types.Checkin{})
|
err := DbSession.CreateTable(&types.Checkin{})
|
||||||
err = DbSession.CreateTable(&types.CheckinHit{})
|
//err = DbSession.CreateTable(&types.CheckinHit{})
|
||||||
err = DbSession.CreateTable(¬ifier.Notification{})
|
err = DbSession.CreateTable(¬ifier.Notification{})
|
||||||
err = DbSession.Table("core").CreateTable(&types.Core{})
|
err = DbSession.Table("core").CreateTable(&types.Core{})
|
||||||
err = DbSession.CreateTable(&types.Failure{})
|
err = DbSession.CreateTable(&types.Failure{})
|
||||||
|
@ -317,7 +317,7 @@ func (db *DbConfig) MigrateDatabase() error {
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
return tx.Error
|
return tx.Error
|
||||||
}
|
}
|
||||||
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &types.CheckinHit{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
|
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, ¬ifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
|
||||||
if tx.Error != nil {
|
if tx.Error != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error))
|
utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error))
|
||||||
|
|
|
@ -172,7 +172,7 @@ func (s *Service) DowntimeText() string {
|
||||||
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
|
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Dbtimestamp(group string) string {
|
func Dbtimestamp(group string, column string) string {
|
||||||
seconds := 60
|
seconds := 60
|
||||||
if group == "second" {
|
if group == "second" {
|
||||||
seconds = 60
|
seconds = 60
|
||||||
|
@ -183,11 +183,11 @@ func Dbtimestamp(group string) string {
|
||||||
}
|
}
|
||||||
switch CoreApp.DbConnection {
|
switch CoreApp.DbConnection {
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(latency) AS value")
|
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(%v) AS value", column)
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(latency) as value", seconds, seconds)
|
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(%v) as value", seconds, seconds, column)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(latency) AS value", group)
|
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(%v) AS value", group, column)
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -207,9 +207,9 @@ func (s *Service) Downtime() time.Duration {
|
||||||
return since
|
return since
|
||||||
}
|
}
|
||||||
|
|
||||||
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string) *DateScanObj {
|
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
|
||||||
var d []DateScan
|
var d []DateScan
|
||||||
model := service.(*Service).HitsBetween(start, end, group)
|
model := service.(*Service).HitsBetween(start, end, group, column)
|
||||||
rows, _ := model.Rows()
|
rows, _ := model.Rows()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var gd DateScan
|
var gd DateScan
|
||||||
|
@ -241,7 +241,7 @@ func (d *DateScanObj) ToString() string {
|
||||||
func (s *Service) GraphData() string {
|
func (s *Service) GraphData() string {
|
||||||
start := time.Now().Add((-24 * 7) * time.Hour)
|
start := time.Now().Add((-24 * 7) * time.Hour)
|
||||||
end := time.Now()
|
end := time.Now()
|
||||||
obj := GraphDataRaw(s, start, end, "hour")
|
obj := GraphDataRaw(s, start, end, "hour", "latency")
|
||||||
data, err := json.Marshal(obj)
|
data, err := json.Marshal(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Log(2, err)
|
utils.Log(2, err)
|
||||||
|
|
|
@ -100,6 +100,7 @@ func TestCheckHTTPService(t *testing.T) {
|
||||||
assert.True(t, service.Online)
|
assert.True(t, service.Online)
|
||||||
assert.Equal(t, 200, service.LastStatusCode)
|
assert.Equal(t, 200, service.LastStatusCode)
|
||||||
assert.NotZero(t, service.Latency)
|
assert.NotZero(t, service.Latency)
|
||||||
|
assert.NotZero(t, service.PingTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceTCPCheck(t *testing.T) {
|
func TestServiceTCPCheck(t *testing.T) {
|
||||||
|
@ -114,6 +115,7 @@ func TestCheckTCPService(t *testing.T) {
|
||||||
assert.Equal(t, "Changed Google DNS", service.Name)
|
assert.Equal(t, "Changed Google DNS", service.Name)
|
||||||
assert.True(t, service.Online)
|
assert.True(t, service.Online)
|
||||||
assert.NotZero(t, service.Latency)
|
assert.NotZero(t, service.Latency)
|
||||||
|
assert.NotZero(t, service.PingTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceOnline24Hours(t *testing.T) {
|
func TestServiceOnline24Hours(t *testing.T) {
|
||||||
|
|
|
@ -89,7 +89,24 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
grouping := fields.Get("group")
|
grouping := fields.Get("group")
|
||||||
startField := utils.StringInt(fields.Get("start"))
|
startField := utils.StringInt(fields.Get("start"))
|
||||||
endField := utils.StringInt(fields.Get("end"))
|
endField := utils.StringInt(fields.Get("end"))
|
||||||
obj := core.GraphDataRaw(service, time.Unix(startField, 0), time.Unix(endField, 0), grouping)
|
obj := core.GraphDataRaw(service, time.Unix(startField, 0), time.Unix(endField, 0), grouping, "latency")
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
service := core.SelectService(utils.StringInt(vars["id"]))
|
||||||
|
if service == nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fields := parseGet(r)
|
||||||
|
grouping := fields.Get("group")
|
||||||
|
startField := utils.StringInt(fields.Get("start"))
|
||||||
|
endField := utils.StringInt(fields.Get("end"))
|
||||||
|
obj := core.GraphDataRaw(service, time.Unix(startField, 0), time.Unix(endField, 0), grouping, "ping_time")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(obj)
|
json.NewEncoder(w).Encode(obj)
|
||||||
|
|
|
@ -164,7 +164,7 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
|
||||||
|
|
||||||
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html"}
|
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html"}
|
||||||
|
|
||||||
javascripts := []string{"chart_index.js"}
|
javascripts := []string{"charts.js", "chart_index.js"}
|
||||||
|
|
||||||
render, err := source.TmplBox.String(file)
|
render, err := source.TmplBox.String(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -221,9 +221,19 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data
|
||||||
"safe": func(html string) template.HTML {
|
"safe": func(html string) template.HTML {
|
||||||
return template.HTML(html)
|
return template.HTML(html)
|
||||||
},
|
},
|
||||||
|
"Services": func() []types.ServiceInterface {
|
||||||
|
return core.CoreApp.Services
|
||||||
|
},
|
||||||
})
|
})
|
||||||
t.Parse(render)
|
_, err = t.Parse(render)
|
||||||
t.Execute(w, data)
|
if err != nil {
|
||||||
|
utils.Log(4, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.Execute(w, data)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log(4, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// error404Handler is a HTTP handler for 404 error pages
|
// error404Handler is a HTTP handler for 404 error pages
|
||||||
|
|
|
@ -47,7 +47,7 @@ func Router() *mux.Router {
|
||||||
r.PathPrefix("/statup.png").Handler(http.FileServer(source.TmplBox.HTTPBox()))
|
r.PathPrefix("/statup.png").Handler(http.FileServer(source.TmplBox.HTTPBox()))
|
||||||
}
|
}
|
||||||
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(source.JsBox.HTTPBox())))
|
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(source.JsBox.HTTPBox())))
|
||||||
r.Handle("/charts/{id}.js", http.HandlerFunc(renderServiceChartHandler))
|
//r.Handle("/charts/{id}.js", http.HandlerFunc(renderServiceChartHandler))
|
||||||
r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler))
|
r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler))
|
||||||
r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET")
|
r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET")
|
||||||
r.Handle("/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
|
r.Handle("/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
|
||||||
|
@ -87,6 +87,7 @@ func Router() *mux.Router {
|
||||||
r.Handle("/api/services", http.HandlerFunc(apiCreateServiceHandler)).Methods("POST")
|
r.Handle("/api/services", http.HandlerFunc(apiCreateServiceHandler)).Methods("POST")
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
|
||||||
r.Handle("/api/services/{id}/data", http.HandlerFunc(apiServiceDataHandler)).Methods("GET")
|
r.Handle("/api/services/{id}/data", http.HandlerFunc(apiServiceDataHandler)).Methods("GET")
|
||||||
|
r.Handle("/api/services/{id}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET")
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
|
||||||
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")
|
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")
|
||||||
|
|
||||||
|
|
|
@ -54,14 +54,14 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
service := core.SelectService(utils.StringInt(vars["id"]))
|
service := core.SelectService(utils.StringInt(vars["id"]))
|
||||||
data := core.GraphDataRaw(service, start, end, "hour").ToString()
|
data := core.GraphDataRaw(service, start, end, "hour", "latency").ToString()
|
||||||
|
|
||||||
out := struct {
|
out := struct {
|
||||||
Services []*core.Service
|
Services []*core.Service
|
||||||
Data []string
|
Data []string
|
||||||
}{[]*core.Service{service}, []string{data}}
|
}{[]*core.Service{service}, []string{data}}
|
||||||
|
|
||||||
executeJSResponse(w, r, "charts.js", out)
|
executeResponse(w, r, "charts.js", out, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
|
func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -69,19 +69,16 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/javascript")
|
w.Header().Set("Content-Type", "text/javascript")
|
||||||
w.Header().Set("Cache-Control", "max-age=60")
|
w.Header().Set("Cache-Control", "max-age=60")
|
||||||
|
|
||||||
var data []string
|
//var data []string
|
||||||
end := now.EndOfDay().UTC()
|
//end := now.EndOfDay().UTC()
|
||||||
start := now.BeginningOfDay().UTC()
|
//start := now.BeginningOfDay().UTC()
|
||||||
|
var srvs []*core.Service
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
d := core.GraphDataRaw(s, start, end, "hour").ToString()
|
srvs = append(srvs, s.(*core.Service))
|
||||||
data = append(data, d)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out := struct {
|
out := struct {
|
||||||
Services []types.ServiceInterface
|
Services []*core.Service
|
||||||
Data []string
|
}{srvs}
|
||||||
}{services, data}
|
|
||||||
|
|
||||||
executeJSResponse(w, r, "charts.js", out)
|
executeJSResponse(w, r, "charts.js", out)
|
||||||
}
|
}
|
||||||
|
@ -194,7 +191,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
end = time.Unix(endField, 0)
|
end = time.Unix(endField, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := core.GraphDataRaw(serv, start, end, "hour")
|
data := core.GraphDataRaw(serv, start, end, "hour", "latency")
|
||||||
|
|
||||||
out := struct {
|
out := struct {
|
||||||
Service *core.Service
|
Service *core.Service
|
||||||
|
|
|
@ -6,8 +6,8 @@ var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Response Time (Milliseconds)',
|
label: 'Response Time (Milliseconds)',
|
||||||
data: [],
|
data: [],
|
||||||
backgroundColor: ['rgba(47, 206, 30, 0.92)'],
|
backgroundColor: ['{{if .Online}}rgba(47, 206, 30, 0.92){{else}}rgb(221, 53, 69){{end}}'],
|
||||||
borderColor: ['rgb(47, 171, 34)'],
|
borderColor: ['{{if .Online}}rgb(47, 171, 34){{else}}rgb(183, 32, 47){{end}}'],
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
@ -88,7 +88,7 @@ var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
|
||||||
display: !1
|
display: !1
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
"enabled": !1
|
enabled: !1
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
yAxes: [{
|
yAxes: [{
|
||||||
|
|
|
@ -1,23 +1,137 @@
|
||||||
/*
|
{{define "charts"}}{{ range Services }}
|
||||||
* Statup
|
var ctx_{{js .Id}} = document.getElementById("service_{{js .Id}}").getContext('2d');
|
||||||
* Copyright (C) 2018. Hunter Long and the project contributors
|
var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
|
||||||
* Written by Hunter Long <info@socialeck.com> and the project contributors
|
type: 'line',
|
||||||
*
|
data: {
|
||||||
* https://github.com/hunterlong/statup
|
datasets: [{
|
||||||
*
|
label: 'Response Time (Milliseconds)',
|
||||||
* The licenses for most software and other practical works are designed
|
data: [],
|
||||||
* to take away your freedom to share and change the works. By contrast,
|
backgroundColor: ['{{if .Online}}rgba(47, 206, 30, 0.92){{else}}rgb(221, 53, 69){{end}}'],
|
||||||
* the GNU General Public License is intended to guarantee your freedom to
|
borderColor: ['{{if .Online}}rgb(47, 171, 34){{else}}rgb(183, 32, 47){{end}}'],
|
||||||
* share and change all versions of a program--to make sure it remains free
|
borderWidth: 1
|
||||||
* software for all its users.
|
}]
|
||||||
*
|
},
|
||||||
* You should have received a copy of the GNU General Public License
|
options: {
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
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 (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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
time: {
|
||||||
|
displayFormats: {
|
||||||
|
'hour': 'MMM DD hA'
|
||||||
|
},
|
||||||
|
source: 'auto'
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
display: !1
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
source: 'auto',
|
||||||
|
stepSize: 1,
|
||||||
|
min: 0,
|
||||||
|
fontColor: "white",
|
||||||
|
fontSize: 20,
|
||||||
|
display: !1
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
{{$d := .Data}}{{ range $i, $s := .Services }}{{ if $s.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 (index $d $i)}},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}
|
AjaxChart(chartdata_{{js .Id}},{{js .Id}},0,99999999999,"hour");
|
||||||
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}
|
{{end}}{{end}}
|
||||||
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,time:{displayFormats:{'hour': 'MMM DD hA'},source: 'auto'},gridLines:{display:!1},ticks:{source:'auto',stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1}}]},elements:{point:{radius:0}}}})
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
|
@ -95,6 +95,25 @@ function AjaxChart(chart, service, start=0, end=9999999999, group="hour") {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PingAjaxChart(chart, service, start=0, end=9999999999, group="hour") {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/services/"+service+"/ping?start="+start+"&end="+end+"&group="+group,
|
||||||
|
type: 'GET',
|
||||||
|
success: function(data) {
|
||||||
|
chart.data.labels.pop();
|
||||||
|
chart.data.datasets.push({
|
||||||
|
label: "Ping Time",
|
||||||
|
backgroundColor: "#bababa"
|
||||||
|
});
|
||||||
|
chart.update();
|
||||||
|
data.data.forEach(function(d) {
|
||||||
|
chart.data.datasets[1].data.push(d);
|
||||||
|
});
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$('select#service_check_type').on('change', function() {
|
$('select#service_check_type').on('change', function() {
|
||||||
var selected = $('#service_check_type option:selected').val();
|
var selected = $('#service_check_type option:selected').val();
|
||||||
if (selected === 'POST') {
|
if (selected === 'POST') {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{define "form_service"}}
|
{{define "form_service"}}
|
||||||
<form action="{{if .}}/service/{{.Id}}{{else}}/service{{end}}" method="POST">
|
<form action="{{if ne .Id 0}}/service/{{.Id}}{{else}}/service{{end}}" 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">
|
||||||
|
@ -49,26 +49,26 @@
|
||||||
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
|
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
|
||||||
<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="{{if ne .ExpectedStatus 0}}{{.ExpectedStatus}}{{end}}" placeholder="200" id="service_response_code">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row{{if eq .Type "http"}} d-none{{end}}">
|
<div class="form-group row{{if eq .Type "http"}} d-none{{end}}">
|
||||||
<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="{{if ne .Port 0}}{{.Port}}{{end}}" 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}}" min="1" id="service_interval" required>
|
<input type="number" name="interval" class="form-control" value="{{if ne .Interval 0}}{{.Interval}}{{end}}" min="1" id="service_interval" required>
|
||||||
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
|
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
|
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<input type="number" name="timeout" class="form-control" value="{{.Timeout}}" id="service_timeout" min="1">
|
<input type="number" name="timeout" class="form-control" value="{{if ne .Timeout 0}}{{.Timeout}}{{end}}" placeholder="15" id="service_timeout" min="1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
|
@ -78,12 +78,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-6">
|
<div class="{{if ne .Id 0}}col-6{{else}}col-12{{end}}">
|
||||||
<button type="submit" class="btn btn-success btn-block">Update Service</button>
|
<button type="submit" class="btn btn-success btn-block">{{if ne .Id 0}}Update Service{{else}}Create Service{{end}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
{{if ne .Id 0}}
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
|
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{define "form_user"}}
|
{{define "form_user"}}
|
||||||
<form action="{{if .}}/user/{{.Id}}{{else}}/user{{end}}" method="POST">
|
<form action="{{if ne .Id 0}}/user/{{.Id}}{{else}}/user{{end}}" method="POST">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="username" class="col-sm-4 col-form-label">Username</label>
|
<label for="username" class="col-sm-4 col-form-label">Username</label>
|
||||||
<div class="col-6 col-md-4">
|
<div class="col-6 col-md-4">
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="submit" class="btn btn-primary btn-block">Update User</button>
|
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update User{{else}}Create User{{end}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -80,9 +80,5 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "extra_scripts"}}
|
{{define "extra_scripts"}}
|
||||||
<script>
|
<script src="/charts.js"></script>
|
||||||
{{ range Services }}
|
|
||||||
{{template "chartIndex" .}}
|
|
||||||
{{end}}
|
|
||||||
</script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -26,6 +26,7 @@ type Failure struct {
|
||||||
Issue string `gorm:"column:issue" json:"issue"`
|
Issue string `gorm:"column:issue" json:"issue"`
|
||||||
Method string `gorm:"column:method" json:"method,omitempty"`
|
Method string `gorm:"column:method" json:"method,omitempty"`
|
||||||
Service int64 `gorm:"index;column:service" json:"-"`
|
Service int64 `gorm:"index;column:service" json:"-"`
|
||||||
|
PingTime float64 `gorm:"column:ping_time"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||||
FailureInterface `gorm:"-" json:"-"`
|
FailureInterface `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ type Service struct {
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||||
Online bool `gorm:"-" json:"online"`
|
Online bool `gorm:"-" json:"online"`
|
||||||
Latency float64 `gorm:"-" json:"latency"`
|
Latency float64 `gorm:"-" json:"latency"`
|
||||||
|
PingTime float64 `gorm:"-" json:"ping_time"`
|
||||||
Online24Hours float32 `gorm:"-" json:"24_hours_online"`
|
Online24Hours float32 `gorm:"-" json:"24_hours_online"`
|
||||||
AvgResponse string `gorm:"-" json:"avg_response"`
|
AvgResponse string `gorm:"-" json:"avg_response"`
|
||||||
Running chan bool `gorm:"-" json:"-"`
|
Running chan bool `gorm:"-" json:"-"`
|
||||||
|
@ -44,7 +45,6 @@ type Service struct {
|
||||||
LastResponse string `gorm:"-" json:"-"`
|
LastResponse string `gorm:"-" json:"-"`
|
||||||
LastStatusCode int `gorm:"-" json:"status_code"`
|
LastStatusCode int `gorm:"-" json:"status_code"`
|
||||||
LastOnline time.Time `gorm:"-" json:"last_online"`
|
LastOnline time.Time `gorm:"-" json:"last_online"`
|
||||||
DnsLookup float64 `gorm:"-" json:"dns_lookup_time"`
|
|
||||||
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
|
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
|
||||||
Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"`
|
Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue