service api data - charts js updates (ajax rendering

pull/78/head
Hunter Long 2018-09-30 06:39:52 -07:00
parent aba8456b90
commit b24f939541
15 changed files with 298 additions and 54 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.70
VERSION=0.71
BINARY_NAME=statup
GOPATH:=$(GOPATH)
GOCMD=go

View File

@ -67,9 +67,9 @@ func checkinDB() *gorm.DB {
}
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time) *gorm.DB {
selector := Dbtimestamp(3600)
return DbSession.Model(&types.Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME)).Group("timeframe")
func (s *Service) HitsBetween(t1, t2 time.Time, group string) *gorm.DB {
selector := Dbtimestamp(group)
return DbSession.Debug().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")
}
func CloseDB() {

View File

@ -128,7 +128,7 @@ type DateScan struct {
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []DateScan
Array []DateScan `json:"data"`
}
// lastFailure returns the last failure a service had
@ -166,12 +166,14 @@ func (s *Service) DowntimeText() string {
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
}
func Dbtimestamp(seconds int64) string {
incrementTime := "second"
if seconds == 60 {
incrementTime = "minute"
} else if seconds == 3600 {
incrementTime = "hour"
func Dbtimestamp(group string) string {
seconds := 60
if group == "second" {
seconds = 60
} else if group == "hour" {
seconds = 3600
} else if group == "day" {
seconds = 86400
}
switch CoreApp.DbConnection {
case "mysql":
@ -179,7 +181,7 @@ func Dbtimestamp(seconds int64) string {
case "sqlite":
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(latency) as value", seconds, seconds)
case "postgres":
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(latency) AS value", incrementTime)
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(latency) AS value", group)
default:
return ""
}
@ -199,16 +201,20 @@ func (s *Service) Downtime() time.Duration {
return since
}
func GraphDataRaw(service types.ServiceInterface, start, end time.Time) *DateScanObj {
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string) *DateScanObj {
var d []DateScan
model := service.(*Service).HitsBetween(start, end)
model := service.(*Service).HitsBetween(start, end, group)
rows, _ := model.Rows()
for rows.Next() {
var gd DateScan
var createdAt string
var value float64
var createdTime time.Time
rows.Scan(&createdAt, &value)
createdTime, _ := time.Parse(types.TIME, createdAt)
createdTime, _ = time.Parse(types.TIME, createdAt)
if CoreApp.DbConnection == "postgres" {
createdTime, _ = time.Parse(types.TIME_NANO, createdAt)
}
gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.TIME)
gd.Value = int64(value * 1000)
d = append(d, gd)
@ -227,9 +233,9 @@ func (d *DateScanObj) ToString() string {
// GraphData returns the JSON object used by Charts.js to render the chart
func (s *Service) GraphData() string {
start := time.Now().Add(-24 * time.Hour)
start := time.Now().Add((-24 * 7) * time.Hour)
end := time.Now()
obj := GraphDataRaw(s, start, end)
obj := GraphDataRaw(s, start, end, "hour")
data, err := json.Marshal(obj)
if err != nil {
utils.Log(2, err)

View File

@ -23,6 +23,7 @@ import (
"github.com/hunterlong/statup/utils"
"net/http"
"os"
"time"
)
type ApiResponse struct {
@ -77,6 +78,23 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(checkin)
}
func apiServiceDataHandler(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)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(obj)
}
func apiServiceHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)

View File

@ -237,6 +237,20 @@ func TestApiDeleteUserHandler(t *testing.T) {
assert.Equal(t, "success", obj.Status)
}
func TestApiServiceDataHandler(t *testing.T) {
grouping := []string{"minute", "hour", "day"}
for _, g := range grouping {
params := "?start=0&end=999999999999&group=" + g
rr, err := httpRequestAPI(t, "GET", "/api/services/1/data"+params, nil)
assert.Nil(t, err)
body := rr.Body.String()
var obj core.DateScanObj
formatJSON(body, &obj)
assert.Equal(t, 200, rr.Code)
assert.NotZero(t, len(obj.Array))
}
}
func httpRequestAPI(t *testing.T, method, url string, body io.Reader) (*httptest.ResponseRecorder, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {

View File

@ -84,26 +84,13 @@ func IsAuthenticated(r *http.Request) bool {
return session.Values["authenticated"].(bool)
}
// 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)
if url, ok := redirect.(string); ok {
http.Redirect(w, r, url, http.StatusSeeOther)
return
}
nav, _ := source.TmplBox.String("nav.html")
footer, _ := source.TmplBox.String("footer.html")
render, err := source.TmplBox.String(file)
if err != nil {
utils.Log(4, err)
}
t := template.New("message")
t.Funcs(template.FuncMap{
"js": func(html string) template.JS {
return template.JS(html)
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)
"safe": func(html interface{}) template.HTML {
return template.HTML(utils.ToString(html))
},
"Auth": func() bool {
return IsAuthenticated(r)
@ -156,7 +143,25 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
"FromUnix": func(t int64) string {
return utils.Timezoner(time.Unix(t, 0), core.CoreApp.Timezone).Format("Monday, January 02")
},
})
}
}
// 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)
if url, ok := redirect.(string); ok {
http.Redirect(w, r, url, http.StatusSeeOther)
return
}
nav, _ := source.TmplBox.String("nav.html")
footer, _ := source.TmplBox.String("footer.html")
chartIndex, _ := source.JsBox.String("chart_index.js")
render, err := source.TmplBox.String(file)
if err != nil {
utils.Log(4, err)
}
t := template.New("message")
t.Funcs(handlerFuncs(w, r))
t, err = t.Parse(nav)
if err != nil {
utils.Log(4, err)
@ -169,6 +174,15 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
if err != nil {
utils.Log(4, err)
}
_, err = t.Parse(chartIndex)
if err != nil {
utils.Log(4, err)
}
fmt.Println(t.Templates())
fmt.Println(t.DefinedTemplates())
t.Lookup("chartIndex").Funcs(handlerFuncs(w, r))
err = t.Execute(w, data)
if err != nil {
utils.Log(4, err)

View File

@ -86,6 +86,7 @@ func Router() *mux.Router {
r.Handle("/api/services", http.HandlerFunc(apiAllServicesHandler)).Methods("GET")
r.Handle("/api/services", http.HandlerFunc(apiCreateServiceHandler)).Methods("POST")
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}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")

View File

@ -54,7 +54,7 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) {
}
service := core.SelectService(utils.StringInt(vars["id"]))
data := core.GraphDataRaw(service, start, end).ToString()
data := core.GraphDataRaw(service, start, end, "hour").ToString()
out := struct {
Services []*core.Service
@ -74,7 +74,7 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
start := now.BeginningOfDay().UTC()
for _, s := range services {
d := core.GraphDataRaw(s, start, end).ToString()
d := core.GraphDataRaw(s, start, end, "hour").ToString()
data = append(data, d)
}
@ -194,7 +194,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
end = time.Unix(endField, 0)
}
data := core.GraphDataRaw(serv, start, end)
data := core.GraphDataRaw(serv, start, end, "hour")
out := struct {
Service *core.Service

137
source/js/chart_index.js Normal file
View File

@ -0,0 +1,137 @@
{{define "chartIndex"}}
var ctx_{{js .Id}} = document.getElementById("service_{{js .Id}}").getContext('2d');
var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
type: 'line',
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: [],
backgroundColor: ['rgba(47, 206, 30, 0.92)'],
borderColor: ['rgb(47, 171, 34)'],
borderWidth: 1
}]
},
options: {
maintainAspectRatio: !1,
scaleShowValues: !0,
layout: {
padding: {
left: 0,
right: 0,
top: 0,
bottom: -10
}
},
hover: {
animationDuration: 0,
},
responsiveAnimationDuration: 0,
animation: {
duration: 3500,
onComplete: function() {
var chartInstance = this.chart,
ctx = chartInstance.ctx;
var controller = this.chart.controller;
var xAxis = controller.scales['x-axis-0'];
var yAxis = controller.scales['y-axis-0'];
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
var numTicks = xAxis.ticks.length;
var yOffsetStart = xAxis.width / numTicks;
var halfBarWidth = (xAxis.width / (numTicks * 2));
xAxis.ticks.forEach(function(value, index) {
var xOffset = 20;
var yOffset = (yOffsetStart * index) + halfBarWidth;
ctx.fillStyle = '#e2e2e2';
ctx.fillText(value, yOffset, xOffset)
});
this.data.datasets.forEach(function(dataset, i) {
var meta = chartInstance.controller.getDatasetMeta(i);
var hxH = 0;
var hyH = 0;
var hxL = 0;
var hyL = 0;
var highestNum = 0;
var lowestnum = 999999999999;
meta.data.forEach(function(bar, index) {
var data = dataset.data[index];
if (lowestnum > data.y) {
lowestnum = data.y;
hxL = bar._model.x;
hyL = bar._model.y
}
if (data.y > highestNum) {
highestNum = data.y;
hxH = bar._model.x;
hyH = bar._model.y
}
});
if (hxH >= 820) {
hxH = 820
} else if (50 >= hxH) {
hxH = 50
}
if (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
}
}
}
});
AjaxChart(chartdata_{{js .Id}},{{js .Id}},0,99999999999,"hour");
{{end}}

View File

@ -18,6 +18,6 @@
{{$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}
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);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}})
ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.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 }}

View File

@ -81,6 +81,20 @@ $('select#service_type').on('change', function() {
});
function AjaxChart(chart, service, start=0, end=9999999999, group="hour") {
$.ajax({
url: "/api/services/"+service+"/data?start="+start+"&end="+end+"&group="+group,
type: 'GET',
success: function(data) {
chart.data.labels.pop();
data.data.forEach(function(d) {
chart.data.datasets[0].data.push(d);
});
chart.update();
}
});
}
$('select#service_check_type').on('change', function() {
var selected = $('#service_check_type option:selected').val();
if (selected === 'POST') {

21
source/tmpl/base.html Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1.0, user-scalable=0">
{{if USE_CDN}}
<link rel="shortcut icon" type="image/x-icon" href="https://assets.statup.io/favicon.ico">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<link rel="stylesheet" href="https://assets.statup.io/base.css">
{{ else }}
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
{{end}}
<title>{{.Name}} Status</title>
</head>
<body>
</body>
</html>

View File

@ -112,18 +112,17 @@
<script src="/js/main.js"></script>
{{end}}
{{if CHART_DATA}}
<script>{{ js CHART_DATA }}</script>
{{ else }}
<script src="/charts.js"></script>
{{ if .Style }}
<style>
{{ safe .Style }}
</style>
{{ end }}
{{ if .Style }}
<style>
{{ safe .Style }}
</style>
{{ end }}
<script>
{{ range Services }}
{{template "chartIndex" .}}
{{end}}
</script>
</body>
</html>

View File

@ -255,7 +255,7 @@
data: {
datasets: [{
label: 'Response Time (Milliseconds)',
data: {{js .Data}},
data: [],
backgroundColor: [
'rgba(47, 206, 30, 0.92)'
],
@ -280,10 +280,27 @@
}],
xAxes: [{
type: 'time',
distribution: 'series',
time: {
displayFormats: {
'millisecond': 'MMM DD',
'second': 'MMM DD',
'minute': 'MMM DD',
'hour': 'MMM DD hA',
'day': 'MMM DD',
'week': 'MMM DD',
'month': 'MMM DD',
'quarter': 'MMM DD',
'year': 'MMM DD',
}
},
gridLines: {
display: true
},
ticks: {
source: 'auto'
}
}]
}],
},
elements: {
point: {
@ -332,6 +349,8 @@
endPick.show()
})
AjaxChart(chartdata,{{$s.Id}},{{.Start}},{{.End}},"hour");
</script>
</body>

View File

@ -23,6 +23,7 @@ const (
TIME_NANOZ = "2006-01-02 15:04:05.999999-0700 MST"
TIME_NANO = "2006-01-02T15:04:05Z"
TIME = "2006-01-02 15:04:05"
TIME_DAY = "2006-01-02"
)
var (