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
}
// 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
func (db *DbConfig) InsertCore() (*Core, error) {
CoreApp = &Core{Core: &types.Core{

View File

@ -31,6 +31,7 @@ type Failure struct {
const (
limitedFailures = 32
limitedHits = 32
)
// 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
func (s *Service) LimitedHits() ([]*types.Hit, error) {
func (s *Service) LimitedHits(amount int64) ([]*types.Hit, error) {
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)
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
func (s *Service) TotalHits() (uint64, error) {
var count uint64
col := hitsDB().Where("service = ?", s.Id)
err := col.Count(&count)
return count, err.Error
col := hitsDB().Where("service = ?", s.Id).Count(&count)
return count, col.Error
}
// TotalHitsSince returns the total amount of hits based on a specific time/date
func (s *Service) TotalHitsSince(ago time.Time) (uint64, error) {
var count uint64
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05"))
err := rows.Count(&count)
return count, err.Error
rows := hitsDB().Where("service = ? AND created_at > ?", s.Id, ago.UTC().Format("2006-01-02 15:04:05")).Count(&count)
return count, rows.Error
}
// Sum returns the added value Latency for all of the services successful hits.

View File

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

View File

@ -92,7 +92,7 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
checkins := service.AllCheckins()
for _, c := range checkins {
c.Failures = c.LimitedFailures(limitedFailures)
c.Hits = c.LimitedHits(limitedFailures)
c.Hits = c.LimitedHits(limitedHits)
service.Checkins = append(service.Checkins, c)
}
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
func (s *Service) AvgTime() float64 {
func (s *Service) AvgTime() string {
total, _ := s.TotalHits()
if total == 0 {
return float64(0)
return "0"
}
sum := s.Sum()
avg := sum / float64(total) * 100
amount := fmt.Sprintf("%0.0f", avg*10)
val, _ := strconv.ParseFloat(amount, 10)
return val
return fmt.Sprintf("%0.0f", avg*10)
}
// 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
@ -185,7 +189,7 @@ func (s *Service) lastFailure() *Failure {
// // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string {
last := s.LimitedFailures(1)
hits, _ := s.LimitedHits()
hits, _ := s.LimitedHits(1)
zone := CoreApp.Timezone
if s.Online {
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
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var d []DateScan
var amount int64
var data []DateScan
outgoing := new(DateScanObj)
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")
rows, err := model.Rows()
if err != nil {
utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err))
}
for rows.Next() {
var gd DateScan
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.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

View File

@ -156,7 +156,7 @@ func TestServiceHits(t *testing.T) {
func TestServiceLimitedHits(t *testing.T) {
service := SelectService(5)
hits, err := service.LimitedHits()
hits, err := service.LimitedHits(1024)
assert.Nil(t, err)
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)
}
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) {
utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
output := apiResponse{

View File

@ -46,9 +46,6 @@ func NewStorage() *Storage {
//Get a cached content by key
func (s Storage) Get(key string) []byte {
s.mu.RLock()
defer s.mu.RUnlock()
item := s.items[key]
if item.Expired() {
CacheStorage.Delete(key)
@ -93,10 +90,10 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
w.Write(content)
return
}
if d, err := time.ParseDuration(duration); err == nil {
CacheStorage.Set(r.RequestURI, content, d)
}
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 (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/gorilla/sessions"
"github.com/hunterlong/statping/core"
@ -27,20 +26,23 @@ import (
"html/template"
"net/http"
"os"
"reflect"
"strings"
"time"
)
const (
cookieKey = "statping_auth"
timeout = time.Second * 60
timeout = time.Second * 30
)
var (
sessionStore *sessions.CookieStore
httpServer *http.Server
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
@ -91,6 +93,7 @@ func RunHTTPServer(ip string, port int) error {
IdleTimeout: timeout,
Handler: router,
}
httpServer.SetKeepAlivesEnabled(false)
return httpServer.ListenAndServe()
}
return nil
@ -166,112 +169,37 @@ func IsUser(r *http.Request) bool {
return session.Values["authenticated"].(bool)
}
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)
},
func loadTemplate(w http.ResponseWriter, r *http.Request) error {
var err error
mainTemplate = template.New("main")
mainTemplate.Funcs(handlerFuncs(w, r))
mainTemplate, err = mainTemplate.Parse(mainTmpl)
if err != nil {
utils.Log(4, err)
return err
}
// 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
func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data interface{}, redirect interface{}) {
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)
return
}
if usingSSL {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
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"}
loadTemplate(w, r)
render, err := source.TmplBox.String(file)
if err != nil {
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
_, err = t.Parse(render)
_, err = mainTemplate.Parse(render)
if err != nil {
utils.Log(4, err)
}
// execute the template
err = t.Execute(w, data)
err = mainTemplate.Execute(w, data)
if err != nil {
utils.Log(4, err)
}

View File

@ -35,7 +35,7 @@ func Router() *mux.Router {
dir := utils.Directory
CacheStorage = NewStorage()
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) {
indexHandler := http.FileServer(http.Dir(dir + "/assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(dir+"/assets/css"))))
@ -95,6 +95,7 @@ func Router() *mux.Router {
// API Routes
r.Handle("/api", http.HandlerFunc(apiIndexHandler))
r.Handle("/api/renew", http.HandlerFunc(apiRenewHandler))
r.Handle("/api/clear_cache", http.HandlerFunc(apiClearCacheHandler))
// API SERVICE Routes
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/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}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET")
r.Handle("/api/services/{id}/heatmap", http.HandlerFunc(apiServiceHeatmapHandler)).Methods("GET")
r.Handle("/api/services/{id}/ping", cached("30s", "application/json", http.HandlerFunc(apiServicePingDataHandler))).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(apiServiceDeleteHandler)).Methods("DELETE")
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("/tray", http.HandlerFunc(trayHandler))
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
r.NotFoundHandler = http.HandlerFunc(error404Handler)
return r
}

View File

@ -18,6 +18,7 @@ package handlers
import (
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
@ -227,7 +228,7 @@ type dataXy struct {
}
type dataXyMonth struct {
Date time.Time `json:"date"`
Date string `json:"date"`
Data []*dataXy `json:"data"`
}
@ -242,26 +243,37 @@ func apiServiceHeatmapHandler(w http.ResponseWriter, r *http.Request) {
var monthOutput []*dataXyMonth
start := service.CreatedAt
//now := time.Now()
if start.Year() <= 2 {
start = service.CreatedAt.Add(time.Duration((-3 * 24) * time.Hour))
}
sY, sM, _ := start.Date()
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
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)
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")

View File

@ -148,6 +148,12 @@ HTML, BODY {
width: 100%;
}
.service-chart-heatmap {
position: relative;
height: 300px;
width: 100%;
}
.btn-primary {
background-color: #3e9bff;
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 }}
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);
})
await RenderChart(chart{{js .Id}}, {{js .Id}}, startOn, endOn);{{end}}
}
$( document ).ready(function() {
{{ range .Services }}AjaxChart(chart{{js .Id}}, {{js .Id}}, 0, 9999999999);
{{end}}
});
RenderCharts()
});
{{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({
url: "/api/services/"+service+"/data?start="+start+"&end="+end+"&group="+group,
type: 'GET',
success: function(data) {
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
}]);
async function RenderChart(chart, service, start=0, end=9999999999, group="hour", retry=true) {
let chartData = await ChartLatency(service, start, end, group, retry);
if (chartData.length === 0) {
chartData = await ChartLatency(service, start, end, "minute", retry);
}
});
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) {

View File

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

View File

@ -42,7 +42,7 @@
{{end}}
<div class="col-12 full-col-12">
{{ if not Services }}
{{ if not .Services }}
<div class="alert alert-danger" role="alert">
<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>
@ -51,7 +51,8 @@
</div>
{{end}}
{{ range Services }}
{{ range .Services }}
{{$avgTime := .AvgTime}}
<div class="mb-4" id="service_id_{{.Id}}">
<div class="card">
<div class="card-body">
@ -65,22 +66,22 @@
<div class="row stats_area mt-5">
<div class="col-4">
<span class="lg_number">{{.AvgTime}}ms</span>
<span class="lg_number">{{$avgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{.Online24}}%</span>
<span class="lg_number">{{.OnlineDaysPercent 1}}%</span>
Uptime last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{.Online7Days}}%</span>
<span class="lg_number">{{.OnlineDaysPercent 7}}%</span>
Uptime last 7 Days
</div>
</div>
</div>
</div>
{{ if .AvgUptime24 }}
{{ if $avgTime }}
<div class="chart-container">
<div id="service_{{ .Id }}"></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>
{{end}}
<h4 class="mt-2">{{ $s.Name }}
<h4 class="mt-2"><a href="/">{{CoreApp.Name}}</a> - {{ $s.Name }}
{{if $s.Online }}
<span class="badge bg-success float-right d-none d-md-block">ONLINE</span>
{{ else }}
@ -28,15 +28,13 @@
<div class="row stats_area mt-5 mb-5">
<div class="col-4">
<span class="lg_number">{{$s.Online24}}%</span>
<span class="lg_number">{{$s.OnlineDaysPercent 1}}%</span>
Online last 24 Hours
</div>
<div class="col-4">
<span class="lg_number">{{$s.AvgTime}}ms</span>
Average Response
</div>
<div class="col-4">
<span class="lg_number">{{$s.TotalUptime}}%</span>
Total Uptime
@ -62,7 +60,7 @@
<div id="service"></div>
</div>
<div class="col-12">
<div class="service-chart-heatmap">
<div id="service_heatmap"></div>
</div>
@ -207,82 +205,137 @@
<script src="/js/flatpickr.js"></script>
<script src="/js/rangePlugin.js"></script>
<script>
$(document).ready(function() {
let options = {
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: false,
initialAnimation: {
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
}
]
let options = {
chart: {
height: "100%",
width: "100%",
type: "area",
animations: {
enabled: false,
initialAnimation: {
enabled: false
}
],
xaxis: {
type: "datetime",
},
yaxis: {
labels: {
formatter: (value) => {
return (value * 0.1).toFixed(0) + "ms"
},
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: {
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);
await RenderChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
}
AjaxChart(chart,{{$s.Id}},{{.StartUnix}},{{.EndUnix}},"hour");
$(document).ready(async function() {
let startDate = $("#service_start").flatpickr({
enableTime: false,
static: true,
@ -293,7 +346,7 @@ $(document).ready(function() {
onChange: function(selectedDates, dateStr, instance) {
var one = Math.round((new Date(selectedDates[0])).getTime() / 1000);
var two = Math.round((new Date(selectedDates[1])).getTime() / 1000);
$("#service_start").val(one);
$("#service_start").val(one);
$("#service_end").val(two);
$("#start_date").html(dateStr);
},
@ -304,46 +357,8 @@ $(document).ready(function() {
startDate.open()
});
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);
}
});
await RenderChartLatency();
await RenderHeatmap();
});
</script>

View File

@ -125,7 +125,8 @@
<h3>Additional Settings</h3>
<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}}
<a href="#" class="btn btn-sm btn-secondary float-right ml-1">Authentication QR Code</a>
{{end}}

File diff suppressed because one or more lines are too long

View File

@ -40,6 +40,15 @@ type CheckinInterface interface {
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
type CheckinHit struct {
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"`
}
// 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
func (s *Checkin) Start() {
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
}
// 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
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"`
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"`
}
// 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 {
Select() *Service
CheckQueue(bool)

View File

@ -28,6 +28,14 @@ type Hit struct {
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
type DbConfig struct {
DbConn string `yaml:"connection"`

View File

@ -39,3 +39,12 @@ type UserInterface interface {
Update() 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