response time fixes - cache fixes - apexcharts

pull/135/head v0.80.41
hunterlong 2019-01-31 20:48:18 -08:00
parent b80c61cd04
commit fd7925aa7e
27 changed files with 472 additions and 447 deletions

View File

@ -161,66 +161,6 @@ func (u *Message) AfterFind() (err error) {
return return
} }
// BeforeCreate for Hit will set CreatedAt to UTC
func (h *Hit) BeforeCreate() (err error) {
if h.CreatedAt.IsZero() {
h.CreatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for Failure will set CreatedAt to UTC
func (f *Failure) BeforeCreate() (err error) {
if f.CreatedAt.IsZero() {
f.CreatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for User will set CreatedAt to UTC
func (u *User) BeforeCreate() (err error) {
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now().UTC()
u.UpdatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for Message will set CreatedAt to UTC
func (u *Message) BeforeCreate() (err error) {
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now().UTC()
u.UpdatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for Service will set CreatedAt to UTC
func (s *Service) BeforeCreate() (err error) {
if s.CreatedAt.IsZero() {
s.CreatedAt = time.Now().UTC()
s.UpdatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for Checkin will set CreatedAt to UTC
func (c *Checkin) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
c.UpdatedAt = time.Now().UTC()
}
return
}
// BeforeCreate for checkinHit will set CreatedAt to UTC
func (c *CheckinHit) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
}
return
}
// InsertCore create the single row for the Core settings in Statping // InsertCore create the single row for the Core settings in Statping
func (db *DbConfig) InsertCore() (*Core, error) { func (db *DbConfig) InsertCore() (*Core, error) {
CoreApp = &Core{Core: &types.Core{ CoreApp = &Core{Core: &types.Core{

View File

@ -31,6 +31,7 @@ type Failure struct {
const ( const (
limitedFailures = 32 limitedFailures = 32
limitedHits = 32
) )
// CreateFailure will create a new Failure record for a service // CreateFailure will create a new Failure record for a service

View File

@ -52,9 +52,9 @@ func (s *Service) Hits() ([]*types.Hit, error) {
} }
// LimitedHits returns the last 1024 successful/online 'hit' records for a service // LimitedHits returns the last 1024 successful/online 'hit' records for a service
func (s *Service) LimitedHits() ([]*types.Hit, error) { func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
var hits []*types.Hit var hits []*types.Hit
col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(1024) col := hitsDB().Where("service = ?", s.Id).Order("id desc").Limit(amount)
err := col.Find(&hits) err := col.Find(&hits)
return reverseHits(hits), err.Error return reverseHits(hits), err.Error
} }
@ -70,17 +70,15 @@ func reverseHits(input []*types.Hit) []*types.Hit {
// TotalHits returns the total amount of successful hits a service has // TotalHits returns the total amount of successful hits a service has
func (s *Service) TotalHits() (uint64, error) { func (s *Service) TotalHits() (uint64, error) {
var count uint64 var count uint64
col := hitsDB().Where("service = ?", s.Id) col := hitsDB().Where("service = ?", s.Id).Count(&count)
err := col.Count(&count) return count, col.Error
return count, err.Error
} }
// TotalHitsSince returns the total amount of hits based on a specific time/date // TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) { func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64 var count uint64
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")) rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
err := rows.Count(&count) return count, rows.Error
return count, err.Error
} }
// Sum returns the added value Latency for all of the services successful hits. // Sum returns the added value Latency for all of the services successful hits.

View File

@ -28,7 +28,7 @@ func InsertSampleData() error {
utils.Log(1, "Inserting Sample Data...") utils.Log(1, "Inserting Sample Data...")
insertSampleGroups() insertSampleGroups()
createdOn := time.Now().Add((-24 * 90) * time.Hour).UTC() createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
s1 := ReturnService(&types.Service{ s1 := ReturnService(&types.Service{
Name: "Google", Name: "Google",
Domain: "https://google.com", Domain: "https://google.com",

View File

@ -92,7 +92,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
checkins := service.AllCheckins() checkins := service.AllCheckins()
for _, c := range checkins { for _, c := range checkins {
c.Failures = c.LimitedFailures(limitedFailures) c.Failures = c.LimitedFailures(limitedFailures)
c.Hits = c.LimitedHits(limitedFailures) c.Hits = c.LimitedHits(limitedHits)
service.Checkins = append(service.Checkins, c) service.Checkins = append(service.Checkins, c)
} }
CoreApp.Services = append(CoreApp.Services, service) CoreApp.Services = append(CoreApp.Services, service)
@ -113,16 +113,20 @@ func (s *Service) ToJSON() string {
} }
// AvgTime will return the average amount of time for a service to response back successfully // AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() float64 { func (s *Service) AvgTime() string {
total, _ := s.TotalHits() total, _ := s.TotalHits()
if total == 0 { if total == 0 {
return float64(0) return "0"
} }
sum := s.Sum() sum := s.Sum()
avg := sum / float64(total) * 100 avg := sum / float64(total) * 100
amount := fmt.Sprintf("%0.0f", avg*10) return fmt.Sprintf("%0.0f", avg*10)
val, _ := strconv.ParseFloat(amount, 10) }
return val
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *Service) OnlineDaysPercent(days int) float32 {
ago := time.Now().Add((-24 * time.Duration(days)) * time.Hour)
return s.OnlineSince(ago)
} }
// Online24 returns the service's uptime percent within last 24 hours // Online24 returns the service's uptime percent within last 24 hours
@ -185,7 +189,7 @@ func (s *Service) lastFailure() *Failure {
// // Online since Monday 3:04:05PM, Jan _2 2006 // // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string { func (s *Service) SmallText() string {
last := s.LimitedFailures(1) last := s.LimitedFailures(1)
hits, _ := s.LimitedHits() hits, _ := s.LimitedHits(1)
zone := CoreApp.Timezone zone := CoreApp.Timezone
if s.Online { if s.Online {
if len(last) == 0 { if len(last) == 0 {
@ -255,19 +259,14 @@ func (s *Service) Downtime() time.Duration {
// GraphDataRaw will return all the hits between 2 times for a Service // GraphDataRaw will return all the hits between 2 times for a Service
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj { func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var d []DateScan var data []DateScan
var amount int64 outgoing := new(DateScanObj)
model := service.(*Service).HitsBetween(start, end, group, column) model := service.(*Service).HitsBetween(start, end, group, column)
model.Count(&amount)
if amount == 0 {
return &DateScanObj{[]DateScan{}}
}
model = model.Order("timeframe asc", false).Group("timeframe") model = model.Order("timeframe asc", false).Group("timeframe")
rows, err := model.Rows() rows, err := model.Rows()
if err != nil { if err != nil {
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err)) utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
} }
for rows.Next() { for rows.Next() {
var gd DateScan var gd DateScan
var createdAt string var createdAt string
@ -285,9 +284,10 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
} }
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME) gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.CHART_TIME)
gd.Value = int64(value * 1000) gd.Value = int64(value * 1000)
d = append(d, gd) data = append(data, gd)
} }
return &DateScanObj{d} outgoing.Array = data
return outgoing
} }
// ToString will convert the DateScanObj into a JSON string for the charts to render // ToString will convert the DateScanObj into a JSON string for the charts to render

View File

@ -156,7 +156,7 @@ func TestServiceHits(t *testing.T) {
func TestServiceLimitedHits(t *testing.T) { func TestServiceLimitedHits(t *testing.T) {
service := SelectService(5) service := SelectService(5)
hits, err := service.LimitedHits() hits, err := service.LimitedHits(1024)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int(1024), len(hits)) assert.Equal(t, int(1024), len(hits))
} }

View File

@ -62,6 +62,15 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/settings", http.StatusSeeOther) http.Redirect(w, r, "/settings", http.StatusSeeOther)
} }
func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
if !IsFullAuthenticated(r) {
sendUnauthorizedJson(w, r)
return
}
CacheStorage = NewStorage()
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) { func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error())) utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
output := apiResponse{ output := apiResponse{

View File

@ -46,9 +46,6 @@ func NewStorage() *Storage {
//Get a cached content by key //Get a cached content by key
func (s Storage) Get(key string) []byte { func (s Storage) Get(key string) []byte {
s.mu.RLock()
defer s.mu.RUnlock()
item := s.items[key] item := s.items[key]
if item.Expired() { if item.Expired() {
CacheStorage.Delete(key) CacheStorage.Delete(key)
@ -93,10 +90,10 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
w.Write(content) w.Write(content)
return return
} }
if d, err := time.ParseDuration(duration); err == nil {
CacheStorage.Set(r.RequestURI, content, d)
}
w.Write(content) w.Write(content)
if d, err := time.ParseDuration(duration); err == nil {
go CacheStorage.Set(r.RequestURI, content, d)
}
} }
}) })
} }

117
handlers/functions.go Normal file
View File

@ -0,0 +1,117 @@
package handlers
import (
"encoding/json"
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"html/template"
"net/http"
"reflect"
"time"
)
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
return template.FuncMap{
"js": func(html interface{}) template.JS {
return template.JS(utils.ToString(html))
},
"safe": func(html string) template.HTML {
return template.HTML(html)
},
"safeURL": func(u string) template.URL {
return template.URL(u)
},
"Auth": func() bool {
return IsFullAuthenticated(r)
},
"IsUser": func() bool {
return IsUser(r)
},
"VERSION": func() string {
return core.VERSION
},
"CoreApp": func() *core.Core {
return core.CoreApp
},
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
"Groups": func(includeAll bool) []*core.Group {
auth := IsUser(r)
return core.SelectGroups(includeAll, auth)
},
"len": func(g interface{}) int {
val := reflect.ValueOf(g)
return val.Len()
},
"IsNil": func(g interface{}) bool {
return g == nil
},
"USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool
},
"QrAuth": func() string {
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
},
"Type": func(g interface{}) []string {
fooType := reflect.TypeOf(g)
var methods []string
methods = append(methods, fooType.String())
for i := 0; i < fooType.NumMethod(); i++ {
method := fooType.Method(i)
fmt.Println(method.Name)
methods = append(methods, method.Name)
}
return methods
},
"ToJSON": func(g interface{}) template.HTML {
data, _ := json.Marshal(g)
return template.HTML(string(data))
},
"underscore": func(html string) string {
return utils.UnderScoreString(html)
},
"URL": func() string {
return r.URL.String()
},
"CHART_DATA": func() string {
return ""
},
"Error": func() string {
return ""
},
"ToString": func(v interface{}) string {
return utils.ToString(v)
},
"Ago": func(t time.Time) string {
return utils.Timestamp(t).Ago()
},
"Duration": func(t time.Duration) string {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", t.Seconds()))
return utils.FormatDuration(duration)
},
"ToUnix": func(t time.Time) int64 {
return t.UTC().Unix()
},
"FromUnix": func(t int64) string {
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
},
"NewService": func() *types.Service {
return new(types.Service)
},
"NewUser": func() *types.User {
return new(types.User)
},
"NewCheckin": func() *types.Checkin {
return new(types.Checkin)
},
"NewMessage": func() *types.Message {
return new(types.Message)
},
"NewGroup": func() *types.Group {
return new(types.Group)
},
}
}

