From 05c77a5e98ecc3a92849f6c2006bd6d58974f21b Mon Sep 17 00:00:00 2001 From: hunterlong Date: Fri, 21 Feb 2020 20:40:05 -0800 Subject: [PATCH] vue --- core/services.go | 26 +++--- core/sparklines.go | 30 ------- dev/docker-compose-database.yml | 51 +++++++++++ docker-compose.yml | 51 ++++++++++- frontend/src/API.js | 2 +- .../src/components/Service/ServiceInfo.vue | 2 +- handlers/routes.go | 4 +- handlers/services.go | 89 ++++--------------- types/gorm.go | 56 +++++++++--- types/types.go | 7 ++ 10 files changed, 187 insertions(+), 131 deletions(-) create mode 100644 dev/docker-compose-database.yml diff --git a/core/services.go b/core/services.go index d3f32fd7..a1ad8f81 100644 --- a/core/services.go +++ b/core/services.go @@ -272,10 +272,16 @@ type DateScanObj struct { } // GraphDataRaw will return all the hits between 2 times for a Service -func GraphHitsDataRaw(service types.ServiceInterface, start, end time.Time, group string, column string) *DateScanObj { - model := service.(*Service).HitsBetween(start, end, group, column) - model = model.Order("timeframe asc", false).Group("timeframe") - outgoing, err := model.ToChart() +func GraphHitsDataRaw(service types.ServiceInterface, query types.GroupQuery, column string) *DateScanObj { + srv := service.(*Service) + + dbQuery := Database(&types.Hit{}). + Where("service = ?", srv.Id). + Between(query.Start, query.End). + MultipleSelects(Database(&types.Hit{}).SelectByTime(query.Group), types.CountAmount()). + GroupByTimeframe() + + outgoing, err := dbQuery.ToChart() if err != nil { log.Error(err) } @@ -283,16 +289,16 @@ func GraphHitsDataRaw(service types.ServiceInterface, start, end time.Time, grou } // GraphDataRaw will return all the hits between 2 times for a Service -func GraphFailuresDataRaw(service types.ServiceInterface, start, end time.Time, group string) []*types.TimeValue { +func GraphFailuresDataRaw(service types.ServiceInterface, query types.GroupQuery) []*types.TimeValue { srv := service.(*Service) - query := Database(&types.Failure{}). + dbQuery := Database(&types.Failure{}). Where("service = ?", srv.Id). - Between(start, end). - MultipleSelects(types.SelectByTime(group), types.CountAmount()). - GroupByTimeframe().Debug() + Between(query.Start, query.End). + MultipleSelects(Database(&types.Failure{}).SelectByTime(query.Group), types.CountAmount()). + GroupByTimeframe() - outgoing, err := query.ToTimeValue(start, end) + outgoing, err := dbQuery.ToTimeValue(query.Start, query.End) if err != nil { log.Error(err) } diff --git a/core/sparklines.go b/core/sparklines.go index 163feb63..9a8bc959 100644 --- a/core/sparklines.go +++ b/core/sparklines.go @@ -1,31 +1 @@ package core - -import ( - "github.com/hunterlong/statping/utils" - "strings" - "time" -) - -// SparklineDayFailures returns a string array of daily service failures -func (s *Service) SparklineDayFailures(days int) string { - var arr []string - ago := time.Now().UTC().Add((time.Duration(days) * -24) * time.Hour) - for day := 1; day <= days; day++ { - ago = ago.Add(24 * time.Hour) - failures, _ := s.TotalFailuresOnDate(ago) - arr = append(arr, utils.ToString(failures)) - } - return "[" + strings.Join(arr, ",") + "]" -} - -// SparklineHourResponse returns a string array for the average response or ping time for a service -func (s *Service) SparklineHourResponse(hours int, method string) string { - var arr []string - end := time.Now().UTC() - start := end.Add(time.Duration(-hours) * time.Hour) - obj := GraphHitsDataRaw(s, start, end, "hour", method) - for _, v := range obj.Array { - arr = append(arr, utils.ToString(v.Value)) - } - return "[" + strings.Join(arr, ",") + "]" -} diff --git a/dev/docker-compose-database.yml b/dev/docker-compose-database.yml new file mode 100644 index 00000000..93d3b4c7 --- /dev/null +++ b/dev/docker-compose-database.yml @@ -0,0 +1,51 @@ +version: '3.1' + +volumes: + mysql_data: {} + postgres_data: {} + +services: + + mysql: + image: mysql:5.7 + volumes: + - mysql_data:/var/lib/mysql + restart: always + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: password123 + MYSQL_DATABASE: statping + MYSQL_USER: root + MYSQL_PASSWORD: password + + postgres: + container_name: postgres + image: postgres:10.0-alpine + ports: + - 5432:5432 + volumes: + - postgres_data:/var/lib/postgresql/data/pg_data + environment: + POSTGRES_PASSWORD: password123 + POSTGRES_DB: statping + POSTGRES_USER: root + POSTGRES_PORT: 5432 + PGDATA: /var/lib/postgresql/data/pg_data + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: phpmyadmin + environment: + - PMA_HOST=mysql + - PMA_USER=root + - PMA_PASSWORD=password123 + restart: always + depends_on: + - mysql + links: + - mysql + ports: + - 7474:80 + volumes: + - /sessions diff --git a/docker-compose.yml b/docker-compose.yml index 6d0666f1..6c7de73a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,52 @@ statping: container_name: statping image: hunterlong/statping restart: always - ports: - - 8080:8080 volumes: - - ./statping:/app + - statping_data:/app environment: - DB_CONN: sqlite \ No newline at end of file + DB_CONN: sqlite + LETSENCRYPT_HOST: demo.statping.com + VIRTUAL_HOST: demo.statping.com + VIRTUAL_PORT: 8080 + +prometheus: + container_name: prometheus + image: prom/prometheus:v2.0.0 + restart: always + ports: + - 9090:9090 + volumes: + - prometheus_config_data:/etc/prometheus/ + - prometheus_data:/prometheus + links: + - statping + depends_on: + - statping + +grafana: + container_name: grafana + image: grafana/grafana + restart: always + ports: + - 3000:3000 + volumes: + - grafana_data:/var/lib/grafana + environment: + - GF_SECURITY_ADMIN_PASSWORD=password123 + - GF_USERS_ALLOW_SIGN_UP=false + depends_on: + - prometheus + links: + - prometheus + + +global: + scrape_interval: 30s + evaluation_interval: 30s + +scrape_configs: + - job_name: 'statping' + scrape_interval: 30s + bearer_token: 'SECRET API KEY HERE' + static_configs: + - targets: ['statping:8080'] diff --git a/frontend/src/API.js b/frontend/src/API.js index 45124634..6ace37f5 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -37,7 +37,7 @@ class Api { } async service_hits(id, start, end, group) { - return axios.get('/api/services/' + id + '/data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data)) + return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data)) } async service_failures_data(id, start, end, group) { diff --git a/frontend/src/components/Service/ServiceInfo.vue b/frontend/src/components/Service/ServiceInfo.vue index 1fc30157..a0321ae0 100644 --- a/frontend/src/components/Service/ServiceInfo.vue +++ b/frontend/src/components/Service/ServiceInfo.vue @@ -49,7 +49,7 @@ } }, async mounted() { - this.set1 = await this.getHits(24 * 2, "hour") + this.set1 = await this.getHits(24, "minute") this.set1_name = this.calc(this.set1) this.set2 = await this.getHits(24 * 7, "hour") this.set2_name = this.calc(this.set2) diff --git a/handlers/routes.go b/handlers/routes.go index 29937e9f..4f3709b6 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -126,9 +126,9 @@ func Router() *mux.Router { api.Handle("/api/services/{id}/hits", scoped(apiServiceHitsHandler)).Methods("GET") // API SERVICE CHART DATA Routes - api.Handle("/api/services/{id}/data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET") + api.Handle("/api/services/{id}/hits_data", cached("30s", "application/json", apiServiceDataHandler)).Methods("GET") api.Handle("/api/services/{id}/failure_data", cached("30s", "application/json", apiServiceFailureDataHandler)).Methods("GET") - api.Handle("/api/services/{id}/ping", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET") + api.Handle("/api/services/{id}/ping_data", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET") api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET") // API INCIDENTS Routes diff --git a/handlers/services.go b/handlers/services.go index 9290a71e..3256e9b8 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -24,7 +24,6 @@ import ( "github.com/hunterlong/statping/types" "github.com/hunterlong/statping/utils" "net/http" - "strconv" "time" ) @@ -73,54 +72,6 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) { returnJson(newOrder, w, r) } -func servicesViewHandler(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fields := parseGet(r) - r.ParseForm() - - var serv *core.Service - id := vars["id"] - if _, err := strconv.Atoi(id); err == nil { - serv = core.SelectService(utils.ToInt(id)) - } else { - serv = core.SelectServiceLink(id) - } - if serv == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - startField := utils.ToInt(fields.Get("start")) - endField := utils.ToInt(fields.Get("end")) - group := r.Form.Get("group") - - end := utils.Now().UTC() - start := end.Add((-24 * 7) * time.Hour).UTC() - - if startField != 0 { - start = time.Unix(startField, 0).UTC() - } - if endField != 0 { - end = time.Unix(endField, 0).UTC() - } - if group == "" { - group = "hour" - } - - data := core.GraphHitsDataRaw(serv, start, end, group, "latency") - - out := struct { - Service *core.Service - Start string - End string - StartUnix int64 - EndUnix int64 - Data string - }{serv, start.Format(utils.FlatpickrReadable), end.Format(utils.FlatpickrReadable), start.Unix(), end.Unix(), data.ToString()} - - ExecuteResponse(w, r, "service.gohtml", out, nil) -} - func apiServiceHandler(r *http.Request) interface{} { vars := mux.Vars(r) servicer := core.SelectService(utils.ToInt(vars["id"])) @@ -205,18 +156,9 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(errors.New("service data not found"), w, r) return } - fields := parseGet(r) - grouping := fields.Get("group") - if grouping == "" { - grouping = "hour" - } - startField := utils.ToInt(fields.Get("start")) - endField := utils.ToInt(fields.Get("end")) + groupQuery := parseGroupQuery(r) - start := time.Unix(startField, 0) - end := time.Unix(endField, 0) - - obj := core.GraphHitsDataRaw(service, start, end, grouping, "latency") + obj := core.GraphHitsDataRaw(service, groupQuery, "latency") returnJson(obj, w, r) } @@ -227,6 +169,13 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(errors.New("service data not found"), w, r) return } + groupQuery := parseGroupQuery(r) + + obj := core.GraphFailuresDataRaw(service, groupQuery) + returnJson(obj, w, r) +} + +func parseGroupQuery(r *http.Request) types.GroupQuery { fields := parseGet(r) grouping := fields.Get("group") if grouping == "" { @@ -235,11 +184,11 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) { startField := utils.ToInt(fields.Get("start")) endField := utils.ToInt(fields.Get("end")) - start := time.Unix(startField, 0).UTC() - end := time.Unix(endField, 0).UTC() - - obj := core.GraphFailuresDataRaw(service, start, end, grouping) - returnJson(obj, w, r) + return types.GroupQuery{ + Start: time.Unix(startField, 0).UTC(), + End: time.Unix(endField, 0).UTC(), + Group: grouping, + } } func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { @@ -249,15 +198,9 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) { sendErrorJson(errors.New("service not found"), w, r) return } - fields := parseGet(r) - grouping := fields.Get("group") - startField := utils.ToInt(fields.Get("start")) - endField := utils.ToInt(fields.Get("end")) + groupQuery := parseGroupQuery(r) - start := time.Unix(startField, 0) - end := time.Unix(endField, 0) - - obj := core.GraphHitsDataRaw(service, start, end, grouping, "ping_time") + obj := core.GraphHitsDataRaw(service, groupQuery, "ping_time") returnJson(obj, w, r) } diff --git a/types/gorm.go b/types/gorm.go index 2e298568..38b752ff 100644 --- a/types/gorm.go +++ b/types/gorm.go @@ -19,6 +19,9 @@ import ( "database/sql" "fmt" "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/mysql" + _ "github.com/jinzhu/gorm/dialects/postgres" + _ "github.com/jinzhu/gorm/dialects/sqlite" "net/http" "strconv" "strings" @@ -112,7 +115,7 @@ type Database interface { GroupByTimeframe() Database ToTimeValue(time.Time, time.Time) ([]*TimeValue, error) - + SelectByTime(string) string MultipleSelects(args ...string) Database Failurer @@ -123,7 +126,26 @@ type Failurer interface { Fails() ([]*Failure, error) } -func sqlTimeframes(increment string) string { +func mysqlTimestamps(increment string) string { + switch increment { + case "second": + return "%Y-%m-%d %H:%i:%S" + case "minute": + return "%Y-%m-%d %H:%i:00" + case "hour": + return "%Y-%m-%d %H:00:00" + case "day": + return "%Y-%m-%d 00:00:00" + case "month": + return "%Y-%m 00:00:00" + case "year": + return "%Y" + default: + return "%Y-%m-%d 00:00:00" + } +} + +func sqliteTimestamps(increment string) string { switch increment { case "second": return "%Y-%m-%d %H:%M:%S" @@ -151,8 +173,15 @@ func CountAmount() string { return fmt.Sprintf("COUNT(id) as amount") } -func SelectByTime(increment string) string { - return fmt.Sprintf("strftime('%s', created_at, 'utc') as timeframe", sqlTimeframes(increment)) +func (it *Db) SelectByTime(increment string) string { + switch it.Type { + case "mysql": + return fmt.Sprintf("CONCAT(date_format(created_at, '%s')) AS timeframe", mysqlTimestamps(increment)) + case "postgres": + return fmt.Sprintf("date_trunc('%s', created_at) AS timeframe", increment) + default: + return fmt.Sprintf("strftime('%s', created_at, 'utc') as timeframe", sqliteTimestamps(increment)) + } } func (it *Db) GroupByTimeframe() Database { @@ -528,11 +557,18 @@ func (it *Db) ToTimeValue(start, end time.Time) ([]*TimeValue, error) { Amount: amount, }) } - return fillMissing(data, start, end), nil + return it.fillMissing(data, start, end), nil } -func parseTime(t time.Time) string { - return t.Format("2006-01-02T00:00:00Z") +func (it *Db) FormatTime(t time.Time) string { + switch it.Type { + case "mysql": + return t.UTC().Format("2006-01-02T00:00:00Z") + case "postgres": + return t.UTC().Format("2006-01-02T00:00:00Z") + default: + return t.UTC().Format("2006-01-02T00:00:00Z") + } } func reparseTime(t string) time.Time { @@ -540,19 +576,19 @@ func reparseTime(t string) time.Time { return re.UTC() } -func fillMissing(vals []*TimeValue, start, end time.Time) []*TimeValue { +func (it *Db) fillMissing(vals []*TimeValue, start, end time.Time) []*TimeValue { timeMap := make(map[string]*TimeValue) var validSet []*TimeValue for _, v := range vals { - timeMap[parseTime(v.Timeframe)] = v + timeMap[it.FormatTime(v.Timeframe)] = v } current := start.UTC() maxTime := end for { amount := int64(0) - currentStr := parseTime(current) + currentStr := it.FormatTime(current) if timeMap[currentStr] != nil { amount = timeMap[currentStr].Amount } diff --git a/types/types.go b/types/types.go index 0b21f774..86f3caf4 100644 --- a/types/types.go +++ b/types/types.go @@ -19,6 +19,13 @@ import ( "time" ) +type GroupQuery struct { + Id int64 + Start time.Time + End time.Time + Group string +} + // Hit struct is a 'successful' ping or web response entry for a service. type Hit struct { Id int64 `gorm:"primary_key;column:id" json:"id"`