diff --git a/Makefile b/Makefile index 561bfee1..6b3fa098 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.65 +VERSION=0.66 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go diff --git a/cmd/main.go b/cmd/main.go index 6a39f606..85f019ee 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -59,6 +59,7 @@ func main() { source.Assets() utils.InitLogs() args := flag.Args() + defer core.CloseDB() if len(args) >= 1 { err := CatchCLI(args) diff --git a/core/database.go b/core/database.go index 9b8045d1..27953727 100644 --- a/core/database.go +++ b/core/database.go @@ -78,6 +78,17 @@ type DbConfig struct { *types.DbConfig } +func (s *Service) HitsBetween(t1, t2 time.Time) *gorm.DB { + selector := Dbtimestamp(3600) + return DbSession.Debug().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 CloseDB() { + if DbSession != nil { + DbSession.DB().Close() + } +} + // Close shutsdown the database connection func (db *DbConfig) Close() error { return DbSession.DB().Close() @@ -104,7 +115,9 @@ func (u *User) AfterFind() (err error) { } func (u *Hit) BeforeCreate() (err error) { - u.CreatedAt = time.Now().UTC() + if u.CreatedAt.IsZero() { + u.CreatedAt = time.Now().UTC() + } return } diff --git a/core/sample.go b/core/sample.go index 64c373c6..5d6a234f 100644 --- a/core/sample.go +++ b/core/sample.go @@ -97,10 +97,10 @@ func InsertSampleHits() error { utils.Log(1, fmt.Sprintf("Adding %v sample hit records to service %v", 360, service.Name)) createdAt := since - for hi := int64(1); hi <= 860; hi++ { + for hi := int64(1); hi <= 1860; hi++ { rand.Seed(time.Now().UnixNano()) latency := rand.Float64() - createdAt = createdAt.Add(15 * time.Minute) + createdAt = createdAt.Add(3 * time.Minute).UTC() hit := &types.Hit{ Service: service.Id, CreatedAt: createdAt, diff --git a/core/services.go b/core/services.go index 653dcace..ed2c3a0c 100644 --- a/core/services.go +++ b/core/services.go @@ -122,8 +122,8 @@ func (s *Service) OnlineSince(ago time.Time) float32 { // DateScan struct is for creating the charts.js graph JSON array type DateScan struct { - CreatedAt time.Time `json:"x"` - Value int64 `json:"y"` + CreatedAt string `json:"x"` + Value int64 `json:"y"` } // DateScanObj struct is for creating the charts.js graph JSON array @@ -167,19 +167,45 @@ func (s *Service) DowntimeText() string { } // GroupDataBy returns a SQL query as a string to group a column by a time -func GroupDataBy(column string, id int64, start, end time.Time, increment string) string { +func GroupDataBy(column string, id int64, start, end time.Time, seconds int64) string { + incrementTime := "second" + if seconds == 60 { + incrementTime = "minute" + } else if seconds == 3600 { + incrementTime = "hour" + } var sql string switch CoreApp.DbConnection { case "mysql": sql = fmt.Sprintf("SELECT CONCAT(date_format(created_at, '%%Y-%%m-%%dT%%H:%%i:00Z')) AS created_at, AVG(latency)*1000 AS value FROM %v WHERE service=%v AND DATE_FORMAT(created_at, '%%Y-%%m-%%dT%%TZ') BETWEEN DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') AND DATE_FORMAT('%v', '%%Y-%%m-%%dT%%TZ') GROUP BY 1 ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) case "sqlite": - sql = fmt.Sprintf("SELECT strftime('%%Y-%%m-%%dT%%H:%%M:00Z', created_at), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY strftime('%%M:00', created_at) ORDER BY created_at ASC;", column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) + sql = fmt.Sprintf("SELECT datetime((strftime('%%s', created_at) / %v) * %v, 'unixepoch'), AVG(latency)*1000 as value FROM %v WHERE service=%v AND created_at BETWEEN '%v' AND '%v' GROUP BY 1 ORDER BY created_at ASC;", seconds, seconds, column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) case "postgres": - sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", increment, column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) + sql = fmt.Sprintf("SELECT date_trunc('%v', created_at), AVG(latency)*1000 AS value FROM %v WHERE service=%v AND created_at >= '%v' AND created_at <= '%v' GROUP BY 1 ORDER BY date_trunc ASC;", incrementTime, column, id, start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339)) } + fmt.Println(sql) return sql } +func Dbtimestamp(seconds int64) string { + incrementTime := "second" + if seconds == 60 { + incrementTime = "minute" + } else if seconds == 3600 { + incrementTime = "hour" + } + 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") + 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) + default: + return "" + } +} + // Downtime returns the amount of time of a offline service func (s *Service) Downtime() time.Duration { hits, _ := s.Hits() @@ -196,27 +222,21 @@ func (s *Service) Downtime() time.Duration { func GraphDataRaw(service types.ServiceInterface, start, end time.Time) *DateScanObj { var d []DateScan - s := service.Select() - sql := GroupDataBy("hits", s.Id, start, end, "minute") - rows, err := DbSession.Raw(sql).Rows() - if err != nil { - utils.Log(2, err) - return nil - } + //s := service.Select() + + model := service.(*Service).HitsBetween(start, end) + rows, _ := model.Rows() + + //sql := GroupDataBy("hits", s.Id, start, end, 3600) for rows.Next() { var gd DateScan - var tt string - var ff float64 - err := rows.Scan(&tt, &ff) - if err != nil { - utils.Log(2, fmt.Sprintf("Issue loading chart data for service %v, %v", s.Name, err)) - } - gd.CreatedAt, err = time.Parse(time.RFC3339, tt) - if err != nil { - utils.Log(2, fmt.Sprintf("Issue parsing time %v", err)) - } - gd.CreatedAt = utils.Timezoner(gd.CreatedAt, CoreApp.Timezone) - gd.Value = int64(ff) + var createdAt string + var value float64 + rows.Scan(&createdAt, &value) + + createdTime, _ := time.Parse(types.TIME, createdAt) + gd.CreatedAt = utils.Timezoner(createdTime, CoreApp.Timezone).Format(types.TIME) + gd.Value = int64(value * 1000) d = append(d, gd) } return &DateScanObj{d} @@ -233,7 +253,7 @@ 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(time.Hour*-24 + time.Minute*0 + time.Second*0) + start := time.Now().Add(-24 * time.Hour) end := time.Now() obj := GraphDataRaw(s, start, end) data, err := json.Marshal(obj) diff --git a/core/services_test.go b/core/services_test.go index 6a5773f5..f73a92fe 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -340,25 +340,3 @@ func TestDNScheckService(t *testing.T) { assert.Nil(t, err) assert.NotZero(t, amount) } - -func TestGroupGraphData(t *testing.T) { - service := SelectService(1) - CoreApp.DbConnection = "mysql" - lastWeek := time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out := GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") - t.Log(out) - assert.Contains(t, out, "SELECT CONCAT(date_format(created_at, '%Y-%m-%dT%H:%i:00Z'))") - - CoreApp.DbConnection = "postgres" - lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") - t.Log(out) - assert.Contains(t, out, "SELECT date_trunc('hour', created_at)") - - CoreApp.DbConnection = "sqlite" - lastWeek = time.Now().Add(time.Hour*-(24*7) + time.Minute*0 + time.Second*0) - out = GroupDataBy("services", service.Id, lastWeek, time.Now(), "hour") - t.Log(out) - assert.Contains(t, out, "SELECT strftime('%Y-%m-%dT%H:%M:00Z'") - -} diff --git a/handlers/dashboard.go b/handlers/dashboard.go index b6fb7d48..b3f932f3 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -16,9 +16,11 @@ package handlers import ( + "encoding/json" "fmt" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/source" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" ) @@ -94,3 +96,19 @@ func logsLineHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(lastLine.FormatForHtml())) } } + +type exportData struct { + Services []types.ServiceInterface +} + +func exportHandler(w http.ResponseWriter, r *http.Request) { + if !IsAuthenticated(r) { + w.WriteHeader(http.StatusInternalServerError) + return + } + + data := exportData{core.CoreApp.Services} + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +} diff --git a/handlers/routes.go b/handlers/routes.go index fb81ac85..c4f8933d 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -74,6 +74,7 @@ func Router() *mux.Router { r.Handle("/settings/build", http.HandlerFunc(saveAssetsHandler)).Methods("GET") r.Handle("/settings/delete_assets", http.HandlerFunc(deleteAssetsHandler)).Methods("GET") r.Handle("/settings/notifier/{method}", http.HandlerFunc(saveNotificationHandler)).Methods("POST") + r.Handle("/settings/export", http.HandlerFunc(exportHandler)).Methods("GET") r.Handle("/plugins/download/{name}", http.HandlerFunc(pluginsDownloadHandler)) r.Handle("/plugins/{name}/save", http.HandlerFunc(pluginSavedHandler)).Methods("POST") r.Handle("/help", http.HandlerFunc(helpHandler)) diff --git a/handlers/services.go b/handlers/services.go index 1270ab65..b30b7f10 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -22,6 +22,7 @@ import ( "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" + "github.com/jinzhu/now" "net/http" "strconv" "time" @@ -39,19 +40,21 @@ func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) { startField := fields.Get("start") endField := fields.Get("end") - var start time.Time - var end time.Time - if startField == "" { - start = time.Now().Add((-24 * 7) * time.Hour).UTC() - } else { - start = time.Unix(utils.StringInt(startField), 0).UTC() + + end := now.EndOfDay().UTC() + start := now.BeginningOfDay().UTC() + + if startField != "" { + start = time.Unix(utils.StringInt(startField), 0) + start = now.New(start).BeginningOfDay().UTC() } - if endField == "" { - end = time.Now().UTC() - } else { - end = time.Unix(utils.StringInt(endField), 0).UTC() + if endField != "" { + end = time.Unix(utils.StringInt(endField), 0) + end = now.New(end).EndOfDay().UTC() } + fmt.Println("start: ", start.String(), "end: ", end.String()) + service := core.SelectService(utils.StringInt(vars["id"])) data := core.GraphDataRaw(service, start, end).ToString() @@ -69,8 +72,9 @@ func renderServiceChartsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=60") var data []string - end := time.Now() - start := end.Add(-(24 * 7) * time.Hour) + end := now.EndOfDay().UTC() + start := now.BeginningOfDay().UTC() + for _, s := range services { d := core.GraphDataRaw(s, start, end).ToString() data = append(data, d) @@ -106,7 +110,6 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) decoder.Decode(&newOrder) for _, s := range newOrder { - fmt.Println("updating: ", s.Id, " to be order_id: ", s.Order) service := core.SelectService(s.Id) service.Order = s.Order service.Update(false) @@ -183,16 +186,24 @@ func servicesViewHandler(w http.ResponseWriter, r *http.Request) { return } - if startField == 0 || endField == 0 { - startField = time.Now().Add((-24 * 7) * time.Hour).UTC().Unix() - endField = time.Now().UTC().Unix() + end := time.Now() + start := end.Add((-24 * 7) * time.Hour) + + if startField != 0 { + start = time.Unix(startField, 0) } + if endField != 0 { + end = time.Unix(endField, 0) + } + + data := core.GraphDataRaw(serv, start, end) out := struct { Service *core.Service Start int64 End int64 - }{serv, startField, endField} + Data string + }{serv, start.Unix(), end.Unix(), data.ToString()} executeResponse(w, r, "service.html", out, nil) } diff --git a/source/tmpl/service.html b/source/tmpl/service.html index 4d5bdf59..f62c820c 100644 --- a/source/tmpl/service.html +++ b/source/tmpl/service.html @@ -57,7 +57,7 @@ -
+
@@ -255,7 +255,7 @@ data: { datasets: [{ label: 'Response Time (Milliseconds)', - data: {{js .GraphData}}, + data: {{js .Data}}, backgroundColor: [ 'rgba(47, 206, 30, 0.92)' ], @@ -275,14 +275,13 @@ beginAtZero: true }, gridLines: { - display:false + display: true } }], xAxes: [{ type: 'time', - distribution: 'series', gridLines: { - display:false + display:true } }] }, diff --git a/types/time.go b/types/time.go index 3693786f..51ddad16 100644 --- a/types/time.go +++ b/types/time.go @@ -19,6 +19,12 @@ import ( "time" ) +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" +) + var ( NOW = func() time.Time { return time.Now() }() HOUR_1_AGO = time.Now().Add(-1 * time.Hour)