View File

@ -17,7 +17,6 @@ package handlers
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/hunterlong/statping/core" "github.com/hunterlong/statping/core"
@ -27,20 +26,23 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"os" "os"
"reflect"
"strings" "strings"
"time" "time"
) )
const ( const (
cookieKey = "statping_auth" cookieKey = "statping_auth"
timeout = time.Second * 60 timeout = time.Second * 30
) )
var ( var (
sessionStore *sessions.CookieStore sessionStore *sessions.CookieStore
httpServer *http.Server httpServer *http.Server
usingSSL bool usingSSL bool
mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
templates = []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
javascripts = []string{"charts.js", "chart_index.js"}
mainTemplate *template.Template
) )
// RunHTTPServer will start a HTTP server on a specific IP and port // RunHTTPServer will start a HTTP server on a specific IP and port
@ -91,6 +93,7 @@ func RunHTTPServer(ip string, port int) error {
IdleTimeout: timeout, IdleTimeout: timeout,
Handler: router, Handler: router,
} }
httpServer.SetKeepAlivesEnabled(false)
return httpServer.ListenAndServe() return httpServer.ListenAndServe()
} }
return nil return nil
@ -166,112 +169,37 @@ func IsUser(r *http.Request) bool {
return session.Values["authenticated"].(bool) return session.Values["authenticated"].(bool)
} }
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap { func loadTemplate(w http.ResponseWriter, r *http.Request) error {
return template.FuncMap{ var err error
"js": func(html interface{}) template.JS { mainTemplate = template.New("main")
return template.JS(utils.ToString(html)) mainTemplate.Funcs(handlerFuncs(w, r))
}, mainTemplate, err = mainTemplate.Parse(mainTmpl)
"safe": func(html string) template.HTML { if err != nil {
return template.HTML(html) utils.Log(4, err)
}, return err
"safeURL": func(u string) template.URL {
return template.URL(u)
},
"Auth": func() bool {
return IsFullAuthenticated(r)
},
"IsUser": func() bool {
return IsUser(r)
},
"VERSION": func() string {
return core.VERSION
},
"CoreApp": func() *core.Core {
return core.CoreApp
},
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
"Groups": func(includeAll bool) []*core.Group {
auth := IsUser(r)
return core.SelectGroups(includeAll, auth)
},
"len": func(g interface{}) int {
val := reflect.ValueOf(g)
return val.Len()
},
"IsNil": func(g interface{}) bool {
return g == nil
},
"USE_CDN": func() bool {
return core.CoreApp.UseCdn.Bool
},
"QrAuth": func() string {
return fmt.Sprintf("statping://setup?domain=%v&api=%v", core.CoreApp.Domain, core.CoreApp.ApiSecret)
},
"Type": func(g interface{}) []string {
fooType := reflect.TypeOf(g)
var methods []string
methods = append(methods, fooType.String())
for i := 0; i < fooType.NumMethod(); i++ {
method := fooType.Method(i)
fmt.Println(method.Name)
methods = append(methods, method.Name)
}
return methods
},
"ToJSON": func(g interface{}) template.HTML {
data, _ := json.Marshal(g)
return template.HTML(string(data))
},
"underscore": func(html string) string {
return utils.UnderScoreString(html)
},
"URL": func() string {
return r.URL.String()
},
"CHART_DATA": func() string {
return ""
},
"Error": func() string {
return ""
},
"ToString": func(v interface{}) string {
return utils.ToString(v)
},
"Ago": func(t time.Time) string {
return utils.Timestamp(t).Ago()
},
"Duration": func(t time.Duration) string {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", t.Seconds()))
return utils.FormatDuration(duration)
},
"ToUnix": func(t time.Time) int64 {
return t.UTC().Unix()
},
"FromUnix": func(t int64) string {
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
},
"NewService": func() *types.Service {
return new(types.Service)
},
"NewUser": func() *types.User {
return new(types.User)
},
"NewCheckin": func() *types.Checkin {
return new(types.Checkin)
},
"NewMessage": func() *types.Message {
return new(types.Message)
},
"NewGroup": func() *types.Group {
return new(types.Group)
},
} }
// render all templates
mainTemplate.Funcs(handlerFuncs(w, r))
for _, temp := range templates {
tmp, _ := source.TmplBox.String(temp)
mainTemplate, err = mainTemplate.Parse(tmp)
if err != nil {
utils.Log(4, err)
return err
}
}
// render all javascript files
for _, temp := range javascripts {
tmp, _ := source.JsBox.String(temp)
mainTemplate, err = mainTemplate.Parse(tmp)
if err != nil {
utils.Log(4, err)
return err
}
}
return err
} }
var mainTmpl = `{{define "main" }} {{ template "base" . }} {{ end }}`
// ExecuteResponse will render a HTTP response for the front end user // ExecuteResponse will render a HTTP response for the front end user
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) { func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
utils.Http(r) utils.Http(r)
@ -279,53 +207,21 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
http.Redirect(w, r, url, http.StatusSeeOther) http.Redirect(w, r, url, http.StatusSeeOther)
return return
} }
if usingSSL { if usingSSL {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
} }
loadTemplate(w, r)
templates := []string{"base.gohtml", "head.gohtml", "nav.gohtml", "footer.gohtml", "scripts.gohtml", "form_service.gohtml", "form_notifier.gohtml", "form_group.gohtml", "form_user.gohtml", "form_checkin.gohtml", "form_message.gohtml"}
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 {
utils.Log(4, err) utils.Log(4, err)
} }
// setup the main template and handler funcs
t := template.New("main")
t.Funcs(handlerFuncs(w, r))
t, err = t.Parse(mainTmpl)
if err != nil {
utils.Log(4, err)
}
// render all templates
for _, temp := range templates {
tmp, _ := source.TmplBox.String(temp)
t, err = t.Parse(tmp)
if err != nil {
utils.Log(4, err)
}
}
// render all javascript files
for _, temp := range javascripts {
tmp, _ := source.JsBox.String(temp)
t, err = t.Parse(tmp)
if err != nil {
utils.Log(4, err)
}
}
// render the page requested // render the page requested
_, err = t.Parse(render) _, err = mainTemplate.Parse(render)
if err != nil { if err != nil {
utils.Log(4, err) utils.Log(4, err)
} }
// execute the template // execute the template
err = t.Execute(w, data) err = mainTemplate.Execute(w, data)
if err != nil { if err != nil {
utils.Log(4, err) utils.Log(4, err)
} }

