pull/429/head
hunterlong 2020-02-22 15:52:05 -08:00
parent 05c77a5e98
commit 871424f9c3
13 changed files with 309 additions and 199 deletions

View File

@ -19,6 +19,7 @@ import (
"fmt"
"github.com/go-yaml/yaml"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/jinzhu/gorm"
@ -32,7 +33,7 @@ import (
var (
// DbSession stores the Statping database session
DbSession types.Database
DbSession database.Database
DbModels []interface{}
)
@ -47,7 +48,7 @@ func init() {
// DbConfig stores the config.yml file for the statup configuration
type DbConfig types.DbConfig
func Database(obj interface{}) types.Database {
func Database(obj interface{}) database.Database {
switch obj.(type) {
case *types.Service, *Service, []*Service:
return DbSession.Model(&types.Service{})
@ -77,7 +78,7 @@ func Database(obj interface{}) types.Database {
}
// HitsBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) types.Database {
func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) database.Database {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return Database(&Hit{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
@ -87,7 +88,7 @@ func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) typ
}
// FailuresBetween returns the gorm database query for a collection of service hits between a time range
func (s *Service) FailuresBetween(t1, t2 time.Time, group string, column string) types.Database {
func (s *Service) FailuresBetween(t1, t2 time.Time, group string, column string) database.Database {
selector := Dbtimestamp(group, column)
if CoreApp.Config.DbConn == "postgres" {
return Database(&Failure{}).Select(selector).Where("service = ? AND created_at BETWEEN ? AND ?", s.Id, t1.UTC().Format(types.TIME), t2.UTC().Format(types.TIME))
@ -226,7 +227,7 @@ func (c *Core) Connect(retry bool, location string) error {
conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", CoreApp.Config.DbUser, CoreApp.Config.DbPass, host, CoreApp.Config.DbData)
}
log.WithFields(utils.ToFields(c, conn)).Debugln("attempting to connect to database")
dbSession, err := types.Openw(dbType, conn)
dbSession, err := database.Openw(dbType, conn)
if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %v", err))
if retry {

View File

@ -18,6 +18,7 @@ package core
import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"net/http"
"sort"
@ -53,7 +54,7 @@ func (s *Service) CreateFailure(f *types.Failure) (int64, error) {
// AllFailures will return all failures attached to a service
func (s *Service) AllFailures() []types.Failure {
var fails []types.Failure
err := DbSession.Failures(s.Id).Find(&fails)
err := Database(&types.Failure{}).Find(&fails)
if err.Error() != nil {
log.Errorln(fmt.Sprintf("Issue getting failures for service %v, %v", s.Name, err))
return nil
@ -61,7 +62,7 @@ func (s *Service) AllFailures() []types.Failure {
return fails
}
func (s *Service) FailuresDb(r *http.Request) types.Database {
func (s *Service) FailuresDb(r *http.Request) database.Database {
return Database(&types.Failure{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
}

View File

@ -16,6 +16,7 @@
package core
import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"net/http"
"time"
@ -51,7 +52,7 @@ func (s *Service) HitsQuery(r *http.Request) ([]*types.Hit, error) {
return hits, err.Error()
}
func (s *Service) HitsDb(r *http.Request) types.Database {
func (s *Service) HitsDb(r *http.Request) database.Database {
return Database(&types.Hit{}).Where("service = ?", s.Id).QuerySearch(r).Order("id desc")
}

View File

@ -19,6 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
)
@ -26,7 +27,7 @@ import (
var (
Integrations []types.Integrator
log = utils.Log.WithField("type", "integration")
db types.Database
db database.Database
)
//func init() {
@ -38,12 +39,12 @@ var (
//}
// integrationsDb returns the 'integrations' database column
func integrationsDb() types.Database {
func integrationsDb() database.Database {
return db.Model(&types.Integration{})
}
// SetDB is called by core to inject the database for a integrator to use
func SetDB(d types.Database) {
func SetDB(d database.Database) {
db = d
}
@ -106,7 +107,7 @@ func Find(name string) (types.Integrator, error) {
}
// db will return the notifier database column/record
func integratorDb(n *types.Integration) types.Database {
func integratorDb(n *types.Integration) database.Database {
return db.Model(&types.Integration{}).Where("name = ?", n.Name).Find(n)
}

View File

@ -19,6 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"reflect"
@ -30,7 +31,7 @@ var (
// AllCommunications holds all the loaded notifiers
AllCommunications []types.AllNotifiers
// db holds the Statping database connection
db types.Database
db database.Database
timezone float32
log = utils.Log.WithField("type", "notifier")
)
@ -111,12 +112,12 @@ func (n *Notification) CanTest() bool {
}
// db will return the notifier database column/record
func modelDb(n *Notification) types.Database {
func modelDb(n *Notification) database.Database {
return db.Model(&Notification{}).Where("method = ?", n.Method).Find(n)
}
// SetDB is called by core to inject the database for a notifier to use
func SetDB(d types.Database) {
func SetDB(d database.Database) {
db = d
}

View File

@ -20,6 +20,7 @@ import (
"fmt"
"github.com/ararog/timeago"
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sort"
@ -268,41 +269,37 @@ func (s *Service) Downtime() time.Duration {
// DateScanObj struct is for creating the charts.js graph JSON array
type DateScanObj struct {
Array []*types.DateScan `json:"data"`
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) *DateScanObj {
func GraphHitsDataRaw(service types.ServiceInterface, query *types.GroupQuery, column string) []*database.TimeValue {
srv := service.(*Service)
dbQuery := Database(&types.Hit{}).
dbQuery, err := Database(&types.Hit{}).
Where("service = ?", srv.Id).
Between(query.Start, query.End).
MultipleSelects(Database(&types.Hit{}).SelectByTime(query.Group), types.CountAmount()).
GroupByTimeframe()
GroupQuery(query).ToTimeValue()
outgoing, err := dbQuery.ToChart()
if err != nil {
log.Error(err)
return nil
}
return &DateScanObj{outgoing}
return dbQuery.FillMissing()
}
// GraphDataRaw will return all the hits between 2 times for a Service
func GraphFailuresDataRaw(service types.ServiceInterface, query types.GroupQuery) []*types.TimeValue {
func GraphFailuresDataRaw(service types.ServiceInterface, query *types.GroupQuery) []*database.TimeValue {
srv := service.(*Service)
dbQuery := Database(&types.Failure{}).
dbQuery, err := Database(&types.Failure{}).
Where("service = ?", srv.Id).
Between(query.Start, query.End).
MultipleSelects(Database(&types.Failure{}).SelectByTime(query.Group), types.CountAmount()).
GroupByTimeframe()
GroupQuery(query).ToTimeValue()
outgoing, err := dbQuery.ToTimeValue(query.Start, query.End)
if err != nil {
log.Error(err)
return nil
}
return outgoing
return dbQuery.FillMissing()
}
// ToString will convert the DateScanObj into a JSON string for the charts to render

View File

@ -1,23 +1,9 @@
// Statup
// Copyright (C) 2020. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// 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 types
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"
@ -28,6 +14,13 @@ import (
"time"
)
const (
TIME_NANO = "2006-01-02T15:04:05Z"
TIME = "2006-01-02 15:04:05"
CHART_TIME = "2006-01-02T15:04:05.999999-07:00"
TIME_DAY = "2006-01-02"
)
// Database is an interface which DB implements
type Database interface {
Close() error
@ -110,58 +103,16 @@ type Database interface {
Since(time.Time) Database
Between(time.Time, time.Time) Database
Hits() ([]*Hit, error)
Hits() ([]*types.Hit, error)
ToChart() ([]*DateScan, error)
GroupByTimeframe() Database
ToTimeValue(time.Time, time.Time) ([]*TimeValue, error)
SelectByTime(string) string
MultipleSelects(args ...string) Database
Failurer
}
FormatTime(t time.Time) string
ParseTime(t string) (time.Time, error)
type Failurer interface {
Failures(id int64) Database
Fails() ([]*Failure, error)
}
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"
case "minute":
return "%Y-%m-%d %H:%M: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"
}
GroupQuery(query *types.GroupQuery) GroupByer
}
func (it *Db) MultipleSelects(args ...string) Database {
@ -173,31 +124,6 @@ func CountAmount() string {
return fmt.Sprintf("COUNT(id) as amount")
}
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 {
return it.Group("timeframe")
}
func (it *Db) Failures(id int64) Database {
return it.Model(&Failure{}).Where("service = ?", id).Not("method = 'checkin'").Order("id desc")
}
func (it *Db) Fails() ([]*Failure, error) {
var fails []*Failure
err := it.Find(&fails)
return fails, err.Error()
}
type Db struct {
Database *gorm.DB
Type string
@ -513,18 +439,18 @@ func (it *Db) Error() error {
return it.Database.Error
}
func (it *Db) Hits() ([]*Hit, error) {
var hits []*Hit
func (it *Db) Hits() ([]*types.Hit, error) {
var hits []*types.Hit
err := it.Find(&hits)
return hits, err.Error()
}
func (it *Db) Since(ago time.Time) Database {
return it.Where("created_at > ?", ago.UTC().Format(TIME))
return it.Where("created_at > ?", it.FormatTime(ago))
}
func (it *Db) Between(t1 time.Time, t2 time.Time) Database {
return it.Where("created_at BETWEEN ? AND ?", t1.UTC().Format(TIME), t2.UTC().Format(TIME))
return it.Where("created_at BETWEEN ? AND ?", it.FormatTime(t1), it.FormatTime(t2))
}
// DateScan struct is for creating the charts.js graph JSON array
@ -538,75 +464,6 @@ type TimeValue struct {
Amount int64 `json:"amount"`
}
func (it *Db) ToTimeValue(start, end time.Time) ([]*TimeValue, error) {
rows, err := it.Database.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, _ := time.Parse(TIME, timeframe)
fmt.Println("got: ", createdTime.UTC(), amount)
data = append(data, &TimeValue{
Timeframe: createdTime.UTC(),
Amount: amount,
})
}
return it.fillMissing(data, start, end), nil
}
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 {
re, _ := time.Parse("2006-01-02T00:00:00Z", t)
return re.UTC()
}
func (it *Db) fillMissing(vals []*TimeValue, start, end time.Time) []*TimeValue {
timeMap := make(map[string]*TimeValue)
var validSet []*TimeValue
for _, v := range vals {
timeMap[it.FormatTime(v.Timeframe)] = v
}
current := start.UTC()
maxTime := end
for {
amount := int64(0)
currentStr := it.FormatTime(current)
if timeMap[currentStr] != nil {
amount = timeMap[currentStr].Amount
}
validSet = append(validSet, &TimeValue{
Timeframe: reparseTime(currentStr),
Amount: amount,
})
if current.After(maxTime) {
break
}
current = current.Add(24 * time.Hour)
}
return validSet
}
func (it *Db) ToChart() ([]*DateScan, error) {
rows, err := it.Database.Rows()
if err != nil {

129
database/group.go Normal file
View File

@ -0,0 +1,129 @@
package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"time"
)
type GroupBy struct {
db Database
query *types.GroupQuery
}
type GroupByer interface {
ToTimeValue() (*TimeVar, error)
}
type GroupMethod interface {
}
var (
ByCount = func() GroupMethod {
return fmt.Sprintf("COUNT(id) as amount")
}
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)
}
)
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(query *types.GroupQuery) GroupByer {
return &GroupBy{execute(db, query), query}
}
type TimeVar struct {
g *GroupBy
data []*TimeValue
}
func (g *GroupBy) ToTimeValue() (*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)
data = append(data, &TimeValue{
Timeframe: createdTime,
Amount: amount,
})
}
return &TimeVar{g, data}, nil
}
func (t *TimeVar) Values() []*TimeValue {
var validSet []*TimeValue
for _, v := range t.data {
validSet = append(validSet, &TimeValue{
Timeframe: v.Timeframe,
Amount: v.Amount,
})
}
return validSet
}
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
}
validSet = append(validSet, &TimeValue{
Timeframe: current,
Amount: amount,
})
if current.After(maxTime) {
break
}
current = current.Add(t.g.duration())
}
return validSet
}
func (g *GroupBy) duration() time.Duration {
switch g.query.Group {
case "second":
return time.Second
case "minute":
return time.Minute
case "hour":
return time.Hour
case "day":
return time.Hour * 24
case "month":
return time.Hour * 730
case "year":
return time.Hour * 8760
default:
return time.Hour
}
}

22
database/groups.go Normal file
View File

@ -0,0 +1,22 @@
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)
}

27
database/service.go Normal file
View File

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

72
database/time.go Normal file
View File

@ -0,0 +1,72 @@
package database
import (
"fmt"
"time"
)
type TimeGroup interface {
}
func (it *Db) ParseTime(t string) (time.Time, error) {
switch it.Type {
case "mysql":
return time.Parse("2006-01-02 15:04:05", t)
case "postgres":
return time.Parse("2006-01-02T15:04:05Z", t)
default:
return time.Parse("2006-01-02 15:04:05", t)
}
}
func (it *Db) FormatTime(t time.Time) string {
switch it.Type {
case "mysql":
return t.UTC().Format("2006-01-02 15:04:05")
case "postgres":
return t.UTC().Format("2006-01-02 15:04:05.999999999")
default:
return t.UTC().Format("2006-01-02 15:04:05")
}
}
func (it *Db) SelectByTime(increment string) string {
switch it.Type {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%s')) AS timeframe", it.correctTimestamp(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", it.correctTimestamp(increment))
}
}
func (it *Db) correctTimestamp(increment string) string {
var timestamper string
switch increment {
case "second":
timestamper = "%Y-%m-%d %H:%M:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%M:00"
case "hour":
timestamper = "%Y-%m-%d %H:00:00"
case "day":
timestamper = "%Y-%m-%d 00:00:00"
case "month":
timestamper = "%Y-%m-01 00:00:00"
case "year":
timestamper = "%Y-01-01 00:00:00"
default:
timestamper = "%Y-%m-%d 00:00:00"
}
switch it.Type {
case "mysql":
case "second":
timestamper = "%Y-%m-%d %H:%i:%S"
case "minute":
timestamper = "%Y-%m-%d %H:%i:00"
}
return timestamper
}

View File

@ -126,14 +126,18 @@
visible: function(newVal, oldVal) {
if (newVal && !this.showing) {
this.showing = true
this.chartHits()
this.chartHits("hour")
}
}
},
methods: {
async chartHits() {
async chartHits(group) {
const start = this.nowSubtract((3600 * 24) * 7)
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
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")
}
this.series = [{
name: this.service.name,
...this.data

View File

@ -175,7 +175,7 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
returnJson(obj, w, r)
}
func parseGroupQuery(r *http.Request) types.GroupQuery {
func parseGroupQuery(r *http.Request) *types.GroupQuery {
fields := parseGet(r)
grouping := fields.Get("group")
if grouping == "" {
@ -184,7 +184,7 @@ func parseGroupQuery(r *http.Request) types.GroupQuery {
startField := utils.ToInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end"))
return types.GroupQuery{
return &types.GroupQuery{
Start: time.Unix(startField, 0).UTC(),
End: time.Unix(endField, 0).UTC(),
Group: grouping,
@ -305,10 +305,7 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
if servicer == nil {
return errors.New("service not found")
}
fails, err := servicer.FailuresDb(r).Fails()
if err != nil {
return err
}
fails := servicer.LimitedFailures(100)
return fails
}