pull/429/head
hunterlong 2020-02-21 20:40:05 -08:00
parent cb7787f26c
commit 05c77a5e98
10 changed files with 187 additions and 131 deletions

View File

@ -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)
}

View File

@ -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, ",") + "]"
}

51
dev/docker-compose-database.yml vendored Normal file
View File

@ -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

View File

@ -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
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']

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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"`