View File

@ -35,7 +35,7 @@ func Router() *mux.Router {
dir := utils.Directory dir := utils.Directory
CacheStorage = NewStorage() CacheStorage = NewStorage()
r := mux.NewRouter() r := mux.NewRouter()
r.Handle("/", cached("120s", "text/html", http.HandlerFunc(indexHandler))) r.Handle("/", cached("60s", "text/html", http.HandlerFunc(indexHandler)))
if source.UsingAssets(dir) { if source.UsingAssets(dir) {
indexHandler := http.FileServer(http.Dir(dir + "/assets/")) indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css")))) r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
@ -95,6 +95,7 @@ func Router() *mux.Router {
// API Routes // API Routes
r.Handle("/api", http.HandlerFunc(apiIndexHandler)) r.Handle("/api", http.HandlerFunc(apiIndexHandler))
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler)) r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
r.Handle("/api/clear_cache", http.HandlerFunc(apiClearCacheHandler))
// API SERVICE Routes // API SERVICE Routes
r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET") r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET")
@ -102,8 +103,8 @@ func Router() *mux.Router {
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET") r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
r.Handle("/api/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST") r.Handle("/api/reorder", http.HandlerFunc(reorderServiceHandler)).Methods("POST")
r.Handle("/api/services/{id}/data", cached("30s", "application/json", http.HandlerFunc(apiServiceDataHandler))).Methods("GET") r.Handle("/api/services/{id}/data", cached("30s", "application/json", http.HandlerFunc(apiServiceDataHandler))).Methods("GET")
r.Handle("/api/services/{id}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET") r.Handle("/api/services/{id}/ping", cached("30s", "application/json", http.HandlerFunc(apiServicePingDataHandler))).Methods("GET")
r.Handle("/api/services/{id}/heatmap", http.HandlerFunc(apiServiceHeatmapHandler)).Methods("GET") r.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", http.HandlerFunc(apiServiceHeatmapHandler))).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")
r.Handle("/api/services/{id}/failures", http.HandlerFunc(apiServiceFailuresHandler)).Methods("GET") r.Handle("/api/services/{id}/failures", http.HandlerFunc(apiServiceFailuresHandler)).Methods("GET")
@ -146,6 +147,7 @@ func Router() *mux.Router {
r.Handle("/health", http.HandlerFunc(healthCheckHandler)) r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.Handle("/tray", http.HandlerFunc(trayHandler)) r.Handle("/tray", http.HandlerFunc(trayHandler))
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known")))) r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
r.NotFoundHandler = http.HandlerFunc(error404Handler) r.NotFoundHandler = http.HandlerFunc(error404Handler)
return r return r
} }

