pull/429/head
hunterlong 2020-02-23 21:53:15 -08:00
parent 871424f9c3
commit f64fc9e682
25 changed files with 433 additions and 395 deletions

View File

@ -20,7 +20,6 @@ import (
"github.com/ararog/timeago"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sort"
"time"
)
@ -94,8 +93,8 @@ func (c *Checkin) CreateFailure() (int64, error) {
PingTime: c.Expected().Seconds(),
}}
row := Database(&Failure{}).Create(&fail)
sort.Sort(types.FailSort(c.Failures))
c.Failures = append(c.Failures, fail)
//sort.Sort(types.FailSort(c.Failures))
//c.Failures = append(c.Failures, fail)
if len(c.Failures) > limitedFailures {
c.Failures = c.Failures[1:]
}
@ -167,23 +166,27 @@ func (c *Checkin) AllHits() []*types.CheckinHit {
return checkins
}
// Hits returns all of the CheckinHits for a given Checkin
func (c *Checkin) LimitedFailures(amount int) []types.FailureInterface {
var failures []*Failure
var failInterfaces []types.FailureInterface
col := Database(&types.Failure{}).Where("checkin = ?", c.Id).Where("method = 'checkin'").Limit(amount).Order("id desc")
col.Find(&failures)
for _, f := range failures {
failInterfaces = append(failInterfaces, f)
}
return failInterfaces
}
// Hits returns all of the CheckinHits for a given Checkin
func (c *Checkin) AllFailures() []*types.Failure {
var failures []*types.Failure
col := Database(&types.Failure{}).Where("checkin = ?", c.Id).Where("method = 'checkin'").Order("id desc")
col.Find(&failures)
Database(&types.Failure{}).
Where("checkin = ?", c.Id).
Where("method = 'checkin'").
Order("id desc").
Find(&failures)
return failures
}
func (c *Checkin) GetFailures(count int) []*types.Failure {
var failures []*types.Failure
Database(&types.Failure{}).
Where("checkin = ?", c.Id).
Where("method = 'checkin'").
Limit(count).
Order("id desc").
Find(&failures)
return failures
}

View File

