chart updates - ping time - core updates

pull/78/head
Hunter Long 2018-10-02 20:48:40 -07:00
parent 6b5660b8d8
commit baf6ca3d34
16 changed files with 265 additions and 82 deletions

View File

@ -74,20 +74,34 @@ func (s *Service) duration() time.Duration {
return amount
}
func (s *Service) parseHost() string {
if s.Type == "tcp" {
return s.Domain
} else {
domain := s.Domain
hasPort, _ := regexp.MatchString(`\:([0-9]+)`, domain)
if hasPort {
splitDomain := strings.Split(s.Domain, ":")
domain = splitDomain[len(splitDomain)-2]
}
host, err := url.Parse(domain)
if err != nil {
return s.Domain
}
return host.Host
}
}
// dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took
func (s *Service) dnsCheck() (float64, error) {
var err error
t1 := time.Now()
domain := s.Domain
hasPort, _ := regexp.MatchString(`\:([0-9]+)`, domain)
if hasPort {
splitDomain := strings.Split(s.Domain, ":")
domain = splitDomain[len(splitDomain)-2]
host := s.parseHost()
if s.Type == "tcp" {
_, err = net.LookupHost(host)
} else {
_, err = net.LookupIP(host)
}
url, err := url.Parse(domain)
if err != nil {
return 0, err
}
_, err = net.LookupIP(url.Host)
if err != nil {
return 0, err
}
@ -98,6 +112,14 @@ func (s *Service) dnsCheck() (float64, error) {
// checkTcp will check a TCP service
func (s *Service) checkTcp(record bool) *Service {
dnsLookup, err := s.dnsCheck()
if err != nil {
if record {
recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err))
}
return s
}
s.PingTime = dnsLookup
t1 := time.Now()
domain := fmt.Sprintf("%v", s.Domain)
if s.Port != 0 {
@ -134,7 +156,7 @@ func (s *Service) checkHttp(record bool) *Service {
}
return s
}
s.DnsLookup = dnsLookup
s.PingTime = dnsLookup
t1 := time.Now()
timeout := time.Duration(s.Timeout)
client := http.Client{
@ -213,9 +235,10 @@ func recordSuccess(s *Service) {
hit := &types.Hit{
Service: s.Id,
Latency: s.Latency,
PingTime: s.PingTime,
CreatedAt: time.Now(),
}
utils.Log(1, fmt.Sprintf("Service %v Successful: %0.2f ms", s.Name, hit.Latency*1000))
utils.Log(1, fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
s.CreateHit(hit)
notifier.OnSuccess(s.Service)
}
@ -226,9 +249,10 @@ func recordFailure(s *Service, issue string) {
fail := &types.Failure{
Service: s.Id,
Issue: issue,
PingTime: s.PingTime,
CreatedAt: time.Now(),
}
utils.Log(2, fmt.Sprintf("Service %v Failing: %v", s.Name, issue))
utils.Log(2, fmt.Sprintf("Service %v Failing: %v | Lookup in: %0.2f ms", s.Name, issue, fail.PingTime*1000))
s.CreateFailure(fail)
notifier.OnFailure(s.Service, fail)
}

View File

@ -67,8 +67,8 @@ 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, group string) *gorm.DB {
selector := Dbtimestamp(group)
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
selector := Dbtimestamp(group, column)
return DbSession.Model(&types.Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.Format(types.TIME_DAY), t2.Format(types.TIME_DAY)).Order("timeframe asc", false).Group("timeframe")
}
@ -291,7 +291,7 @@ func (db *DbConfig) DropDatabase() error {
func (db *DbConfig) CreateDatabase() error {
utils.Log(1, "Creating Database Tables...")
err := DbSession.CreateTable(&types.Checkin{})
err = DbSession.CreateTable(&types.CheckinHit{})
//err = DbSession.CreateTable(&types.CheckinHit{})
err = DbSession.CreateTable(&notifier.Notification{})
err = DbSession.Table("core").CreateTable(&types.Core{})
err = DbSession.CreateTable(&types.Failure{})
@ -317,7 +317,7 @@ func (db *DbConfig) MigrateDatabase() error {
if tx.Error != nil {
return tx.Error
}
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
tx = tx.AutoMigrate(&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Checkin{}, &notifier.Notification{}).Table("core").AutoMigrate(&types.Core{})
if tx.Error != nil {
tx.Rollback()
utils.Log(3, fmt.Sprintf("Statup Database could not be migrated: %v", tx.Error))

View File

@ -172,7 +172,7 @@ func (s *Service) DowntimeText() string {
return fmt.Sprintf("%v has been offline for %v", s.Name, utils.DurationReadable(s.Downtime()))
}
func Dbtimestamp(group string) string {
func Dbtimestamp(group string, column string) string {
seconds := 60
if group == "second" {
seconds = 60
@ -183,11 +183,11 @@ func Dbtimestamp(group string) string {
}
switch CoreApp.DbConnection {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(latency) AS value")
return fmt.Sprintf("CONCAT(date_format(created_at, '%%Y-%%m-%%d %%H:00:00')) AS timeframe, AVG(%v) AS value", column)
case "sqlite":
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(latency) as value", seconds, seconds)
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch') AS timeframe, AVG(%v) as value", seconds, seconds, column)
case "postgres":
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(latency) AS value", group)
return fmt.Sprintf("date_trunc('%v', created_at) AS timeframe, AVG(%v) AS value", group, column)
default:
return ""
}
@ -207,9 +207,9 @@ func (s *Service) Downtime() time.Duration {
return since
}
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string) *DateScanObj {
func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj {
var d []DateScan
model := service.(*Service).HitsBetween(start, end, group)
model := service.(*Service).HitsBetween(start, end, group, column)
rows, _ := model.Rows()
for rows.Next() {
var gd DateScan
@ -241,7 +241,7 @@ func (d *DateScanObj) ToString() string {
func (s *Service) GraphData() string {
start := time.Now().Add((-24 * 7) * time.Hour)
end := time.Now()
obj := GraphDataRaw(s, start, end, "hour")
obj := GraphDataRaw(s, start, end, "hour", "latency")
data, err := json.Marshal(obj)
if err != nil {
utils.Log(2, err)

View File

@ -100,6 +100,7 @@ func TestCheckHTTPService(t *testing.T) {
assert.True(t, service.Online)
assert.Equal(t, 200, service.LastStatusCode)
assert.NotZero(t, service.Latency)
assert.NotZero(t, service.PingTime)
}
func TestServiceTCPCheck(t *testing.T) {
@ -114,6 +115,7 @@ func TestCheckTCPService(t *testing.T) {
assert.Equal(t, "Changed Google DNS", service.Name)
assert.True(t, service.Online)
assert.NotZero(t, service.Latency)
assert.NotZero(t, service.PingTime)
}
func TestServiceOnline24Hours(t *testing.T) {

View File

@ -89,7 +89,24 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
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)
obj := core.GraphDataRaw(service, time.Unix(startField, 0), time.Unix(endField, 0), grouping, "latency")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(obj)
}
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
fields := parseGet(r)
grouping := fields.Get("group")
startField := utils.StringInt(fields.Get("start"))
endField := utils.StringInt(fields.Get("end"))
obj := core.GraphDataRaw(service, time.Unix(startField, 0), time.Unix(endField, 0), grouping, "ping_time")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(obj)

View File

@ -164,7 +164,7 @@ func executeResponse(w http.ResponseWriter, r *http.Request, file string, data i
templates := []string{"base.html", "head.html", "nav.html", "footer.html", "scripts.html", "form_service.html", "form_notifier.html", "form_user.html"}
javascripts := []string{"chart_index.js"}
javascripts := []string{"charts.js", "chart_index.js"}
render, err := source.TmplBox.String(file)
if err != nil {
@ -221,9 +221,19 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data
"safe": func(html string) template.HTML {
return template.HTML(html)
},
"Services": func() []types.ServiceInterface {
return core.CoreApp.Services
},
})
t.Parse(render)
t.Execute(w, data)
_, err = t.Parse(render)
if err != nil {
utils.Log(4, err)
}
err = t.Execute(w, data)
if err != nil {
utils.Log(4, err)
}
}
// error404Handler is a HTTP handler for 404 error pages

View File

@ -47,7 +47,7 @@ func Router() *mux.Router {
r.PathPrefix("/statup.png").Handler(http.FileServer(source.TmplBox.HTTPBox()))
}
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(source.JsBox.HTTPBox())))
r.Handle("/charts/{id}.js", http.HandlerFunc(renderServiceChartHandler))
//r.Handle("/charts/{id}.js", http.HandlerFunc(renderServiceChartHandler))
r.Handle("/charts.js", http.HandlerFunc(renderServiceChartsHandler))
r.Handle("/setup", http.HandlerFunc(setupHandler)).Methods("GET")
r.Handle("/setup", http.HandlerFunc(processSetupHandler)).Methods("POST")
@ -87,6 +87,7 @@ func Router() *mux.Router {
r.Handle("/api/services", http.HandlerFunc(apiCreateServiceHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceHandler)).Methods("GET")
r.Handle("/api/services/{id}/data", http.HandlerFunc(apiServiceDataHandler)).Methods("GET")
r.Handle("/api/services/{id}/ping", http.HandlerFunc(apiServicePingDataHandler)).Methods("GET")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceUpdateHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(apiServiceDeleteHandler)).Methods("DELETE")

View File

@ -54,14 +54,14 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) {
}
service := core.SelectService(utils.StringInt(vars["id"]))
data := core.GraphDataRaw(service, start, end, "hour").ToString()
data := core.GraphDataRaw(service, start, end, "hour", "latency").ToString()
out := struct {
Services []*core.Service
Data []string
}{[]*core.Service{service}, []string{data}}
executeJSResponse(w, r, "charts.js", out)
executeResponse(w, r, "charts.js", out, nil)
}
func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
@ -69,19 +69,16 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60")
var data []string
end := now.EndOfDay().UTC()
start := now.BeginningOfDay().UTC()
//var data []string
//end := now.EndOfDay().UTC()
//start := now.BeginningOfDay().UTC()
var srvs []*core.Service
for _, s := range services {
d := core.GraphDataRaw(s, start, end, "hour").ToString()
data = append(data, d)
srvs = append(srvs, s.(*core.Service))
}
out := struct {
Services []types.ServiceInterface
Data []string
}{services, data}
Services []*core.Service
}{srvs}
executeJSResponse(w, r, "charts.js", out)
}
@ -194,7 +191,7 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) {
end = time.Unix(endField, 0)
}
data := core.GraphDataRaw(serv, start, end, "hour")
data := core.GraphDataRaw(serv, start, end, "hour", "latency")
out := struct {
Service *core.Service

View File

@ -6,8 +6,8 @@ var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
datasets: [{
label: 'Response Time (Milliseconds)',
data: [],
backgroundColor: ['rgba(47, 206, 30, 0.92)'],
borderColor: ['rgb(47, 171, 34)'],
backgroundColor: ['{{if .Online}}rgba(47, 206, 30, 0.92){{else}}rgb(221, 53, 69){{end}}'],
borderColor: ['{{if .Online}}rgb(47, 171, 34){{else}}rgb(183, 32, 47){{end}}'],
borderWidth: 1
}]
},
@ -88,7 +88,7 @@ var chartdata_{{js .Id}} = new Chart(ctx_{{js .Id}}, {
display: !1
},
tooltips: {
"enabled": !1
enabled: !1
},
scales: {
yAxes: [{

View File

@ -1,23 +1,137 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
{{define "charts"}}{{ range Services }}
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: ['{{if .Online}}rgba(47, 206, 30, 0.92){{else}}rgb(221, 53, 69){{end}}'],
borderColor: ['{{if .Online}}rgb(47, 171, 34){{else}}rgb(183, 32, 47){{end}}'],
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
}
}
}
});
{{$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,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 }}
AjaxChart(chartdata_{{js .Id}},{{js .Id}},0,99999999999,"hour");
{{end}}{{end}}

View File

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

View File

@ -1,5 +1,5 @@
{{define "form_service"}}
<form action="{{if .}}/service/{{.Id}}{{else}}/service{{end}}" method="POST">
<form action="{{if ne .Id 0}}/service/{{.Id}}{{else}}/service{{end}}" method="POST">
<div class="form-group row">
<label for="service_name" class="col-sm-4 col-form-label">Service Name</label>
<div class="col-sm-8">
@ -49,26 +49,26 @@
<div class="form-group row{{if eq .Type "tcp"}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{.ExpectedStatus}}" id="service_response_code">
<input type="number" name="expected_status" class="form-control" value="{{if ne .ExpectedStatus 0}}{{.ExpectedStatus}}{{end}}" placeholder="200" id="service_response_code">
</div>
</div>
<div class="form-group row{{if eq .Type "http"}} d-none{{end}}">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{.Port}}" id="service_port" placeholder="8080">
<input type="number" name="port" class="form-control" value="{{if ne .Port 0}}{{.Port}}{{end}}" id="service_port" placeholder="8080">
</div>
</div>
<div class="form-group row">
<label for="service_interval" class="col-sm-4 col-form-label">Check Interval (Seconds)</label>
<div class="col-sm-8">
<input type="number" name="interval" class="form-control" value="{{.Interval}}" min="1" id="service_interval" required>
<input type="number" name="interval" class="form-control" value="{{if ne .Interval 0}}{{.Interval}}{{end}}" min="1" id="service_interval" required>
<small id="emailHelp" class="form-text text-muted">10,000+ will be checked in Microseconds (1 millisecond = 1000 microseconds).</small>
</div>
</div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="{{.Timeout}}" id="service_timeout" min="1">
<input type="number" name="timeout" class="form-control" value="{{if ne .Timeout 0}}{{.Timeout}}{{end}}" placeholder="15" id="service_timeout" min="1">
</div>
</div>
<div class="form-group row">
@ -78,12 +78,14 @@
</div>
</div>
<div class="form-group row">
<div class="col-6">
<button type="submit" class="btn btn-success btn-block">Update Service</button>
<div class="{{if ne .Id 0}}col-6{{else}}col-12{{end}}">
<button type="submit" class="btn btn-success btn-block">{{if ne .Id 0}}Update Service{{else}}Create Service{{end}}</button>
</div>
{{if ne .Id 0}}
<div class="col-6">
<a href="/service/{{ .Id }}/delete_failures" class="btn btn-danger btn-block confirm-btn">Delete All Failures</a>
</div>
{{end}}
</div>
</form>
{{end}}

View File

@ -1,5 +1,5 @@
{{define "form_user"}}
<form action="{{if .}}/user/{{.Id}}{{else}}/user{{end}}" method="POST">
<form action="{{if ne .Id 0}}/user/{{.Id}}{{else}}/user{{end}}" method="POST">
<div class="form-group row">
<label for="username" class="col-sm-4 col-form-label">Username</label>
<div class="col-6 col-md-4">
@ -32,7 +32,7 @@
</div>
<div class="form-group row">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-block">Update User</button>
<button type="submit" class="btn btn-primary btn-block">{{if ne .Id 0}}Update User{{else}}Create User{{end}}</button>
</div>
</div>
</form>

View File

@ -80,9 +80,5 @@
</div>
{{end}}
{{define "extra_scripts"}}
<script>
{{ range Services }}
{{template "chartIndex" .}}
{{end}}
</script>
<script src="/charts.js"></script>
{{end}}

View File

@ -26,6 +26,7 @@ type Failure struct {
Issue string `gorm:"column:issue" json:"issue"`
Method string `gorm:"column:method" json:"method,omitempty"`
Service int64 `gorm:"index;column:service" json:"-"`
PingTime float64 `gorm:"column:ping_time"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
FailureInterface `gorm:"-" json:"-"`
}

View File

@ -36,6 +36,7 @@ type Service struct {
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Online bool `gorm:"-" json:"online"`
Latency float64 `gorm:"-" json:"latency"`
PingTime float64 `gorm:"-" json:"ping_time"`
Online24Hours float32 `gorm:"-" json:"24_hours_online"`
AvgResponse string `gorm:"-" json:"avg_response"`
Running chan bool `gorm:"-" json:"-"`
@ -44,7 +45,6 @@ type Service struct {
LastResponse string `gorm:"-" json:"-"`
LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_online"`
DnsLookup float64 `gorm:"-" json:"dns_lookup_time"`
Failures []interface{} `gorm:"-" json:"failures,omitempty"`
Checkins []*Checkin `gorm:"-" json:"checkins,omitempty"`
}