View File

@ -18,6 +18,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/hunterlong/statping/core" "github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types" "github.com/hunterlong/statping/types"
@ -227,7 +228,7 @@ type dataXy struct {
} }
type dataXyMonth struct { type dataXyMonth struct {
Date time.Time `json:"date"` Date string `json:"date"`
Data []*dataXy `json:"data"` Data []*dataXy `json:"data"`
} }
@ -242,26 +243,37 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
var monthOutput []*dataXyMonth var monthOutput []*dataXyMonth
start := service.CreatedAt start := service.CreatedAt
//now := time.Now()
if start.Year() <= 2 { sY, sM, _ := start.Date()
start = service.CreatedAt.Add(time.Duration((-3 * 24) * time.Hour))
}
for y := start; y.Year() == start.Year(); y = y.AddDate(1, 0, 0) { var date time.Time
for m := y; m.Month() == y.Month(); m = m.AddDate(0, 1, 0) { month := int(sM)
maxMonth := 12
for year := int(sY); year <= time.Now().Year(); year++ {
if year == time.Now().Year() {
maxMonth = int(time.Now().Month())
}
for m := month; m <= maxMonth; m++ {
var output []*dataXy var output []*dataXy
for day := 1; day <= 31; day++ { for day := 1; day <= 31; day++ {
date := time.Date(y.Year(), y.Month(), day, 0, 0, 0, 0, time.UTC) date = time.Date(year, time.Month(m), day, 0, 0, 0, 0, time.UTC)
failures, _ := service.TotalFailuresOnDate(date) failures, _ := service.TotalFailuresOnDate(date)
output = append(output, &dataXy{day, int(failures)}) output = append(output, &dataXy{day, int(failures)})
} }
monthOutput = append(monthOutput, &dataXyMonth{m, output}) thisDate := fmt.Sprintf("%v-%v-01 00:00:00", year, m)
monthOutput = append(monthOutput, &dataXyMonth{thisDate, output})
} }
month = 1
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@ -148,6 +148,12 @@ HTML, BODY {
width: 100%; width: 100%;
} }
.service-chart-heatmap {
position: relative;
height: 300px;
width: 100%;
}
.btn-primary { .btn-primary {
background-color: #3e9bff; background-color: #3e9bff;
border-color: #006fe6; border-color: #006fe6;

View File

@ -148,71 +148,20 @@ let options = {
}, },
}; };
const startOn = Math.floor(Date.now() / 1000) - (86400 * 14);
const endOn = Math.floor(Date.now() / 1000);
async function RenderCharts() {
{{ range .Services }}
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
{{end}}
{{ range .Services }} {{ range .Services }}
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn, endOn);{{end}}
let chart{{.Id}} = new ApexCharts(document.querySelector("#service_{{js .Id}}"), options);
{{end}}
function onChartComplete(chart) {
var chartInstance = chart.chart,
ctx = chartInstance.ctx;
var controller = chart.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);
})
} }
$( document ).ready(function() { $( document ).ready(function() {
{{ range .Services }}AjaxChart(chart{{js .Id}}, {{js .Id}}, 0, 9999999999); RenderCharts()
{{end}} });
});
{{end}} {{end}}

