pull/429/head
hunterlong 2020-02-24 08:26:01 -08:00
parent f64fc9e682
commit 7cf239125f
15 changed files with 485 additions and 255 deletions

View File

@ -17,6 +17,7 @@ package core
import (
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"golang.org/x/crypto/bcrypt"
@ -56,14 +57,14 @@ func SelectUsername(username string) (*User, error) {
// Delete will remove the User record from the database
func (u *User) Delete() error {
return Database(&User{}).Delete(u).Error()
return database.Delete(&u)
}
// Update will update the User's record in database
func (u *User) Update() error {
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
return Database(&User{}).Update(u).Error()
return database.Update(&u)
}
// Create will insert a new User into the database
@ -72,15 +73,16 @@ func (u *User) Create() (int64, error) {
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
db := Database(&User{}).Create(u)
if db.Error() != nil {
return 0, db.Error()
user, err := database.Create(&u)
if err != nil {
return 0, err
}
if u.Id == 0 {
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, db.Error()))
return 0, db.Error()
if user.Id == 0 {
log.Errorln(fmt.Sprintf("Failed to create User %v. %v", u.Username, err))
return 0, err
}
return u.Id, db.Error()
return u.Id, err
}
// SelectAllUsers returns all users

33
database/checkins.go Normal file
View File

@ -0,0 +1,33 @@
package database
import "github.com/hunterlong/statping/types"
type CheckinObj struct {
*types.Checkin
failures
}
func (o *Object) AsCheckin() HitsFailures {
return &CheckinObj{
Checkin: o.model.(*types.Checkin),
}
}
func Checkin(id int64) (HitsFailures, error) {
var checkin types.Checkin
query := database.Model(&types.Checkin{}).Where("id = ?", id).Find(&checkin)
return &CheckinObj{Checkin: &checkin}, query.Error()
}
func (c *CheckinObj) Hits() *hits {
return &hits{
database.Model(&types.Checkin{}).Where("checkin = ?", c.Id),
}
}
func (c *CheckinObj) Failures() *failures {
return &failures{
database.Model(&types.Failure{}).
Where("method = 'checkin' AND service = ?", c.Id).Order("id desc"),
}
}

50
database/crud.go Normal file
View File

@ -0,0 +1,50 @@
package database
import (
"reflect"
)
type Object struct {
Id int64
model interface{}
db Database
}
type HitsFailures interface {
Hits() *hits
Failures() *failures
}
func modelId(model interface{}) int64 {
iface := reflect.ValueOf(model)
field := iface.Elem().FieldByName("Id")
return field.Int()
}
func toModel(model interface{}) Database {
return database.Model(&model)
}
func Create(data interface{}) (*Object, error) {
model := toModel(data)
query := model.Create(data)
if query.Error() != nil {
return nil, query.Error()
}
obj := &Object{
Id: modelId(data),
model: data,
db: model,
}
return obj, query.Error()
}
func Update(data interface{}) error {
model := toModel(data)
return model.Update(&data).Error()
}
func Delete(data interface{}) error {
model := toModel(data)
return model.Delete(data).Error()
}

32
database/failures.go Normal file
View File

@ -0,0 +1,32 @@
package database
import (
"github.com/hunterlong/statping/types"
)
type failures struct {
DB Database
}
func (f *failures) All() []*types.Failure {
var fails []*types.Failure
f.DB = f.DB.Find(&fails)
return fails
}
func (f *failures) Last(amount int) *types.Failure {
var fail types.Failure
f.DB = f.DB.Limit(amount).Find(&fail)
return &fail
}
func (f *failures) Count() int {
var amount int
f.DB = f.DB.Count(&amount)
return amount
}
func (f *failures) Find(data interface{}) error {
q := f.Find(&data)
return q
}

View File

@ -1,215 +1,26 @@
package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
"net/url"
"strconv"
"time"
)
import "github.com/hunterlong/statping/types"
type GroupBy struct {
db Database
query *GroupQuery
type GroupObj struct {
*types.Group
db Database
}
type GroupByer interface {
ToTimeValue(interface{}) (*TimeVar, error)
type Grouper interface {
Services() Database
}
type By string
func (b By) String() string {
return string(b)
func (o *Object) AsGroup() *types.Group {
return o.model.(*types.Group)
}
type GroupQuery struct {
db Database
Start time.Time
End time.Time
Group string
Order string
Limit int
Offset int
FillEmpty bool
func (it *Db) GetGroup(id int64) (*GroupObj, error) {
var group types.Group
query := it.Model(&types.Group{}).Where("id = ?", id).Find(&group)
return &GroupObj{&group, it}, query.Error()
}
func (b GroupQuery) Database() Database {
return b.db
}
var (
ByCount = By("COUNT(id) as amount")
BySum = func(column string) By {
return By(fmt.Sprintf("SUM(%s) as amount", column))
}
ByAverage = func(column string) By {
return By(fmt.Sprintf("AVG(%s) as amount", column))
}
)
func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer {
dbQuery := db.MultipleSelects(
db.SelectByTime(q.Group),
by.String(),
).Group("timeframe")
return &GroupBy{dbQuery, q}
}
type TimeVar struct {
g *GroupBy
data []*TimeValue
}
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 time.Time
amount := float64(0)
rows.Scan(&timeframe, &amount)
newTs := types.FixedTime(timeframe, g.duration())
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
})
}
return &TimeVar{g, data}, nil
}
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 {
timeMap[v.Timeframe] = v.Amount
}
currentStr := types.FixedTime(current, t.g.duration())
for {
var amount float64
if timeMap[currentStr] != 0 {
amount = timeMap[currentStr]
}
validSet = append(validSet, &TimeValue{
Timeframe: currentStr,
Amount: amount,
})
if current.After(end) {
break
}
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.duration())
}
return validSet
}
func (g *GroupBy) duration() time.Duration {
switch g.query.Group {
case "second":
return types.Second
case "minute":
return types.Minute
case "hour":
return types.Hour
case "day":
return types.Day
case "month":
return types.Month
case "year":
return types.Year
default:
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
func (it *GroupObj) Services() Database {
return it.db.Model(&types.Service{}).Where("service = ?", it.Id)
}

215
database/grouping.go Normal file
View File

@ -0,0 +1,215 @@
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 *GroupQuery
}
type GroupByer interface {
ToTimeValue(interface{}) (*TimeVar, error)
}
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 = By("COUNT(id) as amount")
BySum = func(column string) By {
return By(fmt.Sprintf("SUM(%s) as amount", column))
}
ByAverage = func(column string) By {
return By(fmt.Sprintf("AVG(%s) as amount", column))
}
)
func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer {
dbQuery := db.MultipleSelects(
db.SelectByTime(q.Group),
by.String(),
).Group("timeframe")
return &GroupBy{dbQuery, q}
}
type TimeVar struct {
g *GroupBy
data []*TimeValue
}
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 time.Time
amount := float64(0)
rows.Scan(&timeframe, &amount)
newTs := types.FixedTime(timeframe, g.duration())
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
})
}
return &TimeVar{g, data}, nil
}
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 {
timeMap[v.Timeframe] = v.Amount
}
currentStr := types.FixedTime(current, t.g.duration())
for {
var amount float64
if timeMap[currentStr] != 0 {
amount = timeMap[currentStr]
}
validSet = append(validSet, &TimeValue{
Timeframe: currentStr,
Amount: amount,
})
if current.After(end) {
break
}
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.duration())
}
return validSet
}
func (g *GroupBy) duration() time.Duration {
switch g.query.Group {
case "second":
return types.Second
case "minute":
return types.Minute
case "hour":
return types.Hour
case "day":
return types.Day
case "month":
return types.Month
case "year":
return types.Year
default:
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

@ -1,22 +0,0 @@
package database
import "github.com/hunterlong/statping/types"
type Group struct {
db Database
group *types.Group
}
type Groupser interface {
Services() Database
}
func (it *Db) GetGroup(id int64) (Groupser, error) {
var group types.Group
query := it.Model(&types.Group{}).Where("id = ?", id).Find(&group)
return &Group{it, &group}, query.Error()
}
func (it *Group) Services() Database {
return it.db.Model(&types.Service{}).Where("group = ?", it.group.Id)
}

30
database/hits.go Normal file
View File

@ -0,0 +1,30 @@
package database
import "github.com/hunterlong/statping/types"
type hits struct {
DB Database
}
func (h *hits) All() []*types.Hit {
var fails []*types.Hit
h.DB = h.DB.Find(&fails)
return fails
}
func (h *hits) Last(amount int) *types.Hit {
var hits types.Hit
h.DB = h.DB.Limit(amount).Find(&hits)
return &hits
}
func (h *hits) Count() int {
var amount int
h.DB = h.DB.Count(&amount)
return amount
}
func (h *hits) Find(data interface{}) error {
q := h.Find(&data)
return q
}

18
database/incident.go Normal file
View File

@ -0,0 +1,18 @@
package database
import "github.com/hunterlong/statping/types"
type IncidentObj struct {
*types.Incident
db Database
}
func (o *IncidentObj) AsIncident() *types.Incident {
return o.Incident
}
func Incident(id int64) (*IncidentObj, error) {
var incident types.Incident
query := database.Model(&types.Incident{}).Where("id = ?", id).Find(&incident)
return &IncidentObj{Incident: &incident, db: query}, query.Error()
}

View File

@ -3,25 +3,29 @@ package database
import "github.com/hunterlong/statping/types"
type ServiceObj struct {
db Database
service *types.Service
*types.Service
failures
}
type Servicer interface {
Failures() Database
Hits() Database
func (o *Object) AsService() *types.Service {
return o.model.(*types.Service)
}
func Service(id int64) (Servicer, error) {
func Service(id int64) (HitsFailures, error) {
var service types.Service
query := database.Model(&types.Service{}).Where("id = ?", id).Find(&service)
return &ServiceObj{query, &service}, query.Error()
return &ServiceObj{Service: &service}, query.Error()
}
func (s *ServiceObj) Failures() Database {
return database.Model(&types.Failure{}).Where("service = ?", s.service.Id)
func (s *ServiceObj) Hits() *hits {
return &hits{
database.Model(&types.Hit{}).Where("service = ?", s.Id),
}
}
func (s *ServiceObj) Hits() Database {
return database.Model(&types.Hit{}).Where("service = ?", s.service.Id)
func (s *ServiceObj) Failures() *failures {
return &failures{
database.Model(&types.Failure{}).
Where("method != 'checkin' AND service = ?", s.Id).Order("id desc"),
}
}

19
database/user.go Normal file
View File

@ -0,0 +1,19 @@
package database
import "github.com/hunterlong/statping/types"
type UserObj struct {
*types.User
}
func (o *Object) AsUser() *UserObj {
return &UserObj{
User: o.model.(*types.User),
}
}
func User(id int64) (*UserObj, error) {
var user types.User
query := database.Model(&types.User{}).Where("id = ?", id).Find(&user)
return &UserObj{User: &user}, query.Error()
}

View File

@ -1,6 +1,7 @@
package handlers
import (
"bytes"
"github.com/hunterlong/statping/core"
"html/template"
"net/http"
@ -11,6 +12,39 @@ var (
basePath = "/"
)
type HandlerFunc func(Responder, *Request)
func (f HandlerFunc) ServeHTTP(w Responder, r *Request) {
f(w, r)
}
type Handler interface {
ServeHTTP(Responder, *Request)
}
type Responder struct {
Code int // the HTTP response code from WriteHeader
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
}
type Request struct {
*http.Request
}
func (r Responder) Header() http.Header {
return r.HeaderMap
}
func (r Responder) Write(p []byte) (int, error) {
return r.Body.Write(p)
}
func (r Responder) WriteHeader(statusCode int) {
r.Code = statusCode
}
func parseForm(r *http.Request) url.Values {
r.ParseForm()
return r.PostForm

View File

@ -130,8 +130,8 @@ func readOnly(handler func(w http.ResponseWriter, r *http.Request), redirect boo
}
// cached is a middleware function that accepts a duration and content type and will cache the response of the original request
func cached(duration, contentType string, handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
func cached(duration, contentType string, handler func(w Responder, r *Request)) Responder {
return HandlerFunc(func(w Responder, r *Request) {
content := CacheStorage.Get(r.RequestURI)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Access-Control-Allow-Origin", "*")

View File

@ -150,7 +150,7 @@ func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
sendJsonAction(service, "running", w, r)
}
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
func apiServiceDataHandler(w Responder, r *http.Request) {
vars := mux.Vars(r)
service, err := database.Service(utils.ToInt(vars["id"]))
if err != nil {
@ -158,7 +158,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
return
}
groupQuery := database.ParseQueries(r, service.Hits())
groupQuery := database.ParseQueries(r, service.Hits().DB)
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency"))
returnJson(obj, w, r)
@ -171,7 +171,7 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := database.ParseQueries(r, service.Failures())
groupQuery := database.ParseQueries(r, service.Hits().DB)
obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount)
returnJson(obj, w, r)
@ -184,7 +184,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := database.ParseQueries(r, service.Hits())
groupQuery := database.ParseQueries(r, service.Hits().DB)
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time"))
returnJson(obj, w, r)
@ -274,7 +274,7 @@ func joinServices(srvs []types.ServiceInterface) []*types.Service {
return services
}
func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
func servicesDeleteFailuresHandler(w Responder, r *http.Request) {
vars := mux.Vars(r)
service := core.SelectService(utils.ToInt(vars["id"]))
if service == nil {
@ -293,8 +293,12 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
return errors.New("service not found")
}
service.Hits()
service.Failures()
var fails []types.Failure
service.Failures().Requests(r).Find(&fails)
service.Failures().DB.Requests(r).Find(&fails)
return fails
}

View File

@ -30,11 +30,11 @@ type User struct {
Admin NullBool `gorm:"column:administrator" json:"admin,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
UserInterface `gorm:"-" json:"-"`
DatabaseInter `gorm:"-" json:"-"`
}
// UserInterface interfaces the Db functions
type UserInterface interface {
type DatabaseInter interface {
Create() (int64, error)
Update() error
Delete() error