@ -18,9 +18,7 @@ package core
import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"net/http"
"sort"
"strings"
"time"
@ -62,10 +60,6 @@ func (s *Service) AllFailures() []types.Failure {
return fails
}
func (s *Service) FailuresDb(r *http.Request) database.Database {
return Database(&types.Failure{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
}
// DeleteFailures will delete all failures for a service
func (s *Service) DeleteFailures() {
err := DbSession.Exec(`DELETE FROM failures WHERE service = ?`, s.Id)
@ -75,13 +69,6 @@ func (s *Service) DeleteFailures() {
s.Failures = nil
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedFailures(amount int) []*Failure {
var failArr []*Failure
Database(&types.Failure{}).Where("service = ?", s.Id).Not("method = 'checkin'").Order("id desc").Limit(amount).Find(&failArr)
return failArr
}
// LimitedFailures will return the last amount of failures from a service
func (s *Service) LimitedCheckinFailures(amount int) []*Failure {
var failArr []*Failure

View File

@ -47,13 +47,13 @@ func (s *Service) CountHits() (int64, error) {
// Hits returns all successful hits for a service
func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) {
var hits []*types.Hit
col := Database(&types.Hit{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
col := Database(&types.Hit{}).Where("service = ?", s.Id).Requests(r)
err := col.Find(&hits)
return hits, err.Error()
}
func (s *Service) HitsDb(r *http.Request) database.Database {
return Database(&types.Hit{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
return Database(&types.Hit{}).Where("service = ?", s.Id).Requests(r).Order("id desc")
}
// Hits returns all successful hits for a service

View File

@ -16,9 +16,7 @@
package core
import (
"encoding/json"
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
@ -58,6 +56,13 @@ func SelectService(id int64) *Service {
return nil
}
func (s *Service) GetFailures(count int) []*Failure {
var fails []*Failure
db := Database(&types.Failure{}).Where("service = ?", s.Id)
db.Limit(count).Find(&fails)
return fails
}
func (s *Service) UpdateStats() *Service {
s.Online24Hours = s.OnlineDaysPercent(1)
s.Online7Days = s.OnlineDaysPercent(7)
@ -108,7 +113,8 @@ func (s *Service) AllCheckins() []*Checkin {
return checkin
}
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services, should only be called once on startup.
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services
// should only be called once on startup.
func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
var services []*Service
db := Database(&Service{}).Find(&services).Order("order_id desc")
@ -122,13 +128,13 @@ func (c *Core) SelectAllServices(start bool) ([]*Service, error) {
service.Start()
service.CheckinProcess()
}
fails := service.LimitedFailures(limitedFailures)
fails := service.GetFailures(limitedFailures)
for _, f := range fails {
service.Failures = append(service.Failures, f)
}
checkins := service.AllCheckins()
for _, c := range checkins {
c.Failures = c.LimitedFailures(limitedFailures)
c.Failures = c.GetFailures(limitedFailures)
c.Hits = c.LimitedHits(limitedHits)
service.Checkins = append(service.Checkins, c)
}
@ -185,8 +191,8 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
}
// lastFailure returns the last Failure a service had
func (s *Service) lastFailure() *Failure {
limited := s.LimitedFailures(1)
func (s *Service) lastFailure() types.FailureInterface {
limited := s.GetFailures(1)
if len(limited) == 0 {
return nil
}
@ -194,29 +200,6 @@ func (s *Service) lastFailure() *Failure {
return last
}
// SmallText returns a short description about a services status
// service.SmallText()
// // Online since Monday 3:04:05PM, Jan _2 2006
func (s *Service) SmallText() string {
last := s.LimitedFailures(1)
//hits, _ := s.LimitedHits(1)
zone := CoreApp.Timezone
if s.Online {
if len(last) == 0 {
return fmt.Sprintf("Online since %v", utils.Timezoner(s.CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
} else {
return fmt.Sprintf("Online, last Failure was %v", utils.Timezoner(last[0].CreatedAt, zone).Format("Monday 3:04:05PM, Jan _2 2006"))
}
}
if len(last) > 0 {
lastFailure := s.lastFailure()
got, _ := timeago.TimeAgoWithTime(time.Now().UTC().Add(s.Downtime()), time.Now().UTC())
return fmt.Sprintf("Reported offline %v, %v", got, lastFailure.ParseError())
} else {
return fmt.Sprintf("%v is currently offline", s.Name)
}
}
// DowntimeText will return the amount of downtime for a service based on the duration
// service.DowntimeText()
// // Service has been offline for 15 minutes
@ -261,99 +244,24 @@ func (s *Service) Downtime() time.Duration {
return time.Duration(0)
}
if len(hits) == 0 {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
return time.Now().UTC().Sub(fail.Select().CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
since := fail.Select().CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
}
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []*database.DateScan `json:"data"`
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphHitsDataRaw(service types.ServiceInterface, query *types.GroupQuery, column string) []*database.TimeValue {
srv := service.(*Service)
dbQuery, err := Database(&types.Hit{}).
Where("service = ?", srv.Id).
GroupQuery(query).ToTimeValue()
// GraphData will return all hits or failures
func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*database.TimeValue {
dbQuery, err := q.Database().GroupQuery(q, by).ToTimeValue(dbType)
if err != nil {
log.Error(err)
return nil
}
return dbQuery.FillMissing()
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphFailuresDataRaw(service types.ServiceInterface, query *types.GroupQuery) []*database.TimeValue {
srv := service.(*Service)
dbQuery, err := Database(&types.Failure{}).
Where("service = ?", srv.Id).
GroupQuery(query).ToTimeValue()
if err != nil {
log.Error(err)
return nil
if q.FillEmpty {
return dbQuery.FillMissing(q.Start, q.End)
}
return dbQuery.FillMissing()
}
// ToString will convert the DateScanObj into a JSON string for the charts to render
func (d *DateScanObj) ToString() string {
data, err := json.Marshal(d.Array)
if err != nil {
log.Warnln(err)
return "{}"
}
return string(data)
}
// AvgUptime24 returns a service's average online status for last 24 hours
func (s *Service) AvgUptime24() string {
ago := time.Now().UTC().Add(-24 * time.Hour)
return s.AvgUptime(ago)
}
// AvgUptime returns average online status for last 24 hours
func (s *Service) AvgUptime(ago time.Time) string {
failed, _ := s.TotalFailuresSince(ago)
if failed == 0 {
return "100"
}
total, _ := s.TotalHitsSince(ago)
if total == 0 {
return "0.00"
}
percent := float64(failed) / float64(total) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
}
// TotalUptime returns the total uptime percent of a service
func (s *Service) TotalUptime() string {
hits, _ := s.TotalHits()
failures, _ := s.TotalFailures()
percent := float64(failures) / float64(hits) * 100
percent = 100 - percent
if percent < 0 {
percent = 0
}
amount := fmt.Sprintf("%0.2f", percent)
if amount == "100.00" {
amount = "100"
}
return amount
return dbQuery.ToValues()
}
// index returns a services index int for updating the []*core.Services slice

View File

@ -127,12 +127,6 @@ func TestServiceOnline24Hours(t *testing.T) {
assert.True(t, service3.OnlineSince(since) > float32(49))
}
func TestServiceSmallText(t *testing.T) {
service := SelectService(5)
text := service.SmallText()
assert.Contains(t, text, "Online since")
}
func TestServiceAvgUptime(t *testing.T) {
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)

View File

@ -2,14 +2,12 @@ package database
import (
"database/sql"
"fmt"
"github.com/hunterlong/statping/types"
"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"
"time"
)
@ -21,6 +19,10 @@ const (
TIME_DAY = "2006-01-02"
)
var (
database Database
)
// Database is an interface which DB implements
type Database interface {
Close() error
@ -99,11 +101,9 @@ type Database interface {
// extra
Error() error
RowsAffected() int64
QuerySearch(*http.Request) Database
Since(time.Time) Database
Between(time.Time, time.Time) Database
Hits() ([]*types.Hit, error)
ToChart() ([]*DateScan, error)
SelectByTime(string) string
@ -112,7 +112,14 @@ type Database interface {
FormatTime(t time.Time) string
ParseTime(t string) (time.Time, error)
GroupQuery(query *types.GroupQuery) GroupByer
Requests(*http.Request) Database
GroupQuery(query *GroupQuery, by By) GroupByer
}
func (it *Db) Requests(r *http.Request) Database {
g := ParseQueries(r, it)
return g.db
}
func (it *Db) MultipleSelects(args ...string) Database {
@ -120,10 +127,6 @@ func (it *Db) MultipleSelects(args ...string) Database {
return it.Select(joined)
}
func CountAmount() string {
return fmt.Sprintf("COUNT(id) as amount")
}
type Db struct {
Database *gorm.DB
Type string
@ -132,7 +135,11 @@ type Db struct {
// Openw is a drop-in replacement for Open()
func Openw(dialect string, args ...interface{}) (db Database, err error) {
gormdb, err := gorm.Open(dialect, args...)
return Wrap(gormdb), err
if err != nil {
return nil, err
}
database = Wrap(gormdb)
return database, err
}
// Wrap wraps gorm.DB in an interface
@ -460,8 +467,8 @@ type DateScan struct {
}
type TimeValue struct {
Timeframe time.Time `json:"timeframe"`
Amount int64 `json:"amount"`
Timeframe string `json:"timeframe"`
Amount float64 `json:"amount"`
}
func (it *Db) ToChart() ([]*DateScan, error) {
@ -487,55 +494,3 @@ func (it *Db) ToChart() ([]*DateScan, error) {
}
return data, err
}
func (it *Db) QuerySearch(r *http.Request) Database {
if r == nil {
return it
}
db := it.Database
start := defaultField(r, "start")
end := defaultField(r, "end")
limit := defaultField(r, "limit")
offset := defaultField(r, "offset")
params := &Params{
Start: start,
End: end,
Limit: limit,
Offset: offset,
}
if params.Start != nil && params.End != nil {
db = db.Where("created_at BETWEEN ? AND ?", time.Unix(*params.Start, 0).Format(TIME), time.Unix(*params.End, 0).UTC().Format(TIME))
} else if params.Start != nil && params.End == nil {
db = db.Where("created_at > ?", time.Unix(*params.Start, 0).UTC().Format(TIME))
}
if params.Limit != nil {
db = db.Limit(*params.Limit)
} else {
db = db.Limit(10000)
}
if params.Offset != nil {
db = db.Offset(*params.Offset)
} else {
db = db.Offset(0)
}
return Wrap(db)
}
type Params struct {
Start *int64
End *int64
Limit *int64
Offset *int64
}
func defaultField(r *http.Request, key string) *int64 {
r.ParseForm()
val := r.Form.Get(key)
if val == "" {
return nil
}
gg, _ := strconv.Atoi(val)
num := int64(gg)
return &num
}

View File

@ -3,42 +3,60 @@ package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
"net/url"
"strconv"
"time"
)
type GroupBy struct {
db Database
query *types.GroupQuery
query *GroupQuery
}
type GroupByer interface {
ToTimeValue() (*TimeVar, error)
ToTimeValue(interface{}) (*TimeVar, error)
}
type GroupMethod interface {
type By string
func (b By) String() string {
return string(b)
}
type GroupQuery struct {
db Database
Start time.Time
End time.Time
Group string
Order string
Limit int
Offset int
FillEmpty bool
}
func (b GroupQuery) Database() Database {
return b.db
}
var (
ByCount = func() GroupMethod {
return fmt.Sprintf("COUNT(id) as amount")
ByCount = By("COUNT(id) as amount")
BySum = func(column string) By {
return By(fmt.Sprintf("SUM(%s) as amount", column))
}
BySum = func(column string) GroupMethod {
return fmt.Sprintf("SUM(%s) as amount", column)
}
ByAverage = func(column string) GroupMethod {
return fmt.Sprintf("SUM(%s) as amount", column)
ByAverage = func(column string) By {
return By(fmt.Sprintf("AVG(%s) as amount", column))
}
)
func execute(db Database, query *types.GroupQuery) Database {
return db.MultipleSelects(
db.SelectByTime(query.Group),
CountAmount(),
).Between(query.Start, query.End).Group("timeframe").Debug()
}
func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer {
dbQuery := db.MultipleSelects(
db.SelectByTime(q.Group),
by.String(),
).Group("timeframe")
func (db *Db) GroupQuery(query *types.GroupQuery) GroupByer {
return &GroupBy{execute(db, query), query}
return &GroupBy{dbQuery, q}
}
type TimeVar struct {
@ -46,64 +64,72 @@ type TimeVar struct {
data []*TimeValue
}
func (g *GroupBy) ToTimeValue() (*TimeVar, error) {
func (t *TimeVar) ToValues() []*TimeValue {
return t.data
}
func (g *GroupBy) toFloatRows() []*TimeValue {
rows, err := g.db.Rows()
if err != nil {
return nil
}
var data []*TimeValue
for rows.Next() {
var timeframe time.Time
amount := float64(0)
rows.Scan(&timeframe, &amount)
newTs := types.FixedTime(timeframe, g.duration())
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
})
}
return data
}
func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) {
rows, err := g.db.Rows()
if err != nil {
return nil, err
}
var data []*TimeValue
for rows.Next() {
var timeframe string
var amount int64
if err := rows.Scan(&timeframe, &amount); err != nil {
return nil, err
}
createdTime, _ := g.db.ParseTime(timeframe)
var timeframe time.Time
amount := float64(0)
rows.Scan(&timeframe, &amount)
newTs := types.FixedTime(timeframe, g.duration())
data = append(data, &TimeValue{
Timeframe: createdTime,
Timeframe: newTs,
Amount: amount,
})
}
return &TimeVar{g, data}, nil
}
func (t *TimeVar) Values() []*TimeValue {
func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue {
timeMap := make(map[string]float64)
var validSet []*TimeValue
dur := t.g.duration()
for _, v := range t.data {
validSet = append(validSet, &TimeValue{
Timeframe: v.Timeframe,
Amount: v.Amount,
})
timeMap[v.Timeframe] = v.Amount
}
return validSet
}
currentStr := types.FixedTime(current, t.g.duration())
func (t *TimeVar) FillMissing() []*TimeValue {
timeMap := make(map[time.Time]*TimeValue)
var validSet []*TimeValue
if len(t.data) == 0 {
return nil
}
current := t.data[0].Timeframe
for _, v := range t.data {
timeMap[v.Timeframe] = v
}
maxTime := t.g.query.End
for {
amount := int64(0)
if timeMap[current] != nil {
amount = timeMap[current].Amount
var amount float64
if timeMap[currentStr] != 0 {
amount = timeMap[currentStr]
}
validSet = append(validSet, &TimeValue{
Timeframe: current,
Timeframe: currentStr,
Amount: amount,
})
if current.After(maxTime) {
if current.After(end) {
break
}
current = current.Add(t.g.duration())
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.duration())
}
return validSet
@ -112,18 +138,78 @@ func (t *TimeVar) FillMissing() []*TimeValue {
func (g *GroupBy) duration() time.Duration {
switch g.query.Group {
case "second":
return time.Second
return types.Second
case "minute":
return time.Minute
return types.Minute
case "hour":
return time.Hour
return types.Hour
case "day":
return time.Hour * 24
return types.Day
case "month":
return time.Hour * 730
return types.Month
case "year":
return time.Hour * 8760
return types.Year
default:
return time.Hour
return types.Hour
}
}
func ParseQueries(r *http.Request, db Database) *GroupQuery {
fields := parseGet(r)
grouping := fields.Get("group")
if grouping == "" {
grouping = "hour"
}
startField := utils.ToInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end"))
limit := utils.ToInt(fields.Get("limit"))
offset := utils.ToInt(fields.Get("offset"))
fill, _ := strconv.ParseBool(fields.Get("fill"))
orderBy := fields.Get("order")
if limit == 0 {
limit = 10000
}
query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(),
End: time.Unix(endField, 0).UTC(),
Group: grouping,
Order: orderBy,
Limit: int(limit),
Offset: int(offset),
FillEmpty: fill,
}
if query.Limit != 0 {
db = db.Limit(query.Limit)
}
if query.Offset > 0 {
db = db.Offset(query.Offset)
}
if !query.Start.IsZero() && !query.End.IsZero() {
db = db.Where("created_at BETWEEN ? AND ?", db.FormatTime(query.Start), db.FormatTime(query.End))
} else {
if !query.Start.IsZero() {
db = db.Where("created_at > ?", db.FormatTime(query.Start))
}
if !query.End.IsZero() {
db = db.Where("created_at < ?", db.FormatTime(query.End))
}
}
if query.Order != "" {
db = db.Order(query.Order)
}
query.db = db.Debug()
return query
}
func parseForm(r *http.Request) url.Values {
r.ParseForm()
return r.PostForm
}
func parseGet(r *http.Request) url.Values {
r.ParseForm()
return r.Form
}

View File

@ -2,7 +2,7 @@ package database
import "github.com/hunterlong/statping/types"
type Service struct {
type ServiceObj struct {
db Database
service *types.Service
}
@ -12,16 +12,16 @@ type Servicer interface {
Hits() Database
}
func (it *Db) GetService(id int64) (Servicer, error) {
func Service(id int64) (Servicer, error) {
var service types.Service
query := it.Model(&types.Service{}).Where("id = ?", id).Find(&service)
return &Service{it, &service}, query.Error()
query := database.Model(&types.Service{}).Where("id = ?", id).Find(&service)
return &ServiceObj{query, &service}, query.Error()
}
func (s *Service) Failures() Database {
return s.db.Model(&types.Failure{}).Where("service = ?", s.service.Id)
func (s *ServiceObj) Failures() Database {
return database.Model(&types.Failure{}).Where("service = ?", s.service.Id)
}
func (s *Service) Hits() Database {
return s.db.Model(&types.Hit{}).Where("service = ?", s.service.Id)
func (s *ServiceObj) Hits() Database {
return database.Model(&types.Hit{}).Where("service = ?", s.service.Id)
}

View File

@ -37,11 +37,11 @@ class Api {
}
async service_hits(id, start, end, group) {
return axios.get('/api/services/' + id + '/hits_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 + '&fill=true').then(response => (response.data))
}
async service_failures_data(id, start, end, group) {
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group).then(response => (response.data))
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=true').then(response => (response.data))
}
async service_heatmap(id, start, end, group) {

View File

@ -132,15 +132,15 @@
},
methods: {
async chartHits(group) {
const start = this.nowSubtract((3600 * 24) * 7)
const start = this.nowSubtract((3600 * 24) * 30)
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group)
if (this.data.length === 0 && group !== "minute") {
await this.chartHits("minute")
if (this.data.length === 0 && group !== "hour") {
await this.chartHits("hour")
}
this.series = [{
name: this.service.name,
...this.data
...this.convertToChartData(this.data)
}]
this.ready = true
}

View File

@ -16,7 +16,7 @@
</div>
</div>
<span v-for="(failure, index) in failures" v-bind:key="index" class="alert alert-light">
Failed {{ago(parseTime(failure.created_at))}}<br>
Failed {{failure.created_at}}<br>
{{failure.issue}}
</span>
@ -49,9 +49,9 @@
}
},
async mounted() {
this.set1 = await this.getHits(24, "minute")
this.set1 = await this.getHits(24, "hour")
this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24 * 7, "hour")
this.set2 = await this.getHits(24 * 7, "day")
this.set2_name = this.calc(this.set2)
this.loaded = true
},
@ -62,15 +62,16 @@
this.failures = await Api.service_failures(this.service.id, this.toUnix(start), this.toUnix(this.now()), 5)
return [{name: "None", data: []}]
}
const data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group)
const fetched = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(this.now()), group)
if (!data) {
return [{name: "None", data: []}]
}
return [{name: "Latency", data: data.data}]
const data = this.convertToChartData(fetched, 1000, true)
return [{name: "Latency", data}]
},
calc(s) {
let data = s[0].data
if (data.length > 1) {
if (data) {
let total = 0
data.forEach((f) => {
total += f.y

View File

@ -149,7 +149,7 @@
</div>
<div class="form-group row">
<div class="col-12">
<button @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
<button :disabled="loading" @click.prevent="saveService" type="submit" class="btn btn-success btn-block">
{{service.id ? "Update Service" : "Create Service"}}
</button>
</div>
@ -208,6 +208,7 @@
methods: {
async saveService() {
let s = this.service
this.loading = true
delete s.failures
delete s.created_at
delete s.updated_at
@ -219,6 +220,10 @@
} else {
await this.createService(s)
}
const services = await Api.services()
this.$store.commit('setServices', services)
this.loading = false
this.$router.push('/dashboard/services')
},
async createService(s) {
await Api.service_create(s)

View File

@ -93,5 +93,19 @@ export default Vue.mixin({
return "bars"
}
},
convertToChartData(data = [], multiplier=1, asInt=false) {
let newSet = [];
data.forEach((f) => {
let amount = f.amount * multiplier;
if (asInt) {
amount = amount.toFixed(0)
}
newSet.push({
x: f.timeframe,
y: amount
})
})
return {data: newSet}
}
}
});

1
go.mod
View File

@ -24,6 +24,7 @@ require (
github.com/joho/godotenv v1.3.0
github.com/lib/pq v1.2.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/pkg/errors v0.8.1
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect

View File

@ -30,7 +30,7 @@ func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
checkins := core.AllCheckins()
for _, c := range checkins {
c.Hits = c.AllHits()
c.Failures = c.LimitedFailures(64)
c.Failures = c.GetFailures(64)
}
returnJson(checkins, w, r)
}
@ -43,7 +43,7 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
return
}
checkin.Hits = checkin.LimitedHits(32)
checkin.Failures = checkin.LimitedFailures(32)
checkin.Failures = checkin.GetFailures(32)
returnJson(checkin, w, r)
}

47
handlers/errors.go Normal file
View File

@ -0,0 +1,47 @@
package handlers
import (
"encoding/json"
"github.com/pkg/errors"
"net/http"
)
type Error struct {
err error
code int
}
func (e Error) Error() string {
return e.err.Error()
}
var (
NewError = func(e error) Error {
return Error{
err: e,
code: http.StatusInternalServerError,
}
}
NotFound = func(err error) Error {
return Error{
err: errors.Wrap(err, "not found"),
code: http.StatusNotFound,
}
}
Unauthorized = func(e error) Error {
return Error{
err: e,
code: http.StatusUnauthorized,
}
}
)
func RespondError(w http.ResponseWriter, err Error) {
output := apiResponse{
Status: "error",
Error: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(err.code)
json.NewEncoder(w).Encode(output)
}

View File

@ -4,12 +4,23 @@ import (
"github.com/hunterlong/statping/core"
"html/template"
"net/http"
"net/url"
)
var (
basePath = "/"
)
func parseForm(r *http.Request) url.Values {
r.ParseForm()
return r.PostForm
}
func parseGet(r *http.Request) url.Values {
r.ParseForm()
return r.Form
}
var handlerFuncs = func(w http.ResponseWriter, r *http.Request) template.FuncMap {
return template.FuncMap{
"VERSION": func() string {

1
handlers/query.go Normal file
View File

@ -0,0 +1 @@
package handlers

View File

@ -21,6 +21,7 @@ import (
"fmt"
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
@ -151,56 +152,41 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := parseGroupQuery(r)
obj := core.GraphHitsDataRaw(service, groupQuery, "latency")
groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency"))
returnJson(obj, w, r)
}
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := parseGroupQuery(r)
groupQuery := database.ParseQueries(r, service.Failures())
obj := core.GraphFailuresDataRaw(service, groupQuery)
obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount)
returnJson(obj, w, r)
}
func parseGroupQuery(r *http.Request) *types.GroupQuery {
fields := parseGet(r)
grouping := fields.Get("group")
if grouping == "" {
grouping = "hour"
}
startField := utils.ToInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end"))
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) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
sendErrorJson(errors.New("service not found"), w, r)
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := parseGroupQuery(r)
groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphHitsDataRaw(service, groupQuery, "ping_time")
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time"))
returnJson(obj, w, r)
}
@ -301,24 +287,26 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
func apiServiceFailuresHandler(r *http.Request) interface{} {
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
return errors.New("service not found")
}
fails := servicer.LimitedFailures(100)
var fails []types.Failure
service.Failures().Requests(r).Find(&fails)
return fails
}
func apiServiceHitsHandler(r *http.Request) interface{} {
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
return errors.New("service not found")
}
hits, err := servicer.HitsDb(r).Hits()
if err != nil {
return err
}
var hits []types.Hit
service.Hits().Find(&hits)
return hits
}

View File

@ -1,31 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// 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/>.
package handlers
import (
"net/http"
"net/url"
)
func parseForm(r *http.Request) url.Values {
r.ParseForm()
return r.PostForm
}
func parseGet(r *http.Request) url.Values {
r.ParseForm()
return r.Form
}

View File

@ -21,19 +21,19 @@ import (
// Checkin struct will allow an application to send a recurring HTTP GET to confirm a service is online
type Checkin struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
ServiceId int64 `gorm:"index;column:service" json:"service_id"`
Name string `gorm:"column:name" json:"name"`
Interval int64 `gorm:"column:check_interval" json:"interval"`
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Running chan bool `gorm:"-" json:"-"`
Failing bool `gorm:"-" json:"failing"`
LastHit time.Time `gorm:"-" json:"last_hit"`
Hits []*CheckinHit `gorm:"-" json:"hits"`
Failures []FailureInterface `gorm:"-" json:"failures"`
Id int64 `gorm:"primary_key;column:id" json:"id"`
ServiceId int64 `gorm:"index;column:service" json:"service_id"`
Name string `gorm:"column:name" json:"name"`
Interval int64 `gorm:"column:check_interval" json:"interval"`
GracePeriod int64 `gorm:"column:grace_period" json:"grace"`
ApiKey string `gorm:"column:api_key" json:"api_key"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Running chan bool `gorm:"-" json:"-"`
Failing bool `gorm:"-" json:"failing"`
LastHit time.Time `gorm:"-" json:"last_hit"`
Hits []*CheckinHit `gorm:"-" json:"hits"`
Failures []*Failure `gorm:"-" json:"failures"`
}
type CheckinInterface interface {

View File

@ -35,8 +35,6 @@ type Failure struct {
type FailureInterface interface {
Select() *Failure
Ago() string // Ago returns a human readable timestamp
ParseError() string // ParseError returns a human readable error for a service failure
}
// BeforeCreate for Failure will set CreatedAt to UTC

View File

@ -16,6 +16,7 @@
package types
import (
"fmt"
"time"
)
@ -27,11 +28,41 @@ const (
)
var (
NOW = func() time.Time { return time.Now().UTC() }()
//HOUR_1_AGO = time.Now().Add(-1 * time.Hour)
//HOUR_24_AGO = time.Now().Add(-24 * time.Hour)
//HOUR_72_AGO = time.Now().Add(-72 * time.Hour)
//DAY_7_AGO = NOW.AddDate(0, 0, -7)
//MONTH_1_AGO = NOW.AddDate(0, -1, 0)
//YEAR_1_AGO = NOW.AddDate(-1, 0, 0)
Second = time.Second
Minute = time.Minute
Hour = time.Hour
Day = Hour * 24
Week = Day * 7
Month = Week * 4
Year = Day * 365
)
func FixedTime(t time.Time, d time.Duration) string {
switch d {
case Month:
month := fmt.Sprintf("%v", int(t.Month()))
if int(t.Month()) < 10 {
month = fmt.Sprintf("0%v", int(t.Month()))
}
return fmt.Sprintf("%v-%v-01T00:00:00Z", t.Year(), month)
case Year:
return fmt.Sprintf("%v-01-01T00:00:00Z", t.Year())
default:
return t.Format(durationStr(d))
}
}
func durationStr(d time.Duration) string {
switch d {
case Second:
return "2006-01-02T15:04:05Z"
case Minute:
return "2006-01-02T15:04:00Z"
case Hour:
return "2006-01-02T15:00:00Z"
case Day:
return "2006-01-02T00:00:00Z"
default:
return "2006-01-02T00:00:00Z"
}
}

46
types/time_test.go Normal file
View File

@ -0,0 +1,46 @@
package types
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestFixedTime(t *testing.T) {
timeVal, err := time.Parse("2006-01-02T15:04:05Z", "2020-05-22T06:02:13Z")
require.Nil(t, err)
examples := []struct {
Time time.Time
Duration time.Duration
Expected string
}{{
timeVal,
time.Second,
"2020-05-22T06:02:13Z",
}, {
timeVal,
time.Minute,
"2020-05-22T06:02:00Z",
}, {
timeVal,
time.Hour,
"2020-05-22T06:00:00Z",
}, {
timeVal,
time.Hour * 24,
"2020-05-22T00:00:00Z",
}, {
timeVal,
time.Hour * 24 * 30,
"2020-05-01T00:00:00Z",
}}
for _, e := range examples {
assert.Equal(t, e.Expected, FixedTime(e.Time, e.Duration), fmt.Sprintf("reformating for: %v %v", e.Time, e.Duration))
}
}

View File

@ -19,13 +19,6 @@ 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"`
@ -49,7 +42,7 @@ type DbConfig struct {
DbHost string `yaml:"host" json:"-"`
DbUser string `yaml:"user" json:"-"`
DbPass string `yaml:"password" json:"-"`
DbData string `yaml:"Db" json:"-"`
DbData string `yaml:"database" json:"-"`
DbPort int64 `yaml:"port" json:"-"`
ApiKey string `yaml:"api_key" json:"-"`
ApiSecret string `yaml:"api_secret" json:"-"`