View File

@ -111,27 +111,42 @@ $('select#service_type').on('change', function() {
} }
}); });
function AjaxChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
$.ajax({ async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
url: "/api/services/"+service+"/data?start="+start+"&end="+end+"&group="+group, let chartData = await ChartLatency(service, start, end, group, retry);
type: 'GET', if (chartData.length === 0) {
success: function(data) { chartData = await ChartLatency(service, start, end, "minute", retry);
if (data.data.length < 12) {
if (retry) {
AjaxChart(chart, service, 0, 9999999999, "second", false);
}
return;
} else if (data.data.length === 0) {
return;
}
chart.render();
chart.updateSeries([{
data: data.data
}]);
} }
}); chart.render();
chart.updateSeries([{
data: chartData
}]);
} }
function ChartLatency(service, start=0, end=9999999999, group="hour", retry=true) {
return new Promise(resolve => {
$.ajax({
url: "/api/services/" + service + "/data?start=" + start + "&end=" + end + "&group=" + group,
type: 'GET',
success: function (data) {
resolve(data.data);
}
});
});
}
function ChartHeatmap(service) {
return new Promise(resolve => {
$.ajax({
url: "/api/services/" + service + "/heatmap",
type: 'GET',
success: function (data) {
resolve(data);
}
});
});
}
function FailureAnnotations(chart, service, start=0, end=9999999999, group="hour", retry=true) { function FailureAnnotations(chart, service, start=0, end=9999999999, group="hour", retry=true) {

View File

@ -145,6 +145,12 @@ HTML,BODY {
width: 100%; width: 100%;
} }
.service-chart-heatmap {
position: relative;
height: 300px;
width: 100%;
}
@mixin dynamic-color-hov($color) { @mixin dynamic-color-hov($color) {
&.dyn-dark { &.dyn-dark {
background-color: darken($color, 12%) !important; background-color: darken($color, 12%) !important;

View File

@ -42,7 +42,7 @@
{{end}} {{end}}
<div class="col-12 full-col-12"> <div class="col-12 full-col-12">
{{ if not Services }} {{ if not .Services }}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<h4 class="alert-heading">No Services to Monitor!</h4> <h4 class="alert-heading">No Services to Monitor!</h4>
<p>Your Statping Status Page is working correctly, but you don't have any services to monitor. Go to the <b>Dashboard</b> and add a website to begin really using your status page!</p> <p>Your Statping Status Page is working correctly, but you don't have any services to monitor. Go to the <b>Dashboard</b> and add a website to begin really using your status page!</p>
@ -51,7 +51,8 @@
</div> </div>
{{end}} {{end}}
{{ range Services }} {{ range .Services }}
{{$avgTime := .AvgTime}}
<div class="mb-4" id="service_id_{{.Id}}"> <div class="mb-4" id="service_id_{{.Id}}">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@ -65,22 +66,22 @@
<div class="row stats_area mt-5"> <div class="row stats_area mt-5">
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{.AvgTime}}ms</span> <span class="lg_number">{{$avgTime}}ms</span>
Average Response Average Response
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{.Online24}}%</span> <span class="lg_number">{{.OnlineDaysPercent 1}}%</span>
Uptime last 24 Hours Uptime last 24 Hours
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{.Online7Days}}%</span> <span class="lg_number">{{.OnlineDaysPercent 7}}%</span>
Uptime last 7 Days Uptime last 7 Days
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{ if .AvgUptime24 }} {{ if $avgTime }}
<div class="chart-container"> <div class="chart-container">
<div id="service_{{ .Id }}"></div> <div id="service_{{ .Id }}"></div>
</div> </div>

View File

@ -19,7 +19,7 @@
<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">{{ $s.Name }} <h4 class="mt-2"><a href="/">{{CoreApp.Name}}</a> - {{ $s.Name }}
{{if $s.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 }}
@ -28,15 +28,13 @@
<div class="row stats_area mt-5 mb-5"> <div class="row stats_area mt-5 mb-5">
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{$s.Online24}}%</span> <span class="lg_number">{{$s.OnlineDaysPercent 1}}%</span>
Online last 24 Hours Online last 24 Hours
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{$s.AvgTime}}ms</span> <span class="lg_number">{{$s.AvgTime}}ms</span>
Average Response Average Response
</div> </div>
<div class="col-4"> <div class="col-4">
<span class="lg_number">{{$s.TotalUptime}}%</span> <span class="lg_number">{{$s.TotalUptime}}%</span>
Total Uptime Total Uptime
@ -62,7 +60,7 @@
<div id="service"></div> <div id="service"></div>
</div> </div>
<div class="col-12"> <div class="service-chart-heatmap">
<div id="service_heatmap"></div> <div id="service_heatmap"></div>
</div> </div>
@ -207,82 +205,137 @@
<script src="/js/flatpickr.js"></script> <script src="/js/flatpickr.js"></script>
<script src="/js/rangePlugin.js"></script> <script src="/js/rangePlugin.js"></script>
<script> <script>
$(document).ready(function() {
let options = {
let options = { chart: {
chart: { height: "100%",
height: "100%", width: "100%",
width: "100%", type: "area",
type: "area", animations: {
animations: { enabled: false,
enabled: false, initialAnimation: {
initialAnimation: { enabled: false
enabled: false
}
},
},
fill: {
colors: ["#48d338"],
opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
},
series: [
{
name: "Response Time",
data: [
{
x: "02-10-2017 GMT",
y: 34
},
{
x: "02-11-2017 GMT",
y: 43
},
{
x: "02-12-2017 GMT",
y: 31
},
{
x: "02-13-2017 GMT",
y: 43
},
{
x: "02-14-2017 GMT",
y: 33
},
{
x: "02-15-2017 GMT",
y: 52
}
]
} }
],
xaxis: {
type: "datetime",
}, },
yaxis: { },
labels: { fill: {
formatter: (value) => { colors: ["#48d338"],
return (value * 0.1).toFixed(0) + "ms" opacity: 1,
type: 'solid'
},
stroke: {
show: true,
curve: 'smooth',
lineCap: 'butt',
colors: ["#3aa82d"],
},
series: [
{
name: "Response Time",
data: [
{
x: "02-10-2017 GMT",
y: 34
}, },
{
x: "02-11-2017 GMT",
y: 43
},
{
x: "02-12-2017 GMT",
y: 31
},
{
x: "02-13-2017 GMT",
y: 43
},
{
x: "02-14-2017 GMT",
y: 33
},
{
x: "02-15-2017 GMT",
y: 52
}
]
}
],
xaxis: {
type: "datetime",
},
yaxis: {
labels: {
formatter: (value) => {
return (value).toFixed(0) + "ms"
}, },
}, },
dataLabels: { },
enabled: false dataLabels: {
enabled: false
},
};
var heat_options = {
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{}],
yaxis: {
labels: {
formatter: (value) => {
return value
},
}, },
}; },
tooltip: {
enabled: true,
x: {
show: false,
},
y: {
formatter: function(val, opts) { return val+" Failures" },
title: {
formatter: (seriesName) => seriesName,
},
},
}
};
async function RenderHeatmap() {
let heatChart = new ApexCharts(
document.querySelector("#service_heatmap"),
heat_options
);
let dataArr = [];
let heatmapData = await ChartHeatmap({{$s.Id}});
heatmapData.forEach(function(d) {
var date = new Date(d.date);
dataArr.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
});
heatChart.render();
heatChart.updateSeries(dataArr);
}
async function RenderChartLatency() {
let chart = new ApexCharts(document.querySelector("#service"), options); let chart = new ApexCharts(document.querySelector("#service"), options);
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
}
AjaxChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour"); $(document).ready(async function() {
let startDate = $("#service_start").flatpickr({ let startDate = $("#service_start").flatpickr({
enableTime: false, enableTime: false,
static: true, static: true,
@ -293,7 +346,7 @@ $(document).ready(function() {
onChange: function(selectedDates, dateStr, instance) { onChange: function(selectedDates, dateStr, instance) {
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000); var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000); var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
$("#service_start").val(one); $("#service_start").val(one);
$("#service_end").val(two); $("#service_end").val(two);
$("#start_date").html(dateStr); $("#start_date").html(dateStr);
}, },
@ -304,46 +357,8 @@ $(document).ready(function() {
startDate.open() startDate.open()
}); });
await RenderChartLatency();
await RenderHeatmap();
var heat_options = {
chart: {
height: "100%",
width: "100%",
type: 'heatmap',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false,
},
enableShades: true,
shadeIntensity: 0.5,
colors: ["#d53a3b"],
series: [{}],
};
var heatChart = new ApexCharts(
document.querySelector("#service_heatmap"),
heat_options
);
var heatmapData = [];
$.ajax({
url: "/api/services/{{$s.Id}}/heatmap",
type: 'GET',
success: function(data) {
data.forEach(function(d) {
var date = new Date(d.date);
heatmapData.push({name: date.toLocaleString('en-us', { month: 'long' }), data: d.data});
});
heatChart.render();
heatChart.updateSeries(heatmapData);
}
});
}); });
</script> </script>

