262 lines
5.3 KiB
Go
262 lines
5.3 KiB
Go
package database
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/statping-ng/statping-ng/types"
|
|
"github.com/statping-ng/statping-ng/utils"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
type GroupBy struct {
|
|
db Database
|
|
query *GroupQuery
|
|
}
|
|
|
|
type GroupByer interface {
|
|
ToTimeValue() (*TimeVar, error)
|
|
}
|
|
|
|
type By string
|
|
|
|
func (b By) String() string {
|
|
return string(b)
|
|
}
|
|
|
|
type GroupQuery struct {
|
|
Start time.Time
|
|
End time.Time
|
|
Group time.Duration
|
|
Order string
|
|
Limit int
|
|
Offset int
|
|
FillEmpty bool
|
|
|
|
db Database
|
|
}
|
|
|
|
func (b GroupQuery) Find(data interface{}) error {
|
|
return b.db.Order("id DESC").Find(data).Error()
|
|
}
|
|
|
|
func (b GroupQuery) Database() Database {
|
|
return b.db
|
|
}
|
|
|
|
var (
|
|
ByCount = By("COUNT(id) as amount")
|
|
ByAverage = func(column string, multiplier int) By {
|
|
switch database.DbType() {
|
|
case "mysql":
|
|
return By(fmt.Sprintf("CAST(AVG(%s) as UNSIGNED INT) as amount", column))
|
|
case "postgres":
|
|
return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
|
|
default:
|
|
return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
|
|
}
|
|
}
|
|
)
|
|
|
|
type TimeVar struct {
|
|
g *GroupQuery
|
|
data []*TimeValue
|
|
}
|
|
|
|
func (t *TimeVar) ToValues() ([]*TimeValue, error) {
|
|
return t.data, nil
|
|
}
|
|
|
|
// GraphData will return all hits or failures
|
|
func (b *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
|
|
b.db = b.db.MultipleSelects(
|
|
b.db.SelectByTime(b.Group),
|
|
by.String(),
|
|
).Group("timeframe").Order("timeframe", true)
|
|
|
|
caller, err := b.ToTimeValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if b.FillEmpty {
|
|
return caller.FillMissing(b.Start, b.End)
|
|
}
|
|
return caller.ToValues()
|
|
}
|
|
|
|
// ToTimeValue will format the SQL rows into a JSON format for the API.
|
|
// [{"timestamp": "2006-01-02T15:04:05Z", "amount": 468293}]
|
|
// TODO redo this entire function, use better SQL query to group by time
|
|
func (b *GroupQuery) ToTimeValue() (*TimeVar, error) {
|
|
rows, err := b.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 {
|
|
log.Error(err, timeframe)
|
|
}
|
|
trueTime, _ := b.db.ParseTime(timeframe)
|
|
newTs := types.FixedTime(trueTime, b.Group)
|
|
|
|
tv := &TimeValue{
|
|
Timeframe: newTs,
|
|
Amount: amount,
|
|
}
|
|
data = append(data, tv)
|
|
}
|
|
return &TimeVar{b, data}, nil
|
|
}
|
|
|
|
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
|
|
timeMap := make(map[string]int64)
|
|
var validSet []*TimeValue
|
|
for _, v := range t.data {
|
|
timeMap[v.Timeframe] = v.Amount
|
|
}
|
|
|
|
for {
|
|
currentStr := types.FixedTime(current, t.g.Group)
|
|
|
|
var amount int64
|
|
if timeMap[currentStr] != 0 {
|
|
amount = timeMap[currentStr]
|
|
}
|
|
|
|
validSet = append(validSet, &TimeValue{
|
|
Timeframe: currentStr,
|
|
Amount: amount,
|
|
})
|
|
current = current.Add(t.g.Group)
|
|
if current.After(end) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return validSet, nil
|
|
}
|
|
|
|
type isObject interface {
|
|
Db() Database
|
|
}
|
|
|
|
func ParseRequest(r *http.Request) (*GroupQuery, error) {
|
|
fields := parseGet(r)
|
|
grouping := fields.Get("group")
|
|
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
|
|
}
|
|
|
|
if grouping == "" {
|
|
grouping = "1h"
|
|
}
|
|
groupDur, err := time.ParseDuration(grouping)
|
|
if err != nil {
|
|
log.Errorln(err)
|
|
groupDur = 1 * time.Hour
|
|
}
|
|
|
|
query := &GroupQuery{
|
|
Start: time.Unix(startField, 0).UTC(),
|
|
End: time.Unix(endField, 0).UTC(),
|
|
Group: groupDur,
|
|
Order: orderBy,
|
|
Limit: int(limit),
|
|
Offset: int(offset),
|
|
FillEmpty: fill,
|
|
}
|
|
|
|
if query.Start.After(query.End) {
|
|
return nil, errors.New("start time is after ending time")
|
|
}
|
|
|
|
return query, nil
|
|
}
|
|
|
|
func ParseQueries(r *http.Request, o isObject) (*GroupQuery, error) {
|
|
fields := parseGet(r)
|
|
grouping := fields.Get("group")
|
|
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
|
|
}
|
|
|
|
q := o.Db()
|
|
|
|
if grouping == "" {
|
|
grouping = "1h"
|
|
}
|
|
groupDur, err := time.ParseDuration(grouping)
|
|
if err != nil {
|
|
log.Errorln(err)
|
|
groupDur = 1 * time.Hour
|
|
}
|
|
if endField == 0 {
|
|
endField = utils.Now().Unix()
|
|
}
|
|
|
|
query := &GroupQuery{
|
|
Start: time.Unix(startField, 0).UTC(),
|
|
End: time.Unix(endField, 0).UTC(),
|
|
Group: groupDur,
|
|
Order: orderBy,
|
|
Limit: int(limit),
|
|
Offset: int(offset),
|
|
FillEmpty: fill,
|
|
db: q,
|
|
}
|
|
|
|
if query.Start.After(query.End) {
|
|
return nil, errors.New("start time is after ending time")
|
|
}
|
|
|
|
if startField == 0 {
|
|
query.Start = utils.Now().Add(-7 * types.Day)
|
|
}
|
|
if endField == 0 {
|
|
query.End = utils.Now()
|
|
}
|
|
if query.Limit != 0 {
|
|
q = q.Limit(query.Limit)
|
|
}
|
|
if query.Offset > 0 {
|
|
q = q.Offset(query.Offset)
|
|
}
|
|
|
|
q = q.Where("created_at BETWEEN ? AND ?", q.FormatTime(query.Start), q.FormatTime(query.End))
|
|
|
|
if query.Order != "" {
|
|
q = q.Order(query.Order)
|
|
}
|
|
query.db = q
|
|
|
|
return query, nil
|
|
}
|
|
|
|
func parseForm(r *http.Request) url.Values {
|
|
r.ParseForm()
|
|
return r.PostForm
|
|
}
|
|
|
|
func parseGet(r *http.Request) url.Values {
|
|
r.ParseForm()
|
|
return r.Form
|
|
}
|