View File

@ -125,7 +125,8 @@
<h3>Additional Settings</h3> <h3>Additional Settings</h3>
<div class="row"> <div class="row">
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a> <a href="/api/clear_cache" class="btn btn-sm btn-secondary float-right">Clear Cache</a>
<a href="/settings/export" class="btn btn-sm btn-secondary float-right">Export Settings</a>
{{if .Domain}} {{if .Domain}}
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a> <a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
{{end}} {{end}}

File diff suppressed because one or more lines are too long

View File

@ -40,6 +40,15 @@ type CheckinInterface interface {
Select() *Checkin Select() *Checkin
} }
// BeforeCreate for Checkin will set CreatedAt to UTC
func (c *Checkin) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
c.UpdatedAt = time.Now().UTC()
}
return
}
// CheckinHit is a successful response from a Checkin // CheckinHit is a successful response from a Checkin
type CheckinHit struct { type CheckinHit struct {
Id int64 `gorm:"primary_key;column:id" json:"id"` Id int64 `gorm:"primary_key;column:id" json:"id"`
@ -48,6 +57,14 @@ type CheckinHit struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }
// BeforeCreate for checkinHit will set CreatedAt to UTC
func (c *CheckinHit) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
}
return
}
// Start will create a channel for the checkin checking go routine // Start will create a channel for the checkin checking go routine
func (s *Checkin) Start() { func (s *Checkin) Start() {
s.Running = make(chan bool) s.Running = make(chan bool)

View File

@ -39,6 +39,14 @@ type FailureInterface interface {
ParseError() string // ParseError returns a human readable error for a service failure ParseError() string // ParseError returns a human readable error for a service failure
} }
// BeforeCreate for Failure will set CreatedAt to UTC
func (f *Failure) BeforeCreate() (err error) {
if f.CreatedAt.IsZero() {
f.CreatedAt = time.Now().UTC()
}
return
}
type FailSort []FailureInterface type FailSort []FailureInterface
func (s FailSort) Len() int { func (s FailSort) Len() int {

View File

@ -34,3 +34,12 @@ type Message struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"`
} }
// BeforeCreate for Message will set CreatedAt to UTC
func (u *Message) BeforeCreate() (err error) {
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now().UTC()
u.UpdatedAt = time.Now().UTC()
}
return
}

View File

@ -53,6 +53,15 @@ type Service struct {
Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty"` Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty"`
} }
// BeforeCreate for Service will set CreatedAt to UTC
func (s *Service) BeforeCreate() (err error) {
if s.CreatedAt.IsZero() {
s.CreatedAt = time.Now().UTC()
s.UpdatedAt = time.Now().UTC()
}
return
}
type ServiceInterface interface { type ServiceInterface interface {
Select() *Service Select() *Service
CheckQueue(bool) CheckQueue(bool)

View File

@ -28,6 +28,14 @@ type Hit struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }
// BeforeCreate for Hit will set CreatedAt to UTC
func (h *Hit) BeforeCreate() (err error) {
if h.CreatedAt.IsZero() {
h.CreatedAt = time.Now().UTC()
}
return
}
// DbConfig struct is used for the database connection and creates the 'config.yml' file // DbConfig struct is used for the database connection and creates the 'config.yml' file
type DbConfig struct { type DbConfig struct {
DbConn string `yaml:"connection"` DbConn string `yaml:"connection"`

View File

@ -39,3 +39,12 @@ type UserInterface interface {
Update() error Update() error
Delete() error Delete() error
} }
// BeforeCreate for User will set CreatedAt to UTC
func (u *User) BeforeCreate() (err error) {
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now().UTC()
u.UpdatedAt = time.Now().UTC()
}
return
}

View File

@ -1 +1 @@
0.80.40 0.80.41